commit f57af319690f49d2a4544a643c1dd4d6b1ab79ac Author: YuTian <2953516620@qq.com> Date: Sat Jul 27 17:08:05 2024 +0800 1.0 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3a27527 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/.idea/ +/target/ +/ElementOriginLib.iml diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..409f972 --- /dev/null +++ b/pom.xml @@ -0,0 +1,140 @@ + + + 4.0.0 + + com.io.yutian.elementoriginlib + ElementOriginLib + 1.0-SNAPSHOT + jar + + ElementOriginLib + + + 17 + UTF-8 + + + + clean package + + + org.apache.maven.plugins + maven-compiler-plugin + 3.13.0 + + ${java.version} + ${java.version} + + + + org.apache.maven.plugins + maven-shade-plugin + 3.5.3 + + + package + + shade + + + + + + + + src/main/resources + true + + + + + + + papermc-repo + https://repo.papermc.io/repository/maven-public/ + + + sonatype + https://oss.sonatype.org/content/groups/public/ + + + public + https://repo.aurora-pixels.com/repository/public/ + + + public-rpg + https://repo.aurora-pixels.com/repository/public-rpg/ + + + spigotmc-repo + https://hub.spigotmc.org/nexus/content/repositories/snapshots/ + + + rapture-snapshots + https://repo.rapture.pw/repository/maven-snapshots/ + + + + + + io.papermc.paper + paper-api + 1.20.1-R0.1-SNAPSHOT + provided + + + org.spigotmc + spigot + 1.20.1 + nms + provided + + + com.mojang + authlib + 3.3.39 + provided + + + com.mojang + brigadier + 1.0.18 + provided + + + com.mojang + datafixerupper + 4.1.27 + provided + + + org.apache.commons + commons-compress + 1.26.2 + + + com.github.luben + zstd-jni + 1.5.6-3 + + + com.zaxxer + HikariCP + 4.0.3 + + + + org.xerial + sqlite-jdbc + 3.46.0.0 + + + + redis.clients + jedis + 5.1.3 + + + diff --git a/src/main/java/com/io/yutian/elementoriginlib/ElementOriginLib.java b/src/main/java/com/io/yutian/elementoriginlib/ElementOriginLib.java new file mode 100644 index 0000000..b970359 --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/ElementOriginLib.java @@ -0,0 +1,21 @@ +package com.io.yutian.elementoriginlib; + +import org.bukkit.plugin.java.JavaPlugin; + +public final class ElementOriginLib extends JavaPlugin { + + private static ElementOriginLib instance; + + @Override + public void onEnable() { + } + + @Override + public void onDisable() { + } + + public static ElementOriginLib inst() { + return instance; + } + +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/command/Command.java b/src/main/java/com/io/yutian/elementoriginlib/command/Command.java new file mode 100644 index 0000000..6c6972e --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/command/Command.java @@ -0,0 +1,8 @@ +package com.io.yutian.elementoriginlib.command; + +@FunctionalInterface +public interface Command { + + void run(S context); + +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/command/CommandContext.java b/src/main/java/com/io/yutian/elementoriginlib/command/CommandContext.java new file mode 100644 index 0000000..f2d2f05 --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/command/CommandContext.java @@ -0,0 +1,38 @@ +package com.io.yutian.elementoriginlib.command; + +import com.io.yutian.elementoriginlib.command.argument.ArgumentValue; +import org.bukkit.command.CommandSender; + +import java.util.Map; + +public class CommandContext { + + private String command; + private String label; + private CommandSender sender; + private Map argumentsValues; + + public CommandContext(String command, String label, CommandSender sender, Map argumentsValues) { + this.command = command; + this.label = label; + this.sender = sender; + this.argumentsValues = argumentsValues; + } + + public String getCommand() { + return command; + } + + public String getLabel() { + return label; + } + + public CommandSender getSender() { + return sender; + } + + public ArgumentValue getArgumentsValue(String key) { + return argumentsValues.getOrDefault(key, new ArgumentValue(null)); + } + +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/command/CommandNode.java b/src/main/java/com/io/yutian/elementoriginlib/command/CommandNode.java new file mode 100644 index 0000000..f5d2086 --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/command/CommandNode.java @@ -0,0 +1,92 @@ +package com.io.yutian.elementoriginlib.command; + +import com.io.yutian.elementoriginlib.command.argument.Argument; +import org.bukkit.command.CommandSender; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.function.Predicate; + +public class CommandNode { + + private String name; + + private List childrens = new ArrayList<>(); + private List arguments = new ArrayList<>(); + + private List alias = new ArrayList<>(); + + private Predicate commandSenderPredicate = (commandSender -> true); + private Command command; + + public CommandNode(String name) { + this.name = name; + } + + public CommandNode(String name, String[] alias) { + this.name = name; + this.alias = Arrays.asList(alias); + } + + public CommandNode(String name, List alias) { + this.name = name; + this.alias = alias; + } + + public CommandNode permission(Predicate commandSenderPredicate) { + this.commandSenderPredicate = commandSenderPredicate; + return this; + } + + public List getAlias() { + return alias; + } + + public CommandNode setAlias(List alias) { + this.alias = alias; + return this; + } + + public CommandNode addAilas(String alias) { + this.alias.add(alias); + return this; + } + + public CommandNode addArgument(Argument argument) { + arguments.add(argument); + return this; + } + + public CommandNode addChildren(CommandNode commandNode) { + this.childrens.add(commandNode); + return this; + } + + public List getChildrens() { + return childrens; + } + + public List getArguments() { + return arguments; + } + + @Deprecated + public CommandNode executes(Command command) { + this.command = command; + return this; + } + + public Command getCommand() { + return command; + } + + public String getName() { + return name; + } + + public static CommandNode node(String name) { + return new CommandNode(name); + } + +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/command/IAlias.java b/src/main/java/com/io/yutian/elementoriginlib/command/IAlias.java new file mode 100644 index 0000000..4f8fe79 --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/command/IAlias.java @@ -0,0 +1,7 @@ +package com.io.yutian.elementoriginlib.command; + +public interface IAlias { + + String[] getAlias(); + +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/command/ICommand.java b/src/main/java/com/io/yutian/elementoriginlib/command/ICommand.java new file mode 100644 index 0000000..e5e914c --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/command/ICommand.java @@ -0,0 +1,67 @@ +package com.io.yutian.elementoriginlib.command; + +import com.io.yutian.elementoriginlib.command.argument.Argument; +import org.bukkit.command.CommandSender; + +import java.util.ArrayList; +import java.util.List; + +public abstract class ICommand { + + private String name; + private String description; + + private List commandNodes = new ArrayList<>(); + private List arguments = new ArrayList<>(); + + public ICommand(String name) { + this(name, null); + } + + public ICommand(String name, String description) { + this.name = name; + this.description = description; + } + + public void executes(CommandContext commandContext) { + } + + public boolean emptyExecutes(CommandSender commandSender) { + return false; + } + + public boolean hasPermission(CommandSender sender) { + return sender.isOp() || sender.hasPermission(getPermissionPrefix()+"."+name); + } + + public String getPermissionPrefix() { + return "command."+name; + } + + public ICommand addArgument(Argument argument) { + arguments.add(argument); + return this; + } + + public ICommand addCommandNode(CommandNode commandNode) { + this.commandNodes.add(commandNode); + return this; + } + + public List getCommandNodes() { + return commandNodes; + } + + public List getArguments() { + return arguments; + } + + public String getName() { + return name; + } + + public String getDescription() { + return description; + } + +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/command/ICommandManager.java b/src/main/java/com/io/yutian/elementoriginlib/command/ICommandManager.java new file mode 100644 index 0000000..9b7c580 --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/command/ICommandManager.java @@ -0,0 +1,11 @@ +package com.io.yutian.elementoriginlib.command; + +import java.util.List; + +public interface ICommandManager { + + String getName(); + + List getCommands(); + +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/command/ITabCompleter.java b/src/main/java/com/io/yutian/elementoriginlib/command/ITabCompleter.java new file mode 100644 index 0000000..41fb3d0 --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/command/ITabCompleter.java @@ -0,0 +1,29 @@ +package com.io.yutian.elementoriginlib.command; + +import org.bukkit.Bukkit; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +import java.util.ArrayList; +import java.util.List; + +public interface ITabCompleter { + + List onTabComplete(CommandSender commandSender, String[] args, int index, String lastArg); + + static List getPlayerList(String arg) { + List list = new ArrayList<>(); + for (Player player : Bukkit.getOnlinePlayers()) { + String name = player.getName(); + if (arg != null && !arg.trim().equalsIgnoreCase("")) { + if (name.toLowerCase().startsWith(arg.toLowerCase())) { + list.add(name); + } + } else { + list.add(name); + } + } + return list; + } + +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/command/SimpleCommandHandler.java b/src/main/java/com/io/yutian/elementoriginlib/command/SimpleCommandHandler.java new file mode 100644 index 0000000..3a90352 --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/command/SimpleCommandHandler.java @@ -0,0 +1,295 @@ +package com.io.yutian.elementoriginlib.command; + +import com.io.yutian.elementoriginlib.command.argument.Argument; +import com.io.yutian.elementoriginlib.command.argument.ArgumentValue; +import com.io.yutian.elementoriginlib.command.handler.CommandHandler; +import com.io.yutian.elementoriginlib.lang.Lang; +import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; + +import java.util.*; +import java.util.stream.Stream; + +public class SimpleCommandHandler extends Command { + + private ICommand iCommand; + + public SimpleCommandHandler(String name, ICommand iCommand) { + super(name); + this.iCommand = iCommand; + } + + @Override + public boolean execute(CommandSender sender, String commandLabel, String[] args) { + executes(sender, commandLabel, args); + return true; + } + + public void executes(CommandSender sender, String commandLabel, String[] args) { + if (!iCommand.hasPermission(sender)) { + sender.sendMessage(Lang.get("command-no-permission")); + return; + } + List commandNodes = iCommand.getCommandNodes(); + StringBuilder stringBuilder = new StringBuilder(); + for (int i = 0; i < args.length; i++) { + stringBuilder.append(args[i]); + if (i < args.length - 1) { + stringBuilder.append(" "); + } + } + String commandString = stringBuilder.toString(); + if (commandNodes.size() == 0) { + Map map = new HashMap<>(); + if (iCommand.getArguments().size() > 0) { + int argSize = args.length; + List arguments = iCommand.getArguments(); + int k = 0; + if (arguments.get(arguments.size()-1).isOptional()) { + k++; + } + if (argSize < arguments.size()-k) { + sender.sendMessage(Lang.get("command-short-arg")); + return; + } + for (int l = 0; l < arguments.size(); l++) { + Argument a = arguments.get(l); + int index = l; + if (index >= args.length) { + break; + } + String arg = args[index]; + if (!a.getArgumentsType().test(arg)) { + sender.sendMessage(Lang.get("command-unknown-arg", index+1, arg)); + return; + } + } + map = parseArgumentValue(sender, arguments, args, -1); + } + iCommand.executes(new CommandContext(commandString, commandLabel, sender, map)); + return; + } + + int nodeSize = args.length; + + if (commandNodes.size() > 0 && nodeSize == 0) { + if (!iCommand.emptyExecutes(sender)) { + sender.sendMessage(Lang.get("command-short-arg")); + } + return; + } + + String mainNode = args[0]; + + Stream nodeStream = commandNodes.stream().filter((n) -> { + return n.getName().equalsIgnoreCase(mainNode) || n.getAlias().contains(mainNode); + }); + Optional nodeOptional = nodeStream.findFirst(); + if (!nodeOptional.isPresent()) { + sender.sendMessage(Lang.get("command-unknown-arg", 1, mainNode)); + return; + } + CommandNode node = nodeOptional.get(); + + if (node.getChildrens().size() > 0) { + checkClidren(commandString, commandLabel, sender, 0, args, node); + } else { + if (node.getCommand() != null) { + Map map = new HashMap<>(); + if (node.getArguments().size() > 0) { + int argSize = args.length - 1; + List arguments = node.getArguments(); + int k = 0; + if (arguments.get(arguments.size()-1).isOptional()) { + k++; + } + if (argSize < arguments.size()-k) { + sender.sendMessage(Lang.get("command-short-arg")); + return; + } + for (int l = 0; l < arguments.size(); l++) { + Argument a = arguments.get(l); + int index = l + 1; + if (index >= args.length) { + break; + } + String arg = args[index]; + if (!a.getArgumentsType().test(arg)) { + sender.sendMessage(Lang.get("command-error-arg", index+1, arg)); + return; + } + } + map = parseArgumentValue(sender, node.getArguments(), args, 0); + } + node.getCommand().run(new CommandContext(commandString, commandLabel, sender, map)); + } else { + sender.sendMessage(Lang.get("command-unknown-arg", 2, mainNode)); + return; + } + } + + } + + private void checkClidren(String commandString, String commandLabel, CommandSender sender, int i, String[] args, CommandNode node) { + i++; + if (i >= args.length) { + if (node.getCommand() == null) { + sender.sendMessage(Lang.get("command-short-arg")); + } else { + node.getCommand().run(new CommandContext(commandString, commandLabel, sender, new HashMap<>())); + } + return; + } + String s = args[i]; + Stream nodeStream = node.getChildrens().stream().filter((n) -> { + return n.getName().equalsIgnoreCase(s) || n.getAlias().contains(s); + }); + Optional nodeOptional = nodeStream.findFirst(); + if (!nodeOptional.isPresent()) { + sender.sendMessage(Lang.get("command-unknown-arg", i+1, s)); + return; + } + CommandNode node1 = nodeOptional.get(); + + if (node1.getChildrens().size() > 0) { + checkClidren(commandString, commandLabel, sender, i, args, node1); + } else { + if (node1.getCommand() != null) { + Map map = new HashMap<>(); + if (node1.getArguments().size() > 0) { + int argSize = args.length - i - 1; + List arguments = node1.getArguments(); + int k = 0; + if (arguments.get(arguments.size()-1).isOptional()) { + k++; + } + if (argSize < arguments.size()-k) { + sender.sendMessage(Lang.get("command-short-arg")); + return; + } + for (int l = 0; l < arguments.size(); l++) { + Argument a = arguments.get(l); + int index = i + l + 1; + if (index >= args.length) { + break; + } + String arg = args[index]; + if (!a.getArgumentsType().test(arg)) { + sender.sendMessage(Lang.get("command-unknown-arg", index+1, arg)); + return; + } + } + map = parseArgumentValue(sender, node1.getArguments(), args, i); + } + node1.getCommand().run(new CommandContext(commandString, commandLabel, sender, map)); + } else { + sender.sendMessage(Lang.get("command-unknown-arg", i+1, s)); + return; + } + } + + } + + private Map parseArgumentValue(CommandSender commandSender, List argumentList, String[] args, int i) { + Map map = new HashMap<>(); + List arguments = argumentList; + for (int l = 0; l < arguments.size(); l++) { + Argument a = arguments.get(l); + if (i+1+l >= args.length) { + if (a.isOptional()) { + map.put(a.getName(), new ArgumentValue(a.getDefaultValue())); + } + return map; + } + String arg = args[i+1+l]; + if (!a.getArgumentsType().test(arg)) { + continue; + } + ArgumentValue argumentValue = new ArgumentValue(a.getArgumentsType().get(arg)); + map.put(a.getName(), argumentValue); + } + return map; + } + + @Override + public List tabComplete(CommandSender sender, String alias, String[] args) throws IllegalArgumentException { + return onTabComplete(sender, alias, args); + } + + public List onTabComplete(CommandSender sender, String alias, String[] args) { + List list = new ArrayList<>(); + int index = args.length; + String arg = args[index-1]; + if (!iCommand.hasPermission(sender)) { + return list; + } + if (iCommand instanceof ITabCompleter) { + ITabCompleter tabCompleter = (ITabCompleter) iCommand; + return tabCompleter.onTabComplete(sender, args, index-1, arg); + } else { + Map> map = new HashMap<>(); + if (iCommand.getCommandNodes().size() > 0) { + List list1 = new ArrayList<>(); + for (CommandNode node : iCommand.getCommandNodes()) { + list1.add(node.getName()); + list1.addAll(node.getAlias()); + if (index >= 2) { + if (!node.getName().equalsIgnoreCase(args[0])) { + continue; + } + } + if (node.getChildrens().size() > 0) { + getTabComplete(node, 1, map); + } else if (node.getArguments().size() > 0) { + List arguments = node.getArguments(); + for (int l = 0; l < arguments.size(); l++) { + Argument argument = arguments.get(l); + if (argument.getSuggest() != null) { + map.put(2+l, argument.getSuggest().getSuggest()); + continue; + } + map.put(2+l, Arrays.asList("<"+argument.getName()+">")); + } + } + } + map.put(1, list1); + return CommandHandler.preseSuggest(map.getOrDefault(index, list), arg); + } else if (iCommand.getArguments().size() > 0) { + List arguments = iCommand.getArguments(); + for (int l = 0; l < arguments.size(); l++) { + Argument argument = arguments.get(l); + if (argument.getSuggest() != null) { + map.put(0+l+1, argument.getSuggest().getSuggest()); + continue; + } + map.put(0+l+1, Arrays.asList("<"+argument.getName()+">")); + } + return CommandHandler.preseSuggest(map.getOrDefault(index, list), arg); + } + } + return CommandHandler.preseSuggest(list, arg); + } + + private void getTabComplete(CommandNode node, int i, Map> map) { + i++; + List list = map.getOrDefault(i, new ArrayList<>()); + for (CommandNode c : node.getChildrens()) { + list.add(c.getName()); + if (c.getChildrens().size() > 0) { + getTabComplete(c, i, map); + } else if (c.getArguments().size() > 0) { + List arguments = c.getArguments(); + for (int l = 0; l < arguments.size(); l++) { + Argument argument = arguments.get(l); + if (argument.getSuggest() != null) { + map.put(i+l+1, argument.getSuggest().getSuggest()); + continue; + } + map.put(i+l+1, Arrays.asList("<"+argument.getName()+">")); + } + } + } + map.put(i, list); + } + +} \ No newline at end of file diff --git a/src/main/java/com/io/yutian/elementoriginlib/command/SimpleCommandManager.java b/src/main/java/com/io/yutian/elementoriginlib/command/SimpleCommandManager.java new file mode 100644 index 0000000..6113962 --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/command/SimpleCommandManager.java @@ -0,0 +1,116 @@ +package com.io.yutian.elementoriginlib.command; + +import com.io.yutian.elementoriginlib.command.handler.CommandHandler; +import com.io.yutian.elementoriginlib.command.list.CommandHelp; +import org.bukkit.Bukkit; +import org.bukkit.command.Command; +import org.bukkit.command.CommandMap; +import org.bukkit.plugin.Plugin; +import org.jetbrains.annotations.NotNull; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class SimpleCommandManager implements ICommandManager { + + protected static Map bukkitCommandMap = new HashMap<>(); + protected static CommandMap commandMap; + + private final Plugin plugin; + private final String name; + + @NotNull + private List commands; + + + public SimpleCommandManager(Plugin plugin, String name) { + this(plugin, name, new ArrayList<>()); + } + + public SimpleCommandManager(Plugin plugin, String name, @NotNull List commands) { + this.plugin = plugin; + this.name = name; + this.commands = commands; + register(new CommandHelp(this)); + } + + public void register(@NotNull ICommand command) { + if (command == null) { + return; + } + commands.add(command); + if (command instanceof IAlias alias) { + String[] array = alias.getAlias(); + for (String s : array) { + registerPluginBukkitCommand(plugin, s, command); + } + } + } + + public void unregisterAll() { + for (ICommand command : commands) { + if (command instanceof IAlias) { + unregister(command); + } + } + } + + public static void unregister(ICommand command) { + if (!(command instanceof IAlias)) { + return; + } + try { + Map map = (Map) commandMap.getClass().getMethod("getKnownCommands").invoke(commandMap); + for (String name : ((IAlias) command).getAlias()) { + map.remove(name); + Command bukkitCommand = bukkitCommandMap.get(name); + bukkitCommand.unregister(commandMap); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + public void registerPluginCommand(@NotNull Plugin plugin, String commandName) { + Bukkit.getPluginCommand(commandName).setExecutor(new CommandHandler(plugin, this)); + } + + protected static void registerPluginBukkitCommand(Plugin plugin, String name, ICommand command) { + SimpleCommandHandler simpleCommandHandler = new SimpleCommandHandler(name, command); + bukkitCommandMap.put(name, simpleCommandHandler); + commandMap.register(plugin.getName(), simpleCommandHandler); + } + + public Plugin getPlugin() { + return plugin; + } + + @NotNull + @Override + public String getName() { + return name; + } + + @NotNull + @Override + public List getCommands() { + return commands; + } + + static { + try { + Class c = Bukkit.getServer().getClass(); + for (Method method : c.getDeclaredMethods()) { + if (method.getName().equals("getCommandMap")) { + commandMap = (CommandMap) method.invoke(Bukkit.getServer(), new Object[0]); + } + } + } catch (Exception e) { + e.printStackTrace(); + } + } + +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/command/Suggest.java b/src/main/java/com/io/yutian/elementoriginlib/command/Suggest.java new file mode 100644 index 0000000..d4639b1 --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/command/Suggest.java @@ -0,0 +1,10 @@ +package com.io.yutian.elementoriginlib.command; + +import java.util.List; + +@FunctionalInterface +public interface Suggest { + + List getSuggest(); + +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/command/Suggests.java b/src/main/java/com/io/yutian/elementoriginlib/command/Suggests.java new file mode 100644 index 0000000..eb23c09 --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/command/Suggests.java @@ -0,0 +1,28 @@ +package com.io.yutian.elementoriginlib.command; + +import org.bukkit.Bukkit; +import org.bukkit.World; +import org.bukkit.entity.Player; + +import java.util.LinkedList; +import java.util.List; + +public class Suggests { + + public static final Suggest WORLD_LIST = ()->{ + List list = new LinkedList<>(); + for (World world : Bukkit.getWorlds()) { + list.add(world.getName()); + } + return list; + }; + + public static final Suggest PLAYER_LIST = ()->{ + List list = new LinkedList<>(); + for (Player player : Bukkit.getOnlinePlayers()) { + list.add(player.getName()); + } + return list; + }; + +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/command/argument/Argument.java b/src/main/java/com/io/yutian/elementoriginlib/command/argument/Argument.java new file mode 100644 index 0000000..94f0f5f --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/command/argument/Argument.java @@ -0,0 +1,56 @@ +package com.io.yutian.elementoriginlib.command.argument; + +import com.io.yutian.elementoriginlib.command.Suggest; + +public class Argument { + + private String name; + + private ArgumentType argumentsType; + + private Suggest suggest; + + private boolean optional = false; + private Object defaultValue = null; + + public Argument(String name, ArgumentType argumentsType) { + this.name = name; + this.argumentsType = argumentsType; + } + + public Argument optional(Object defaultValue) { + optional = true; + this.defaultValue = defaultValue; + return this; + } + + public String getName() { + return name; + } + + public ArgumentType getArgumentsType() { + return argumentsType; + } + + public boolean isOptional() { + return optional; + } + + public Object getDefaultValue() { + return defaultValue; + } + + public Argument suggest(Suggest suggest) { + this.suggest = suggest; + return this; + } + + public Suggest getSuggest() { + return suggest; + } + + public static Argument argument(String name, ArgumentType type) { + return new Argument(name, type); + } + +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/command/argument/ArgumentType.java b/src/main/java/com/io/yutian/elementoriginlib/command/argument/ArgumentType.java new file mode 100644 index 0000000..122fa5d --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/command/argument/ArgumentType.java @@ -0,0 +1,31 @@ +package com.io.yutian.elementoriginlib.command.argument; + +import java.util.function.Function; +import java.util.function.Predicate; + +public class ArgumentType { + + private final String name; + + private Predicate predicate; + private Function function; + + public ArgumentType(String name, Predicate predicate, Function function) { + this.name = name; + this.predicate = predicate; + this.function = function; + } + + public String getName() { + return name; + } + + public boolean test(String string) { + return predicate.test(string); + } + + public T get(String t) { + return function.apply(t); + } + +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/command/argument/ArgumentTypes.java b/src/main/java/com/io/yutian/elementoriginlib/command/argument/ArgumentTypes.java new file mode 100644 index 0000000..f4fcf3d --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/command/argument/ArgumentTypes.java @@ -0,0 +1,19 @@ +package com.io.yutian.elementoriginlib.command.argument; + +import com.io.yutian.elementoriginlib.util.StringUtil; + +import java.util.UUID; + +public class ArgumentTypes { + + public static final ArgumentType STRING = new ArgumentType<>("string", (s) -> true, (s) -> s); + + public static final ArgumentType INTEGER = new ArgumentType<>("integer", StringUtil::isInt, Integer::parseInt); + + public static final ArgumentType DOUBLE = new ArgumentType<>("double", StringUtil::isDouble, Double::parseDouble); + + public static final ArgumentType UUID = new ArgumentType<>("uuid", StringUtil::isUUID, java.util.UUID::fromString); + + public static final ArgumentType BOOLEAN = new ArgumentType<>("boolean", StringUtil::isBoolean, Boolean::parseBoolean); + +} \ No newline at end of file diff --git a/src/main/java/com/io/yutian/elementoriginlib/command/argument/ArgumentValue.java b/src/main/java/com/io/yutian/elementoriginlib/command/argument/ArgumentValue.java new file mode 100644 index 0000000..74ac912 --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/command/argument/ArgumentValue.java @@ -0,0 +1,65 @@ +package com.io.yutian.elementoriginlib.command.argument; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +public class ArgumentValue { + + private static final Map, Class> PRIMITIVE_TO_WRAPPER = new HashMap<>(); + + static { + PRIMITIVE_TO_WRAPPER.put(boolean.class, Boolean.class); + PRIMITIVE_TO_WRAPPER.put(byte.class, Byte.class); + PRIMITIVE_TO_WRAPPER.put(short.class, Short.class); + PRIMITIVE_TO_WRAPPER.put(char.class, Character.class); + PRIMITIVE_TO_WRAPPER.put(int.class, Integer.class); + PRIMITIVE_TO_WRAPPER.put(long.class, Long.class); + PRIMITIVE_TO_WRAPPER.put(float.class, Float.class); + PRIMITIVE_TO_WRAPPER.put(double.class, Double.class); + } + + private Object value; + + public ArgumentValue(Object value) { + this.value = value; + } + + public Object getValue() { + return value; + } + + public V get(Class clazz) { + if (PRIMITIVE_TO_WRAPPER.getOrDefault(clazz, clazz).isAssignableFrom(value.getClass())) { + return (V) value; + } + return null; + } + + public String getString() { + return (String) value; + } + + public int getInt() { + return (Integer) value; + } + + public double getDouble() { + return (Double) value; + } + + public boolean getBoolean() { + return (Boolean) value; + } + + public UUID getUUID() { + return (UUID) value; + } + + @Override + public String toString() { + return "ArgumentValue{" + + "value=" + value + + '}'; + } +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/command/handler/CommandHandler.java b/src/main/java/com/io/yutian/elementoriginlib/command/handler/CommandHandler.java new file mode 100644 index 0000000..fc35a19 --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/command/handler/CommandHandler.java @@ -0,0 +1,341 @@ +package com.io.yutian.elementoriginlib.command.handler; + +import com.io.yutian.elementoriginlib.command.*; +import com.io.yutian.elementoriginlib.command.argument.Argument; +import com.io.yutian.elementoriginlib.command.argument.ArgumentValue; +import com.io.yutian.elementoriginlib.lang.Lang; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.command.TabCompleter; +import org.bukkit.plugin.Plugin; + +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class CommandHandler implements CommandExecutor, TabCompleter { + + private ICommandManager commandManager; + + public CommandHandler(Plugin plugin, ICommandManager commandManager) { + this.commandManager = commandManager; + } + + public void execute(CommandSender sender, String label, String[] args) { + if (args.length == 0) { + execute(sender, label, new String[]{"help", "1"}); + return; + } + List commands = commandManager.getCommands(); + String command = args[0]; + Stream stream = commands.stream().filter((c) -> c.getName().equalsIgnoreCase(command)); + Optional optional = stream.findFirst(); + if (!optional.isPresent()) { + sender.sendMessage(Lang.get("command-unknown", command)); + return; + } + ICommand iCommand = optional.get(); + if (!iCommand.hasPermission(sender)) { + sender.sendMessage(Lang.get("command-no-permission")); + return; + } + List commandNodes = iCommand.getCommandNodes(); + StringBuilder stringBuilder = new StringBuilder(); + for (int i = 0; i < args.length; i++) { + stringBuilder.append(args[i]); + if (i < args.length - 1) { + stringBuilder.append(" "); + } + } + String commandString = stringBuilder.toString(); + if (commandNodes.size() == 0) { + Map map = new HashMap<>(); + if (iCommand.getArguments().size() > 0) { + int argSize = args.length - 1; + List arguments = iCommand.getArguments(); + int k = 0; + if (arguments.get(arguments.size()-1).isOptional()) { + k++; + } + if (argSize < arguments.size()-k) { + sender.sendMessage(Lang.get("command-short-arg")); + return; + } + for (int l = 0; l < arguments.size(); l++) { + Argument a = arguments.get(l); + int index = l + 1; + if (index >= args.length) { + break; + } + String arg = args[index]; + if (!a.getArgumentsType().test(arg)) { + sender.sendMessage(Lang.get("command-unknown-arg", index+1, arg)); + return; + } + } + map = parseArgumentValue(sender, arguments, args, 0); + } + iCommand.executes(new CommandContext(commandString, label, sender, map)); + return; + } + + int nodeSize = args.length - 1; + + if (commandNodes.size() > 0 && nodeSize == 0) { + if (!iCommand.emptyExecutes(sender)) { + sender.sendMessage(Lang.get("command-short-arg")); + } + return; + } + + String mainNode = args[1]; + Stream nodeStream = commandNodes.stream().filter((n) -> n.getName().equalsIgnoreCase(mainNode) || n.getAlias().contains(mainNode)); + Optional nodeOptional = nodeStream.findFirst(); + if (!nodeOptional.isPresent()) { + sender.sendMessage(Lang.get("command-unknown-arg", 2, mainNode)); + return; + } + CommandNode node = nodeOptional.get(); + + if (node.getChildrens().size() > 0) { + checkClidren(commandString, label, sender, 1, args, node); + } else { + if (node.getCommand() != null) { + Map map = new HashMap<>(); + if (node.getArguments().size() > 0) { + int argSize = args.length - 2; + List arguments = node.getArguments(); + int k = 0; + if (arguments.get(arguments.size()-1).isOptional()) { + k++; + } + if (argSize < arguments.size()-k) { + sender.sendMessage(Lang.get("command-short-arg")); + return; + } + for (int l = 0; l < arguments.size(); l++) { + Argument a = arguments.get(l); + int index = l + 1; + if (index >= args.length) { + break; + } + if (index+1 >= args.length) { + break; + } + String arg = args[index+1]; + if (!a.getArgumentsType().test(arg)) { + sender.sendMessage(Lang.get("command-error-arg", index+1, arg)); + return; + } + } + map = parseArgumentValue(sender, node.getArguments(), args, 1); + } + node.getCommand().run(new CommandContext(commandString, label, sender, map)); + } else { + sender.sendMessage(Lang.get("command-unknown-arg", 3, mainNode)); + } + } + + } + + private Map parseArgumentValue(CommandSender commandSender, List argumentList, String[] args, int i) { + Map map = new HashMap<>(); + List arguments = argumentList; + for (int l = 0; l < arguments.size(); l++) { + Argument a = arguments.get(l); + if (i+1+l >= args.length) { + if (a.isOptional()) { + map.put(a.getName(), new ArgumentValue(a.getDefaultValue())); + } + return map; + } + String arg = args[i+1+l]; + if (!a.getArgumentsType().test(arg)) { + continue; + } + ArgumentValue argumentValue = new ArgumentValue(a.getArgumentsType().get(arg)); + map.put(a.getName(), argumentValue); + } + return map; + } + + private void checkClidren(String commandString, String label, CommandSender sender, int i, String[] args, CommandNode node) { + i++; + if (i >= args.length) { + if (node.getCommand() == null) { + sender.sendMessage(Lang.get("command-short-arg")); + } else { + node.getCommand().run(new CommandContext(commandString, label, sender, new HashMap<>())); + } + return; + } + String s = args[i]; + Stream nodeStream = node.getChildrens().stream().filter((n) -> n.getName().equalsIgnoreCase(s) || n.getAlias().contains(s)); + Optional nodeOptional = nodeStream.findFirst(); + if (!nodeOptional.isPresent()) { + sender.sendMessage(Lang.get("command-unknown-arg", i+1, s)); + return; + } + CommandNode node1 = nodeOptional.get(); + + if (node1.getChildrens().size() > 0) { + checkClidren(commandString, label, sender, i, args, node1); + } else { + if (node1.getCommand() != null) { + Map map = new HashMap<>(); + if (node1.getArguments().size() > 0) { + int argSize = args.length - i - 1; + List arguments = node1.getArguments(); + int k = 0; + if (arguments.get(arguments.size()-1).isOptional()) { + k++; + } + if (argSize < arguments.size()-k) { + sender.sendMessage(Lang.get("command-short-arg")); + return; + } + for (int l = 0; l < arguments.size(); l++) { + Argument a = arguments.get(l); + int index = i + l + 1; + if (index >= args.length) { + break; + } + String arg = args[index]; + if (!a.getArgumentsType().test(arg)) { + sender.sendMessage(Lang.get("command-unknown-arg", index+1, arg)); + return; + } + } + map = parseArgumentValue(sender, node1.getArguments(), args, i); + } + node1.getCommand().run(new CommandContext(commandString, label, sender, map)); + } else { + sender.sendMessage(Lang.get("command-unknown-arg", i+1, s)); + } + } + + } + + @Override + public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { + execute(sender, label, args); + return true; + } + + @Override + public List onTabComplete(CommandSender sender, Command command, String alias, String[] args) { + List list = new ArrayList<>(); + int index = args.length; + String arg = args[index-1]; + List commands = commandManager.getCommands(); + if (index == 1) { + List commandList = commands.stream().filter((c)->c.getName().startsWith(arg)).collect(Collectors.toList()); + if (commandList.size() > 0) { + commandList.forEach(c-> { + if (c.hasPermission(sender)) { + list.add(c.getName()); + } + }); + return list; + } + commandList = commands.stream().filter((c)->c.getName().contains(arg)).collect(Collectors.toList()); + if (commandList.size() > 0) { + commandList.forEach(c-> { + if (c.hasPermission(sender)) { + list.add(c.getName()); + } + }); + return list; + } + } else { + Optional iCommandOptional = commands.stream().filter((c)->c.getName().equalsIgnoreCase(args[0])).findFirst(); + if (!iCommandOptional.isPresent()) { + return list; + } + ICommand iCommand = iCommandOptional.get(); + if (!iCommand.hasPermission(sender)) { + return list; + } + if (iCommand instanceof ITabCompleter) { + ITabCompleter tabCompleter = (ITabCompleter) iCommand; + return tabCompleter.onTabComplete(sender, args, index-2, arg); + } else { + Map> map = new HashMap<>(); + if (iCommand.getCommandNodes().size() > 0) { + List list1 = new ArrayList<>(); + for (CommandNode node : iCommand.getCommandNodes()) { + list1.add(node.getName()); + list1.addAll(node.getAlias()); + if (index >= 2) { + if (!node.getName().equalsIgnoreCase(args[1])) { + continue; + } + } + if (node.getChildrens().size() > 0) { + getTabComplete(node, 2, map); + } else if (node.getArguments().size() > 0) { + List arguments = node.getArguments(); + for (int l = 0; l < arguments.size(); l++) { + Argument argument = arguments.get(l); + if (argument.getSuggest() != null) { + map.put(2+l+1, argument.getSuggest().getSuggest()); + continue; + } + map.put(2+l+1, Arrays.asList("<"+argument.getName()+">")); + } + } + } + map.put(2, list1); + return preseSuggest(map.getOrDefault(index, list), arg); + } else if (iCommand.getArguments().size() > 0) { + List arguments = iCommand.getArguments(); + for (int l = 0; l < arguments.size(); l++) { + Argument argument = arguments.get(l); + if (argument.getSuggest() != null) { + map.put(1+l+1, argument.getSuggest().getSuggest()); + continue; + } + map.put(1+l+1, Arrays.asList("<"+argument.getName()+">")); + } + return preseSuggest(map.getOrDefault(index, list), arg); + } + } + } + return preseSuggest(list, arg); + } + + public static List preseSuggest(List list, String arg) { + List newList = new ArrayList<>(); + List list1 = list.stream().filter((c)->c.startsWith(arg)||c.toLowerCase().startsWith(arg.toLowerCase())).collect(Collectors.toList()); + List list2 = list.stream().filter((c)->c.contains(arg)||c.toLowerCase().contains(arg.toLowerCase())).collect(Collectors.toList()); + List list3 = list.stream().filter((c)->c.equalsIgnoreCase(arg)|| c.equalsIgnoreCase(arg)).collect(Collectors.toList()); + newList.addAll(list1); + newList.addAll(list2); + newList.addAll(list3); + return newList; + } + + private void getTabComplete(CommandNode node, int i, Map> map) { + i++; + List list = map.getOrDefault(i, new ArrayList<>()); + for (CommandNode c : node.getChildrens()) { + list.add(c.getName()); + if (c.getChildrens().size() > 0) { + getTabComplete(c, i, map); + } else if (c.getArguments().size() > 0) { + List arguments = c.getArguments(); + for (int l = 0; l < arguments.size(); l++) { + Argument argument = arguments.get(l); + if (argument.getSuggest() != null) { + map.put(i+l+1, argument.getSuggest().getSuggest()); + continue; + } + map.put(i+l+1, Arrays.asList("<"+argument.getName()+">")); + } + } + } + map.put(i, list); + } + +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/command/list/CommandHelp.java b/src/main/java/com/io/yutian/elementoriginlib/command/list/CommandHelp.java new file mode 100644 index 0000000..167897d --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/command/list/CommandHelp.java @@ -0,0 +1,117 @@ +package com.io.yutian.elementoriginlib.command.list; + +import com.io.yutian.elementoriginlib.command.CommandContext; +import com.io.yutian.elementoriginlib.command.CommandNode; +import com.io.yutian.elementoriginlib.command.ICommand; +import com.io.yutian.elementoriginlib.command.ICommandManager; +import com.io.yutian.elementoriginlib.command.argument.Argument; +import com.io.yutian.elementoriginlib.command.argument.ArgumentTypes; +import com.io.yutian.elementoriginlib.lang.Lang; +import com.io.yutian.elementoriginlib.util.ComponentBuilder; +import com.io.yutian.elementoriginlib.list.PageList; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.event.ClickEvent; +import net.kyori.adventure.text.event.HoverEvent; +import org.bukkit.command.CommandSender; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class CommandHelp extends ICommand { + + private ICommandManager commandManager; + @Nullable + private String alias; + + public CommandHelp(ICommandManager commandManager) { + this(commandManager, null); + } + + public CommandHelp(ICommandManager commandManager, String alias) { + super("help"); + this.commandManager = commandManager; + this.alias = alias; + addArgument(Argument.argument("page", ArgumentTypes.INTEGER).optional(1)); + } + + @Override + public boolean hasPermission(CommandSender sender) { + return true; + } + + @Override + public void executes(CommandContext commandContext) { + String commandAlias = alias != null ? alias : commandContext.getLabel(); + CommandSender sender = commandContext.getSender(); + int page = commandContext.getArgumentsValue("page").getInt(); + if (page <= 0) { + sender.sendMessage(Lang.get("command.help.page-error")); + return; + } + List commands = commandManager.getCommands(); + Stream stream = commands.stream().filter((c) -> c.hasPermission(sender)); + List list = stream.collect(Collectors.toList()); + PageList pageList = new PageList<>(list, 8); + if (page > pageList.size()) { + sender.sendMessage(Lang.get("command.help.page-error")); + return; + } + sender.sendMessage(" "); + List commandList = pageList.getList(page); + sender.sendMessage("§7======[ §e§l"+commandManager.getName()+" §7]======"); + for (ICommand command : commandList) { + StringBuilder stringBuilder = new StringBuilder("§6/"+commandAlias+" "+command.getName()); + stringBuilder.append("§f"); + if (command.getCommandNodes().size() > 0) { + StringBuilder sb = new StringBuilder(); + sb.append(" ["); + int i = 0; + for (CommandNode node : command.getCommandNodes()) { + sb.append(node.getName()); + if (i + 1 < command.getCommandNodes().size()) { + sb.append("/"); + } + i++; + } + sb.append("]"); + stringBuilder.append(sb); + } else { + for (Argument argument : command.getArguments()) { + stringBuilder.append(" "); + stringBuilder.append("<"+argument.getName()+">"); + } + } + if (command.getDescription() != null) { + stringBuilder.append(" "); + stringBuilder.append("§7- §f"+command.getDescription()); + } + sender.sendMessage(stringBuilder.toString()); + } + ComponentBuilder componentBuilder = new ComponentBuilder(); + boolean hasUpPage = page > 1; + boolean hasNextPage = page < pageList.size(); + componentBuilder.add("§7====="); + if (hasUpPage) { + componentBuilder.add(" §7["+getColor(true)+"◀§7] ", ClickEvent.clickEvent(ClickEvent.Action.RUN_COMMAND, "/"+commandAlias+" help "+(page-1)), HoverEvent.hoverEvent(HoverEvent.Action.SHOW_TEXT, Component.text("§f上一页"))); + } else { + componentBuilder.add(" §7["+getColor(false)+"◀§7] "); + } + componentBuilder.add("§7===="); + componentBuilder.add("(§a"+page+"§f/§e"+pageList.size()+"§7)"); + componentBuilder.add("§7===="); + if (hasNextPage) { + componentBuilder.add(" §7["+getColor(true)+"▶§7] ", ClickEvent.clickEvent(ClickEvent.Action.RUN_COMMAND, "/"+commandAlias+" help "+(page+1)), HoverEvent.hoverEvent(HoverEvent.Action.SHOW_TEXT, Component.text("§f下一页"))); + } else { + componentBuilder.add(" §7["+getColor(false)+"▶§7] "); + } + componentBuilder.add("§7====="); + sender.sendMessage(componentBuilder.build()); + } + + private String getColor(boolean hasPage) { + return hasPage ? "§a" : "§c"; + } + +} \ No newline at end of file diff --git a/src/main/java/com/io/yutian/elementoriginlib/exception/SerializeException.java b/src/main/java/com/io/yutian/elementoriginlib/exception/SerializeException.java new file mode 100644 index 0000000..b09572b --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/exception/SerializeException.java @@ -0,0 +1,9 @@ +package com.io.yutian.elementoriginlib.exception; + +public class SerializeException extends RuntimeException { + + public SerializeException(Class clazz, Throwable cause) { + super(clazz.toString()); + } + +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/expiringmap/EntryLoader.java b/src/main/java/com/io/yutian/elementoriginlib/expiringmap/EntryLoader.java new file mode 100644 index 0000000..6903cdd --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/expiringmap/EntryLoader.java @@ -0,0 +1,17 @@ +package com.io.yutian.elementoriginlib.expiringmap; + +/** + * Loads entries on demand. + * + * @param Key type + * @param Value type + */ +public interface EntryLoader { + /** + * Called to load a new value for the {@code key} into an expiring map. + * + * @param key to load a value for + * @return new value to load + */ + V load(K key); +} \ No newline at end of file diff --git a/src/main/java/com/io/yutian/elementoriginlib/expiringmap/ExpirationListener.java b/src/main/java/com/io/yutian/elementoriginlib/expiringmap/ExpirationListener.java new file mode 100644 index 0000000..ac1275e --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/expiringmap/ExpirationListener.java @@ -0,0 +1,17 @@ +package com.io.yutian.elementoriginlib.expiringmap; + +/** + * A listener for expired object events. + * + * @param Key type + * @param Value type + */ +public interface ExpirationListener { + /** + * Called when a map entry expires. + * + * @param key Expired key + * @param value Expired value + */ + void expired(K key, V value); +} \ No newline at end of file diff --git a/src/main/java/com/io/yutian/elementoriginlib/expiringmap/ExpirationPolicy.java b/src/main/java/com/io/yutian/elementoriginlib/expiringmap/ExpirationPolicy.java new file mode 100644 index 0000000..24736c9 --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/expiringmap/ExpirationPolicy.java @@ -0,0 +1,11 @@ +package com.io.yutian.elementoriginlib.expiringmap; + +/** + * Determines how ExpiringMap entries should be expired. + */ +public enum ExpirationPolicy { + /** Expires entries based on when they were last accessed */ + ACCESSED, + /** Expires entries based on when they were created */ + CREATED; +} \ No newline at end of file diff --git a/src/main/java/com/io/yutian/elementoriginlib/expiringmap/ExpiringEntryLoader.java b/src/main/java/com/io/yutian/elementoriginlib/expiringmap/ExpiringEntryLoader.java new file mode 100644 index 0000000..aef842f --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/expiringmap/ExpiringEntryLoader.java @@ -0,0 +1,17 @@ +package com.io.yutian.elementoriginlib.expiringmap; + +/** + * Loads entries on demand, with control over each value's expiry duration (i.e. variable expiration). + * + * @param Key type + * @param Value type + */ +public interface ExpiringEntryLoader { + /** + * Called to load a new value for the {@code key} into an expiring map. + * + * @param key to load a value for + * @return contains new value to load along with its expiry duration + */ + ExpiringValue load(K key); +} \ No newline at end of file diff --git a/src/main/java/com/io/yutian/elementoriginlib/expiringmap/ExpiringMap.java b/src/main/java/com/io/yutian/elementoriginlib/expiringmap/ExpiringMap.java new file mode 100644 index 0000000..475bfa2 --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/expiringmap/ExpiringMap.java @@ -0,0 +1,1422 @@ +package com.io.yutian.elementoriginlib.expiringmap; + +import java.lang.ref.WeakReference; +import java.util.AbstractCollection; +import java.util.AbstractSet; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Set; +import java.util.SortedSet; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.ConcurrentSkipListSet; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +import com.io.yutian.elementoriginlib.expiringmap.internal.Assert; +import com.io.yutian.elementoriginlib.expiringmap.internal.NamedThreadFactory; + +/** + * A thread-safe map that expires entries. Optional features include expiration policies, variable entry expiration, + * lazy entry loading, and expiration listeners. + * + *

+ * Entries are tracked by expiration time and expired by a single thread. + * + *

+ * Expiration listeners are called synchronously as entries are expired and block write operations to the map until they + * completed. Asynchronous expiration listeners are called on a separate thread pool and do not block map operations. + * + *

+ * When variable expiration is disabled (default), put/remove operations have a time complexity O(1). When + * variable expiration is enabled, put/remove operations have time complexity of O(log n). + * + *

+ * Example usages: + * + *

+ * {@code
+ * Map map = ExpiringMap.create();
+ * Map map = ExpiringMap.builder().expiration(30, TimeUnit.SECONDS).build();
+ * Map map = ExpiringMap.builder()
+ *   .expiration(10, TimeUnit.MINUTES)
+ *   .entryLoader(new EntryLoader() {
+ *     public Connection load(String address) {
+ *       return new Connection(address);
+ *     }
+ *   })
+ *   .expirationListener(new ExpirationListener() {
+ *     public void expired(String key, Connection connection) {
+ *       connection.close();
+ *     }
+ *   })
+ *   .build();
+ * }
+ * 
+ * + * @author Jonathan Halterman + * @param Key type + * @param Value type + */ +public class ExpiringMap implements ConcurrentMap { + static volatile ScheduledExecutorService EXPIRER; + static volatile ThreadPoolExecutor LISTENER_SERVICE; + static ThreadFactory THREAD_FACTORY; + + List> expirationListeners; + List> asyncExpirationListeners; + private AtomicLong expirationNanos; + private int maxSize; + private final AtomicReference expirationPolicy; + private final EntryLoader entryLoader; + private final ExpiringEntryLoader expiringEntryLoader; + private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock(); + private final Lock readLock = readWriteLock.readLock(); + private final Lock writeLock = readWriteLock.writeLock(); + /** Guarded by "readWriteLock" */ + private final EntryMap entries; + private final boolean variableExpiration; + + /** + * Sets the {@link ThreadFactory} that is used to create expiration and listener callback threads for all ExpiringMap + * instances. + * + * @param threadFactory + * @throws NullPointerException if {@code threadFactory} is null + */ + public static void setThreadFactory(ThreadFactory threadFactory) { + THREAD_FACTORY = Assert.notNull(threadFactory, "threadFactory"); + } + + /** + * Creates a new instance of ExpiringMap. + * + * @param builder The map builder + */ + private ExpiringMap(final Builder builder) { + if (EXPIRER == null) { + synchronized (ExpiringMap.class) { + if (EXPIRER == null) { + EXPIRER = Executors.newSingleThreadScheduledExecutor( + THREAD_FACTORY == null ? new NamedThreadFactory("ExpiringMap-Expirer") : THREAD_FACTORY); + } + } + } + + if (LISTENER_SERVICE == null && builder.asyncExpirationListeners != null) + initListenerService(); + + variableExpiration = builder.variableExpiration; + entries = variableExpiration ? new EntryTreeHashMap() : new EntryLinkedHashMap(); + if (builder.expirationListeners != null) + expirationListeners = new CopyOnWriteArrayList>(builder.expirationListeners); + if (builder.asyncExpirationListeners != null) + asyncExpirationListeners = new CopyOnWriteArrayList>(builder.asyncExpirationListeners); + expirationPolicy = new AtomicReference(builder.expirationPolicy); + expirationNanos = new AtomicLong(TimeUnit.NANOSECONDS.convert(builder.duration, builder.timeUnit)); + maxSize = builder.maxSize; + entryLoader = builder.entryLoader; + expiringEntryLoader = builder.expiringEntryLoader; + } + + /** + * Builds ExpiringMap instances. Defaults to ExpirationPolicy.CREATED, expiration of 60 TimeUnit.SECONDS and + * a maxSize of Integer.MAX_VALUE. + */ + public static final class Builder { + private ExpirationPolicy expirationPolicy = ExpirationPolicy.CREATED; + private List> expirationListeners; + private List> asyncExpirationListeners; + private TimeUnit timeUnit = TimeUnit.SECONDS; + private boolean variableExpiration; + private long duration = 60; + private int maxSize = Integer.MAX_VALUE; + private EntryLoader entryLoader; + private ExpiringEntryLoader expiringEntryLoader; + + /** + * Creates a new Builder object. + */ + private Builder() { + } + + /** + * Builds and returns an expiring map. + * + * @param Key type + * @param Value type + */ + @SuppressWarnings("unchecked") + public ExpiringMap build() { + return new ExpiringMap((Builder) this); + } + + /** + * Sets the default map entry expiration. + * + * @param duration the length of time after an entry is created that it should be removed + * @param timeUnit the unit that {@code duration} is expressed in + * @throws NullPointerException if {@code timeUnit} is null + */ + public Builder expiration(long duration, TimeUnit timeUnit) { + this.duration = duration; + this.timeUnit = Assert.notNull(timeUnit, "timeUnit"); + return this; + } + + /** + * Sets the maximum size of the map. Once this size has been reached, adding an additional entry will expire the + * first entry in line for expiration based on the expiration policy. + * + * @param maxSize The maximum size of the map. + */ + public Builder maxSize(int maxSize) { + Assert.operation(maxSize > 0, "maxSize"); + this.maxSize = maxSize; + return this; + } + + /** + * Sets the EntryLoader to use when loading entries. Either an EntryLoader or ExpiringEntryLoader may be set, not + * both. + * + * @param loader to set + * @throws NullPointerException if {@code loader} is null + * @throws IllegalStateException if an {@link #expiringEntryLoader(ExpiringEntryLoader) ExpiringEntryLoader} is set + */ + @SuppressWarnings("unchecked") + public Builder entryLoader(EntryLoader loader) { + assertNoLoaderSet(); + entryLoader = (EntryLoader) Assert.notNull(loader, "loader"); + return (Builder) this; + } + + /** + * Sets the ExpiringEntryLoader to use when loading entries and configures {@link #variableExpiration() variable + * expiration}. Either an EntryLoader or ExpiringEntryLoader may be set, not both. + * + * @param loader to set + * @throws NullPointerException if {@code loader} is null + * @throws IllegalStateException if an {@link #entryLoader(EntryLoader) EntryLoader} is set + */ + @SuppressWarnings("unchecked") + public Builder expiringEntryLoader( + ExpiringEntryLoader loader) { + assertNoLoaderSet(); + expiringEntryLoader = (ExpiringEntryLoader) Assert.notNull(loader, "loader"); + variableExpiration(); + return (Builder) this; + } + + /** + * Configures the expiration listener that will receive notifications upon each map entry's expiration. + * Notifications are delivered synchronously and block map write operations. + * + * @param listener to set + * @throws NullPointerException if {@code listener} is null + */ + @SuppressWarnings("unchecked") + public Builder expirationListener( + ExpirationListener listener) { + Assert.notNull(listener, "listener"); + if (expirationListeners == null) + expirationListeners = new ArrayList>(); + expirationListeners.add((ExpirationListener) listener); + return (Builder) this; + } + + /** + * Configures the expiration listeners which will receive notifications upon each map entry's expiration. + * Notifications are delivered synchronously and block map write operations. + * + * @param listeners to set + * @throws NullPointerException if {@code listener} is null + */ + @SuppressWarnings("unchecked") + public Builder expirationListeners( + List> listeners) { + Assert.notNull(listeners, "listeners"); + if (expirationListeners == null) + expirationListeners = new ArrayList>(listeners.size()); + for (ExpirationListener listener : listeners) + expirationListeners.add((ExpirationListener) listener); + return (Builder) this; + } + + /** + * Configures the expiration listener which will receive asynchronous notifications upon each map entry's + * expiration. + * + * @param listener to set + * @throws NullPointerException if {@code listener} is null + */ + @SuppressWarnings("unchecked") + public Builder asyncExpirationListener( + ExpirationListener listener) { + Assert.notNull(listener, "listener"); + if (asyncExpirationListeners == null) + asyncExpirationListeners = new ArrayList>(); + asyncExpirationListeners.add((ExpirationListener) listener); + return (Builder) this; + } + + /** + * Configures the expiration listeners which will receive asynchronous notifications upon each map entry's + * expiration. + * + * @param listeners to set + * @throws NullPointerException if {@code listener} is null + */ + @SuppressWarnings("unchecked") + public Builder asyncExpirationListeners( + List> listeners) { + Assert.notNull(listeners, "listeners"); + if (asyncExpirationListeners == null) + asyncExpirationListeners = new ArrayList>(listeners.size()); + for (ExpirationListener listener : listeners) + asyncExpirationListeners.add((ExpirationListener) listener); + return (Builder) this; + } + + /** + * Configures the map entry expiration policy. + * + * @param expirationPolicy + * @throws NullPointerException if {@code expirationPolicy} is null + */ + public Builder expirationPolicy(ExpirationPolicy expirationPolicy) { + this.expirationPolicy = Assert.notNull(expirationPolicy, "expirationPolicy"); + return this; + } + + /** + * Allows for map entries to have individual expirations and for expirations to be changed. + */ + public Builder variableExpiration() { + variableExpiration = true; + return this; + } + + private void assertNoLoaderSet() { + Assert.state(entryLoader == null && expiringEntryLoader == null, + "Either entryLoader or expiringEntryLoader may be set, not both"); + } + } + + /** Entry map definition. */ + private interface EntryMap extends Map> { + /** Returns the first entry in the map or null if the map is empty. */ + ExpiringEntry first(); + + /** + * Reorders the given entry in the map. + * + * @param entry to reorder + */ + void reorder(ExpiringEntry entry); + + /** Returns a values iterator. */ + Iterator> valuesIterator(); + } + + /** Entry LinkedHashMap implementation. */ + private static class EntryLinkedHashMap extends LinkedHashMap> + implements EntryMap { + private static final long serialVersionUID = 1L; + + @Override + public boolean containsValue(Object value) { + for (ExpiringEntry entry : values()) { + V v = entry.value; + if (v == value || (value != null && value.equals(v))) + return true; + } + return false; + } + + @Override + public ExpiringEntry first() { + return isEmpty() ? null : values().iterator().next(); + } + + @Override + public void reorder(ExpiringEntry value) { + remove(value.key); + value.resetExpiration(); + put(value.key, value); + } + + @Override + public Iterator> valuesIterator() { + return values().iterator(); + } + + abstract class AbstractHashIterator { + private final Iterator>> iterator; + private ExpiringEntry next; + + @SuppressWarnings({"unchecked", "rawtypes"}) + AbstractHashIterator() { + iterator = (Iterator) Arrays.asList(entrySet().toArray(new Map.Entry[0])).iterator(); + } + + public boolean hasNext() { + return iterator.hasNext(); + } + + public ExpiringEntry getNext() { + next = iterator.next().getValue(); + return next; + } + + public void remove() { + iterator.remove(); + } + } + + final class KeyIterator extends AbstractHashIterator implements Iterator { + public K next() { + return getNext().key; + } + } + + final class ValueIterator extends AbstractHashIterator implements Iterator { + public V next() { + return getNext().value; + } + } + + public final class EntryIterator extends AbstractHashIterator implements Iterator> { + public Map.Entry next() { + return mapEntryFor(getNext()); + } + } + } + + /** Entry TreeHashMap implementation for variable expiration ExpiringMap entries. */ + private static class EntryTreeHashMap extends HashMap> implements EntryMap { + private static final long serialVersionUID = 1L; + SortedSet> sortedSet = new ConcurrentSkipListSet>(); + + @Override + public void clear() { + super.clear(); + sortedSet.clear(); + } + + @Override + public boolean containsValue(Object value) { + for (ExpiringEntry entry : values()) { + V v = entry.value; + if (v == value || (value != null && value.equals(v))) + return true; + } + return false; + } + + @Override + public ExpiringEntry first() { + return sortedSet.isEmpty() ? null : sortedSet.first(); + } + + @Override + public ExpiringEntry put(K key, ExpiringEntry value) { + sortedSet.add(value); + return super.put(key, value); + } + + @Override + public ExpiringEntry remove(Object key) { + ExpiringEntry entry = super.remove(key); + if (entry != null) + sortedSet.remove(entry); + return entry; + } + + @Override + public void reorder(ExpiringEntry value) { + sortedSet.remove(value); + value.resetExpiration(); + sortedSet.add(value); + } + + @Override + public Iterator> valuesIterator() { + return new ExpiringEntryIterator(); + } + + abstract class AbstractHashIterator { + private final Iterator> iterator = sortedSet.iterator(); + protected ExpiringEntry next; + + public boolean hasNext() { + return iterator.hasNext(); + } + + public ExpiringEntry getNext() { + next = iterator.next(); + return next; + } + + public void remove() { + EntryTreeHashMap.super.remove(next.key); + iterator.remove(); + } + } + + final class ExpiringEntryIterator extends AbstractHashIterator implements Iterator> { + public final ExpiringEntry next() { + return getNext(); + } + } + + final class KeyIterator extends AbstractHashIterator implements Iterator { + public final K next() { + return getNext().key; + } + } + + final class ValueIterator extends AbstractHashIterator implements Iterator { + public final V next() { + return getNext().value; + } + } + + final class EntryIterator extends AbstractHashIterator implements Iterator> { + public final Entry next() { + return mapEntryFor(getNext()); + } + } + } + + /** Expiring map entry implementation. */ + static class ExpiringEntry implements Comparable> { + final AtomicLong expirationNanos; + /** Epoch time at which the entry is expected to expire */ + final AtomicLong expectedExpiration; + final AtomicReference expirationPolicy; + final K key; + /** Guarded by "this" */ + volatile Future entryFuture; + /** Guarded by "this" */ + V value; + /** Guarded by "this" */ + volatile boolean scheduled; + + /** + * Creates a new ExpiringEntry object. + * + * @param key for the entry + * @param value for the entry + * @param expirationPolicy for the entry + * @param expirationNanos for the entry + */ + ExpiringEntry(K key, V value, AtomicReference expirationPolicy, AtomicLong expirationNanos) { + this.key = key; + this.value = value; + this.expirationPolicy = expirationPolicy; + this.expirationNanos = expirationNanos; + this.expectedExpiration = new AtomicLong(); + resetExpiration(); + } + + @Override + public int compareTo(ExpiringEntry other) { + if (key.equals(other.key)) + return 0; + return expectedExpiration.get() < other.expectedExpiration.get() ? -1 : 1; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((key == null) ? 0 : key.hashCode()); + result = prime * result + ((value == null) ? 0 : value.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + ExpiringEntry other = (ExpiringEntry) obj; + if (!key.equals(other.key)) + return false; + if (value == null) { + if (other.value != null) + return false; + } else if (!value.equals(other.value)) + return false; + return true; + } + + @Override + public String toString() { + return value.toString(); + } + + /** + * Marks the entry as canceled. + * + * @return true if the entry was scheduled + */ + synchronized boolean cancel() { + boolean result = scheduled; + if (entryFuture != null) + entryFuture.cancel(false); + + entryFuture = null; + scheduled = false; + return result; + } + + /** Gets the entry value. */ + synchronized V getValue() { + return value; + } + + /** Resets the entry's expected expiration. */ + void resetExpiration() { + expectedExpiration.set(expirationNanos.get() + System.nanoTime()); + } + + /** Marks the entry as scheduled. */ + synchronized void schedule(Future entryFuture) { + this.entryFuture = entryFuture; + scheduled = true; + } + + /** Sets the entry value. */ + synchronized void setValue(V value) { + this.value = value; + } + } + + /** + * Creates an ExpiringMap builder. + * + * @return New ExpiringMap builder + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Creates a new instance of ExpiringMap with ExpirationPolicy.CREATED and an expiration of 60 seconds. + */ + @SuppressWarnings("unchecked") + public static ExpiringMap create() { + return new ExpiringMap((Builder) ExpiringMap.builder()); + } + + /** + * Adds an expiration listener. + * + * @param listener to add + * @throws NullPointerException if {@code listener} is null + */ + public synchronized void addExpirationListener(ExpirationListener listener) { + Assert.notNull(listener, "listener"); + if (expirationListeners == null) + expirationListeners = new CopyOnWriteArrayList>(); + expirationListeners.add(listener); + } + + /** + * Adds an asynchronous expiration listener. + * + * @param listener to add + * @throws NullPointerException if {@code listener} is null + */ + public synchronized void addAsyncExpirationListener(ExpirationListener listener) { + Assert.notNull(listener, "listener"); + if (asyncExpirationListeners == null) + asyncExpirationListeners = new CopyOnWriteArrayList>(); + asyncExpirationListeners.add(listener); + // If asyncListener was not added on Builder, LISTENER_SERVICE was not initialized and remain null + if (LISTENER_SERVICE == null) + initListenerService(); + } + + @Override + public void clear() { + writeLock.lock(); + try { + for (ExpiringEntry entry : entries.values()) + entry.cancel(); + entries.clear(); + } finally { + writeLock.unlock(); + } + } + + @Override + public boolean containsKey(Object key) { + readLock.lock(); + try { + return entries.containsKey(key); + } finally { + readLock.unlock(); + } + } + + @Override + public boolean containsValue(Object value) { + readLock.lock(); + try { + return entries.containsValue(value); + } finally { + readLock.unlock(); + } + } + + /** + * Returns a copy of the map's entries, which can be iterated over safely by multiple threads. + * + * @return Copied set of map entries. + */ + @Override + public Set> entrySet() { + return new AbstractSet>() { + @Override + public void clear() { + ExpiringMap.this.clear(); + } + + @Override + public boolean contains(Object entry) { + if (!(entry instanceof Map.Entry)) + return false; + Entry e = (Entry) entry; + return containsKey(e.getKey()); + } + + @Override + public Iterator> iterator() { + return (entries instanceof EntryLinkedHashMap) ? ((EntryLinkedHashMap) entries).new EntryIterator() + : ((EntryTreeHashMap) entries).new EntryIterator(); + } + + @Override + public boolean remove(Object entry) { + if (entry instanceof Map.Entry) { + Entry e = (Entry) entry; + return ExpiringMap.this.remove(e.getKey()) != null; + } + return false; + } + + @Override + public int size() { + return ExpiringMap.this.size(); + } + }; + } + + @Override + public boolean equals(Object obj) { + readLock.lock(); + try { + return entries.equals(obj); + } finally { + readLock.unlock(); + } + } + + @Override + @SuppressWarnings("unchecked") + public V get(Object key) { + ExpiringEntry entry = getEntry(key); + + if (entry == null) { + return load((K) key); + } else if (ExpirationPolicy.ACCESSED.equals(entry.expirationPolicy.get())) + resetEntry(entry, false); + + return entry.getValue(); + } + + private V load(K key) { + if (entryLoader == null && expiringEntryLoader == null) + return null; + + writeLock.lock(); + try { + // Double check for entry + ExpiringEntry entry = getEntry(key); + if (entry != null) + return entry.getValue(); + + if (entryLoader != null) { + V value = entryLoader.load(key); + put(key, value); + return value; + } else { + ExpiringValue expiringValue = expiringEntryLoader.load(key); + if (expiringValue == null) { + put(key, null); + return null; + } else { + long duration = expiringValue.getTimeUnit() == null ? expirationNanos.get() : expiringValue.getDuration(); + TimeUnit timeUnit = expiringValue.getTimeUnit() == null ? TimeUnit.NANOSECONDS : expiringValue.getTimeUnit(); + put(key, expiringValue.getValue(), expiringValue.getExpirationPolicy() == null ? expirationPolicy.get() + : expiringValue.getExpirationPolicy(), duration, timeUnit); + return expiringValue.getValue(); + } + } + } finally { + writeLock.unlock(); + } + } + + /** + * Returns the map's default expiration duration in milliseconds. + * + * @return The expiration duration (milliseconds) + */ + public long getExpiration() { + return TimeUnit.NANOSECONDS.toMillis(expirationNanos.get()); + } + + /** + * Gets the expiration duration in milliseconds for the entry corresponding to the given key. + * + * @param key + * @return The expiration duration in milliseconds + * @throws NullPointerException if {@code key} is null + * @throws NoSuchElementException If no entry exists for the given key + */ + public long getExpiration(K key) { + Assert.notNull(key, "key"); + ExpiringEntry entry = getEntry(key); + Assert.element(entry, key); + return TimeUnit.NANOSECONDS.toMillis(entry.expirationNanos.get()); + } + + /** + * Gets the ExpirationPolicy for the entry corresponding to the given {@code key}. + * + * @param key + * @return The ExpirationPolicy for the {@code key} + * @throws NullPointerException if {@code key} is null + * @throws NoSuchElementException If no entry exists for the given key + */ + public ExpirationPolicy getExpirationPolicy(K key) { + Assert.notNull(key, "key"); + ExpiringEntry entry = getEntry(key); + Assert.element(entry, key); + return entry.expirationPolicy.get(); + } + + /** + * Gets the expected expiration, in milliseconds from the current time, for the entry corresponding to the given + * {@code key}. + * + * @param key + * @return The expiration duration in milliseconds + * @throws NullPointerException if {@code key} is null + * @throws NoSuchElementException If no entry exists for the given key + */ + public long getExpectedExpiration(K key) { + Assert.notNull(key, "key"); + ExpiringEntry entry = getEntry(key); + Assert.element(entry, key); + return TimeUnit.NANOSECONDS.toMillis(entry.expectedExpiration.get() - System.nanoTime()); + } + + /** + * Gets the maximum size of the map. Once this size has been reached, adding an additional entry will expire the + * first entry in line for expiration based on the expiration policy. + * + * @return The maximum size of the map. + */ + public int getMaxSize() { + return maxSize; + } + + @Override + public int hashCode() { + readLock.lock(); + try { + return entries.hashCode(); + } finally { + readLock.unlock(); + } + } + + @Override + public boolean isEmpty() { + readLock.lock(); + try { + return entries.isEmpty(); + } finally { + readLock.unlock(); + } + } + + /** + * Returns a copy of the map's keys, which can be iterated over safely by multiple threads. + * + * @return Copied set of map keys. + */ + @Override + public Set keySet() { + return new AbstractSet() { + @Override + public void clear() { + ExpiringMap.this.clear(); + } + + @Override + public boolean contains(Object key) { + return containsKey(key); + } + + @Override + public Iterator iterator() { + readLock.lock(); + try { + return (entries instanceof EntryLinkedHashMap) ? ((EntryLinkedHashMap) entries).new KeyIterator() + : ((EntryTreeHashMap) entries).new KeyIterator(); + } finally { + readLock.unlock(); + } + } + + @Override + public boolean remove(Object value) { + return ExpiringMap.this.remove(value) != null; + } + + @Override + public int size() { + return ExpiringMap.this.size(); + } + }; + } + + /** + * Puts {@code value} in the map for {@code key}. Resets the entry's expiration unless an entry already exists for the + * same {@code key} and {@code value}. + * + * @param key to put value for + * @param value to put for key + * @return the old value + * @throws NullPointerException if {@code key} is null + */ + @Override + public V put(K key, V value) { + Assert.notNull(key, "key"); + return putInternal(key, value, expirationPolicy.get(), expirationNanos.get()); + } + + /** + * @see #put(Object, Object, ExpirationPolicy, long, TimeUnit) + */ + public V put(K key, V value, ExpirationPolicy expirationPolicy) { + return put(key, value, expirationPolicy, expirationNanos.get(), TimeUnit.NANOSECONDS); + } + + /** + * @see #put(Object, Object, ExpirationPolicy, long, TimeUnit) + */ + public V put(K key, V value, long duration, TimeUnit timeUnit) { + return put(key, value, expirationPolicy.get(), duration, timeUnit); + } + + /** + * Puts {@code value} in the map for {@code key}. Resets the entry's expiration unless an entry already exists for the + * same {@code key} and {@code value}. Requires that variable expiration be enabled. + * + * @param key Key to put value for + * @param value Value to put for key + * @param duration the length of time after an entry is created that it should be removed + * @param timeUnit the unit that {@code duration} is expressed in + * @return the old value + * @throws UnsupportedOperationException If variable expiration is not enabled + * @throws NullPointerException if {@code key}, {@code expirationPolicy} or {@code timeUnit} are null + */ + public V put(K key, V value, ExpirationPolicy expirationPolicy, long duration, TimeUnit timeUnit) { + Assert.notNull(key, "key"); + Assert.notNull(expirationPolicy, "expirationPolicy"); + Assert.notNull(timeUnit, "timeUnit"); + Assert.operation(variableExpiration, "Variable expiration is not enabled"); + return putInternal(key, value, expirationPolicy, TimeUnit.NANOSECONDS.convert(duration, timeUnit)); + } + + @Override + public void putAll(Map map) { + Assert.notNull(map, "map"); + long expiration = expirationNanos.get(); + ExpirationPolicy expirationPolicy = this.expirationPolicy.get(); + writeLock.lock(); + try { + for (Entry entry : map.entrySet()) + putInternal(entry.getKey(), entry.getValue(), expirationPolicy, expiration); + } finally { + writeLock.unlock(); + } + } + + @Override + public V putIfAbsent(K key, V value) { + Assert.notNull(key, "key"); + writeLock.lock(); + try { + if (!entries.containsKey(key)) + return putInternal(key, value, expirationPolicy.get(), expirationNanos.get()); + else + return entries.get(key).getValue(); + } finally { + writeLock.unlock(); + } + } + + @Override + public V remove(Object key) { + Assert.notNull(key, "key"); + writeLock.lock(); + try { + ExpiringEntry entry = entries.remove(key); + if (entry == null) + return null; + if (entry.cancel()) + scheduleEntry(entries.first()); + return entry.getValue(); + } finally { + writeLock.unlock(); + } + } + + @Override + public boolean remove(Object key, Object value) { + Assert.notNull(key, "key"); + writeLock.lock(); + try { + ExpiringEntry entry = entries.get(key); + if (entry != null && entry.getValue().equals(value)) { + entries.remove(key); + if (entry.cancel()) + scheduleEntry(entries.first()); + return true; + } else + return false; + } finally { + writeLock.unlock(); + } + } + + @Override + public V replace(K key, V value) { + Assert.notNull(key, "key"); + writeLock.lock(); + try { + if (entries.containsKey(key)) { + return putInternal(key, value, expirationPolicy.get(), expirationNanos.get()); + } else + return null; + } finally { + writeLock.unlock(); + } + } + + @Override + public boolean replace(K key, V oldValue, V newValue) { + Assert.notNull(key, "key"); + writeLock.lock(); + try { + ExpiringEntry entry = entries.get(key); + if (entry != null && entry.getValue().equals(oldValue)) { + putInternal(key, newValue, expirationPolicy.get(), expirationNanos.get()); + return true; + } else + return false; + } finally { + writeLock.unlock(); + } + } + + /** + * Removes an expiration listener. + * + * @param listener + * @throws NullPointerException if {@code listener} is null + */ + public void removeExpirationListener(ExpirationListener listener) { + Assert.notNull(listener, "listener"); + for (int i = 0; i < expirationListeners.size(); i++) { + if (expirationListeners.get(i).equals(listener)) { + expirationListeners.remove(i); + return; + } + } + } + + /** + * Removes an asynchronous expiration listener. + * + * @param listener + * @throws NullPointerException if {@code listener} is null + */ + public void removeAsyncExpirationListener(ExpirationListener listener) { + Assert.notNull(listener, "listener"); + for (int i = 0; i < asyncExpirationListeners.size(); i++) { + if (asyncExpirationListeners.get(i).equals(listener)) { + asyncExpirationListeners.remove(i); + return; + } + } + } + + /** + * Resets expiration for the entry corresponding to {@code key}. + * + * @param key to reset expiration for + * @throws NullPointerException if {@code key} is null + */ + public void resetExpiration(K key) { + Assert.notNull(key, "key"); + ExpiringEntry entry = getEntry(key); + if (entry != null) + resetEntry(entry, false); + } + + /** + * Sets the expiration duration for the entry corresponding to the given key. Supported only if variable expiration is + * enabled. + * + * @param key Key to set expiration for + * @param duration the length of time after an entry is created that it should be removed + * @param timeUnit the unit that {@code duration} is expressed in + * @throws NullPointerException if {@code key} or {@code timeUnit} are null + * @throws UnsupportedOperationException If variable expiration is not enabled + */ + public void setExpiration(K key, long duration, TimeUnit timeUnit) { + Assert.notNull(key, "key"); + Assert.notNull(timeUnit, "timeUnit"); + Assert.operation(variableExpiration, "Variable expiration is not enabled"); + writeLock.lock(); + try { + ExpiringEntry entry = entries.get(key); + if (entry != null) { + entry.expirationNanos.set(TimeUnit.NANOSECONDS.convert(duration, timeUnit)); + resetEntry(entry, true); + } + } finally { + writeLock.unlock(); + } + } + + /** + * Updates the default map entry expiration. Supported only if variable expiration is enabled. + * + * @param duration the length of time after an entry is created that it should be removed + * @param timeUnit the unit that {@code duration} is expressed in + * @throws NullPointerException {@code timeUnit} is null + * @throws UnsupportedOperationException If variable expiration is not enabled + */ + public void setExpiration(long duration, TimeUnit timeUnit) { + Assert.notNull(timeUnit, "timeUnit"); + Assert.operation(variableExpiration, "Variable expiration is not enabled"); + expirationNanos.set(TimeUnit.NANOSECONDS.convert(duration, timeUnit)); + } + + /** + * Sets the global expiration policy for the map. Individual expiration policies may override the global policy. + * + * @param expirationPolicy + * @throws NullPointerException {@code expirationPolicy} is null + */ + public void setExpirationPolicy(ExpirationPolicy expirationPolicy) { + Assert.notNull(expirationPolicy, "expirationPolicy"); + this.expirationPolicy.set(expirationPolicy); + } + + /** + * Sets the expiration policy for the entry corresponding to the given key. + * + * @param key to set policy for + * @param expirationPolicy to set + * @throws NullPointerException if {@code key} or {@code expirationPolicy} are null + * @throws UnsupportedOperationException If variable expiration is not enabled + */ + public void setExpirationPolicy(K key, ExpirationPolicy expirationPolicy) { + Assert.notNull(key, "key"); + Assert.notNull(expirationPolicy, "expirationPolicy"); + Assert.operation(variableExpiration, "Variable expiration is not enabled"); + ExpiringEntry entry = getEntry(key); + if (entry != null) + entry.expirationPolicy.set(expirationPolicy); + } + + /** + * Sets the maximum size of the map. Once this size has been reached, adding an additional entry will expire the + * first entry in line for expiration based on the expiration policy. + * + * @param maxSize The maximum size of the map. + */ + public void setMaxSize(int maxSize) { + Assert.operation(maxSize > 0, "maxSize"); + this.maxSize = maxSize; + } + + @Override + public int size() { + readLock.lock(); + try { + return entries.size(); + } finally { + readLock.unlock(); + } + } + + @Override + public String toString() { + readLock.lock(); + try { + return entries.toString(); + } finally { + readLock.unlock(); + } + } + + /** + * Returns a copy of the map's values, which can be iterated over safely by multiple threads. + * + * @return Copied set of map values. + */ + @Override + public Collection values() { + return new AbstractCollection() { + @Override + public void clear() { + ExpiringMap.this.clear(); + } + + @Override + public boolean contains(Object value) { + return containsValue(value); + } + + @Override + public Iterator iterator() { + readLock.lock(); + try { + return (entries instanceof EntryLinkedHashMap) ? ((EntryLinkedHashMap) entries).new ValueIterator() + : ((EntryTreeHashMap) entries).new ValueIterator(); + } finally { + readLock.unlock(); + } + } + + @Override + public int size() { + return ExpiringMap.this.size(); + } + }; + } + + /** + * Notifies expiration listeners that the given entry expired. Must not be called from within a locked context. + * + * @param entry Entry to expire + */ + void notifyListeners(final ExpiringEntry entry) { + if (asyncExpirationListeners != null) + for (final ExpirationListener listener : asyncExpirationListeners) { + LISTENER_SERVICE.execute(new Runnable() { + public void run() { + try { + listener.expired(entry.key, entry.getValue()); + } catch (Exception ignoreUserExceptions) { + } + } + }); + } + + if (expirationListeners != null) + for (final ExpirationListener listener : expirationListeners) { + try { + listener.expired(entry.key, entry.getValue()); + } catch (Exception ignoreUserExceptions) { + } + } + } + + /** + * Returns the internal ExpiringEntry for the {@code key}, obtaining a read lock. + */ + ExpiringEntry getEntry(Object key) { + readLock.lock(); + try { + return entries.get(key); + } finally { + readLock.unlock(); + } + } + + /** + * Puts the given key/value in storage, scheduling the new entry for expiration if needed. If a previous value existed + * for the given key, it is first cancelled and the entries reordered to reflect the new expiration. + */ + V putInternal(K key, V value, ExpirationPolicy expirationPolicy, long expirationNanos) { + writeLock.lock(); + try { + ExpiringEntry entry = entries.get(key); + V oldValue = null; + + if (entry == null) { + entry = new ExpiringEntry(key, value, + variableExpiration ? new AtomicReference(expirationPolicy) : this.expirationPolicy, + variableExpiration ? new AtomicLong(expirationNanos) : this.expirationNanos); + if (entries.size() >= maxSize) { + ExpiringEntry expiredEntry = entries.first(); + entries.remove(expiredEntry.key); + notifyListeners(expiredEntry); + } + entries.put(key, entry); + if (entries.size() == 1 || entries.first().equals(entry)) + scheduleEntry(entry); + } else { + oldValue = entry.getValue(); + if (!ExpirationPolicy.ACCESSED.equals(expirationPolicy) + && ((oldValue == null && value == null) || (oldValue != null && oldValue.equals(value)))) + return value; + + entry.setValue(value); + resetEntry(entry, false); + } + + return oldValue; + } finally { + writeLock.unlock(); + } + } + + /** + * Resets the given entry's schedule canceling any existing scheduled expiration and reordering the entry in the + * internal map. Schedules the next entry in the map if the given {@code entry} was scheduled or if + * {@code scheduleNext} is true. + * + * @param entry to reset + * @param scheduleFirstEntry whether the first entry should be automatically scheduled + */ + void resetEntry(ExpiringEntry entry, boolean scheduleFirstEntry) { + writeLock.lock(); + try { + boolean scheduled = entry.cancel(); + entries.reorder(entry); + + if (scheduled || scheduleFirstEntry) + scheduleEntry(entries.first()); + } finally { + writeLock.unlock(); + } + } + + /** + * Schedules an entry for expiration. Guards against concurrent schedule/schedule, cancel/schedule and schedule/cancel + * calls. + * + * @param entry Entry to schedule + */ + void scheduleEntry(ExpiringEntry entry) { + if (entry == null || entry.scheduled) + return; + + Runnable runnable = null; + synchronized (entry) { + if (entry.scheduled) + return; + + final WeakReference> entryReference = new WeakReference>(entry); + runnable = new Runnable() { + @Override + public void run() { + ExpiringEntry entry = entryReference.get(); + + writeLock.lock(); + try { + if (entry != null && entry.scheduled) { + entries.remove(entry.key); + notifyListeners(entry); + } + + try { + // Expires entries and schedules the next entry + Iterator> iterator = entries.valuesIterator(); + boolean schedulePending = true; + + while (iterator.hasNext() && schedulePending) { + ExpiringEntry nextEntry = iterator.next(); + if (nextEntry.expectedExpiration.get() <= System.nanoTime()) { + iterator.remove(); + notifyListeners(nextEntry); + } else { + scheduleEntry(nextEntry); + schedulePending = false; + } + } + } catch (NoSuchElementException ignored) { + } + } finally { + writeLock.unlock(); + } + } + }; + + Future entryFuture = EXPIRER.schedule(runnable, entry.expectedExpiration.get() - System.nanoTime(), + TimeUnit.NANOSECONDS); + entry.schedule(entryFuture); + } + } + + private static Entry mapEntryFor(final ExpiringEntry entry) { + return new Entry() { + @Override + public K getKey() { + return entry.key; + } + + @Override + public V getValue() { + return entry.value; + } + + @Override + public V setValue(V value) { + throw new UnsupportedOperationException(); + } + }; + } + + private void initListenerService() { + synchronized (ExpiringMap.class) { + if (LISTENER_SERVICE == null) { + LISTENER_SERVICE = (ThreadPoolExecutor) Executors.newCachedThreadPool( + THREAD_FACTORY == null ? new NamedThreadFactory("ExpiringMap-Listener-%s") : THREAD_FACTORY); + } + } + } +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/expiringmap/ExpiringValue.java b/src/main/java/com/io/yutian/elementoriginlib/expiringmap/ExpiringValue.java new file mode 100644 index 0000000..7e97e0f --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/expiringmap/ExpiringValue.java @@ -0,0 +1,122 @@ +package com.io.yutian.elementoriginlib.expiringmap; + +import java.util.concurrent.TimeUnit; + +/** + * A value which should be stored in an {@link ExpiringMap} with optional control over its expiration. + * + * @param the type of value being stored + */ +public final class ExpiringValue { + private static final long UNSET_DURATION = -1L; + private final V value; + private final ExpirationPolicy expirationPolicy; + private final long duration; + private final TimeUnit timeUnit; + + /** + * Creates an ExpiringValue to be stored in an {@link ExpiringMap}. The map's default values for + * {@link ExpirationPolicy expiration policy} and {@link ExpiringMap#getExpiration()} expiration} will be used. + * + * @param value the value to store + * @see ExpiringMap#put(Object, Object) + */ + public ExpiringValue(V value) { + this(value, UNSET_DURATION, null, null); + } + + /** + * Creates an ExpiringValue to be stored in an {@link ExpiringMap}. The map's default + * {@link ExpiringMap#getExpiration()} expiration} will be used. + * + * @param value the value to store + * @param expirationPolicy the expiration policy for the value + * @see ExpiringMap#put(Object, Object, ExpirationPolicy) + */ + public ExpiringValue(V value, ExpirationPolicy expirationPolicy) { + this(value, UNSET_DURATION, null, expirationPolicy); + } + + /** + * Creates an ExpiringValue to be stored in an {@link ExpiringMap}. The map's default {@link ExpirationPolicy + * expiration policy} will be used. + * + * @param value the value to store + * @param duration the length of time after an entry is created that it should be removed + * @param timeUnit the unit that {@code duration} is expressed in + * @see ExpiringMap#put(Object, Object, long, TimeUnit) + * @throws NullPointerException on null timeUnit + */ + public ExpiringValue(V value, long duration, TimeUnit timeUnit) { + this(value, duration, timeUnit, null); + if (timeUnit == null) { + throw new NullPointerException(); + } + } + + /** + * Creates an ExpiringValue to be stored in an {@link ExpiringMap}. + * + * @param value the value to store + * @param duration the length of time after an entry is created that it should be removed + * @param timeUnit the unit that {@code duration} is expressed in + * @param expirationPolicy the expiration policy for the value + * @see ExpiringMap#put(Object, Object, ExpirationPolicy, long, TimeUnit) + * @throws NullPointerException on null timeUnit + */ + public ExpiringValue(V value, ExpirationPolicy expirationPolicy, long duration, TimeUnit timeUnit) { + this(value, duration, timeUnit, expirationPolicy); + if (timeUnit == null) { + throw new NullPointerException(); + } + } + + private ExpiringValue(V value, long duration, TimeUnit timeUnit, ExpirationPolicy expirationPolicy) { + this.value = value; + this.expirationPolicy = expirationPolicy; + this.duration = duration; + this.timeUnit = timeUnit; + } + + public V getValue() { + return value; + } + + public ExpirationPolicy getExpirationPolicy() { + return expirationPolicy; + } + + public long getDuration() { + return duration; + } + + public TimeUnit getTimeUnit() { + return timeUnit; + } + + @Override + public int hashCode() { + return value != null ? value.hashCode() : 0; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + ExpiringValue that = (ExpiringValue) o; + return !(value != null ? !value.equals(that.value) : that.value != null) + && expirationPolicy == that.expirationPolicy && duration == that.duration && timeUnit == that.timeUnit; + + } + + @Override + public String toString() { + return "ExpiringValue{" + "value=" + value + ", expirationPolicy=" + expirationPolicy + ", duration=" + duration + + ", timeUnit=" + timeUnit + '}'; + } +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/expiringmap/internal/Assert.java b/src/main/java/com/io/yutian/elementoriginlib/expiringmap/internal/Assert.java new file mode 100644 index 0000000..f30ee71 --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/expiringmap/internal/Assert.java @@ -0,0 +1,32 @@ +package com.io.yutian.elementoriginlib.expiringmap.internal; + +import java.util.NoSuchElementException; + +/** + * @author Jonathan Halterman + */ +public final class Assert { + private Assert() { + } + + public static T notNull(T reference, String parameterName) { + if (reference == null) + throw new NullPointerException(parameterName + " cannot be null"); + return reference; + } + + public static void operation(boolean condition, String message) { + if (!condition) + throw new UnsupportedOperationException(message); + } + + public static void state(boolean expression, String errorMessageFormat, Object... args) { + if (!expression) + throw new IllegalStateException(String.format(errorMessageFormat, args)); + } + + public static void element(Object element, Object key) { + if (element == null) + throw new NoSuchElementException(key.toString()); + } +} \ No newline at end of file diff --git a/src/main/java/com/io/yutian/elementoriginlib/expiringmap/internal/NamedThreadFactory.java b/src/main/java/com/io/yutian/elementoriginlib/expiringmap/internal/NamedThreadFactory.java new file mode 100644 index 0000000..52f0705 --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/expiringmap/internal/NamedThreadFactory.java @@ -0,0 +1,27 @@ +package com.io.yutian.elementoriginlib.expiringmap.internal; + +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Named thread factory. + */ +public class NamedThreadFactory implements ThreadFactory { + private final AtomicInteger threadNumber = new AtomicInteger(1); + private final String nameFormat; + + /** + * Creates a thread factory that names threads according to the {@code nameFormat} by supplying a + * single argument to the format representing the thread number. + */ + public NamedThreadFactory(String nameFormat) { + this.nameFormat = nameFormat; + } + + @Override + public Thread newThread(Runnable r) { + Thread thread = new Thread(r, String.format(nameFormat, threadNumber.getAndIncrement())); + thread.setDaemon(true); + return thread; + } +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/gui/Gui.java b/src/main/java/com/io/yutian/elementoriginlib/gui/Gui.java new file mode 100644 index 0000000..3190b95 --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/gui/Gui.java @@ -0,0 +1,133 @@ +package com.io.yutian.elementoriginlib.gui; + +import com.io.yutian.elementoriginlib.ElementOriginLib; +import com.io.yutian.elementoriginlib.gui.button.Button; +import com.io.yutian.elementoriginlib.gui.button.ClickType; +import com.io.yutian.elementoriginlib.gui.button.ItemButton; +import net.kyori.adventure.text.Component; +import org.bukkit.Sound; +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.InventoryAction; +import org.bukkit.event.inventory.InventoryClickEvent; + +import java.util.HashMap; +import java.util.Map; + +public class Gui extends IGui { + + private Map buttons = new HashMap<>(); + + public Gui(Player player, Component title, int size) { + super(player, title, size); + } + + @Override + public void init() { + } + + @Override + public void handler(Player player, int slot, InventoryClickEvent event) { + if (buttons.containsKey(slot)) { + Button button = buttons.get(slot); + if (button == null) { + if (slot < inventory.getSize()) { + event.setCancelled(true); + } else { + if (event.getAction().equals(InventoryAction.MOVE_TO_OTHER_INVENTORY)) { + event.setCancelled(true); + } + } + return; + } + event.setCancelled(true); + clickButton(event, slot, button); + if (button.isPlaySound()) { + player.playSound(player.getLocation(), Sound.UI_BUTTON_CLICK, 1.0f, 1.0f); + } + InventoryAction action = event.getAction(); + ClickType clickType = ClickType.LEFT_CLICK; + if (action.equals(InventoryAction.PICKUP_ALL)) { + clickType = ClickType.LEFT_CLICK; + } else if (action.equals(InventoryAction.PICKUP_HALF)) { + clickType = ClickType.RIGHT_CLICK; + } else if (action.equals(InventoryAction.MOVE_TO_OTHER_INVENTORY)) { + clickType = ClickType.SHIFT_CLICK; + } + if (button.getClickConsumer() != null) { + button.getClickConsumer().accept(player, clickType); + } + } else { + if (slot < inventory.getSize()) { + event.setCancelled(true); + } + } + } + + public void addButton(int index, Button button) { + buttons.put(index, button); + } + + public Button getButton(int index) { + return buttons.getOrDefault(index, null); + } + + public final void initButton(int index) { + inventory.setItem(index, null); + Button button = getButton(index); + if (button == null) { + return; + } + if (button.isAsynchronous()) { + ElementOriginLib.inst().getServer().getScheduler().runTaskAsynchronously(ElementOriginLib.inst(), ()->{ + if (button instanceof ItemButton itemButton) { + if (itemButton.getItem() != null) { + inventory.setItem(index, itemButton.getItem()); + return; + } + } + inventory.setItem(index, button.getItemStack()); + }); + } else { + if (button instanceof ItemButton itemButton) { + if (itemButton.getItem() != null) { + inventory.setItem(index, itemButton.getItem()); + return; + } + } + inventory.setItem(index, button.getItemStack()); + } + } + + public final void initButton() { + for (int i = 0; i < inventory.getSize(); i++) { + inventory.setItem(i, null); + } + for (Map.Entry entry : buttons.entrySet()) { + Button button = entry.getValue(); + if (button.isAsynchronous()) { + ElementOriginLib.inst().getServer().getScheduler().runTaskAsynchronously(ElementOriginLib.inst(), ()->{ + if (button instanceof ItemButton itemButton) { + if (itemButton.getItem() != null) { + inventory.setItem(entry.getKey(), itemButton.getItem()); + return; + } + } + inventory.setItem(entry.getKey(), button.getItemStack()); + }); + } else { + if (button instanceof ItemButton itemButton) { + if (itemButton.getItem() != null) { + inventory.setItem(entry.getKey(), itemButton.getItem()); + continue; + } + } + inventory.setItem(entry.getKey(), button.getItemStack()); + } + } + } + + public Map getButtons() { + return buttons; + } + +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/gui/GuiHandler.java b/src/main/java/com/io/yutian/elementoriginlib/gui/GuiHandler.java new file mode 100644 index 0000000..8629c43 --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/gui/GuiHandler.java @@ -0,0 +1,68 @@ +package com.io.yutian.elementoriginlib.gui; + +import com.io.yutian.elementoriginlib.listener.IListener; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.inventory.InventoryAction; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.event.inventory.InventoryCloseEvent; +import org.bukkit.event.inventory.InventoryDragEvent; +import org.bukkit.inventory.InventoryHolder; +import org.bukkit.plugin.Plugin; + +public class GuiHandler extends IListener { + + public GuiHandler(Plugin plugin) { + super(plugin); + } + + @EventHandler(priority = EventPriority.HIGHEST) + public void onInventoryClick(InventoryClickEvent event) { + if (event.getInventory().getHolder() == null) { + return; + } + Player player = (Player) event.getWhoClicked(); + InventoryHolder holder = event.getInventory().getHolder(); + if (holder instanceof IGui iGui) { + if (event.getClickedInventory() != event.getInventory()) { + if (event.getAction().equals(InventoryAction.MOVE_TO_OTHER_INVENTORY) || event.getAction().equals(InventoryAction.COLLECT_TO_CURSOR)) { + event.setCancelled(true); + return; + } + } + if (event.getAction().equals(InventoryAction.HOTBAR_SWAP) || event.getAction().equals(InventoryAction.HOTBAR_MOVE_AND_READD)) { + event.setCancelled(true); + player.getInventory().setItemInOffHand(player.getInventory().getItemInOffHand()); + return; + } + int slot = event.getRawSlot(); + iGui.handler(player, slot, event); + } + } + + @EventHandler + public void onInventoryDrag(InventoryDragEvent event) { + if (event.getInventory().getHolder() != null) { + InventoryHolder inventoryHolder = event.getInventory().getHolder(); + if (inventoryHolder instanceof IGui) { + if (inventoryHolder instanceof IGuiDrag iGuiDrag) { + iGuiDrag.onInventoryDrag(event); + } else { + event.setCancelled(true); + } + } + } + } + + @EventHandler + public void onInventoryClose(InventoryCloseEvent event) { + if (event.getInventory().getHolder() != null) { + InventoryHolder inventoryHolder = event.getInventory().getHolder(); + if (inventoryHolder instanceof IGui iGui) { + iGui.close(event); + } + } + } + +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/gui/IGui.java b/src/main/java/com/io/yutian/elementoriginlib/gui/IGui.java new file mode 100644 index 0000000..8e5e904 --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/gui/IGui.java @@ -0,0 +1,142 @@ +package com.io.yutian.elementoriginlib.gui; + +import com.io.yutian.elementoriginlib.gui.button.Button; +import com.io.yutian.elementoriginlib.gui.button.ButtonHandler; +import com.io.yutian.elementoriginlib.gui.button.ItemButton; +import com.io.yutian.elementoriginlib.gui.button.ItemSlotButton; +import net.kyori.adventure.text.Component; +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.event.inventory.InventoryCloseEvent; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.InventoryHolder; +import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.NotNull; + +public abstract class IGui implements InventoryHolder { + + public Inventory inventory; + public Player player; + + private Component title; + private int size; + + public IGui(Player player, Component title, int size) { + this.inventory = Bukkit.createInventory(this, size, title); + this.title = title; + this.size = size; + this.player = player; + } + + public abstract void init(); + + public abstract void handler(Player player, int slot, InventoryClickEvent event); + + public void close(InventoryCloseEvent event) { + } + + public void open() { + player.openInventory(inventory); + } + + public void clickButton(InventoryClickEvent event, int slot, Button button) { + if (button instanceof ItemButton itemButton) { + ItemStack itemStack = event.getCurrentItem(); + ItemStack item = event.getCursor(); + if (itemButton.isItem(itemStack)) { + if (item != null & !item.getType().equals(Material.AIR)) { + if (itemButton.getPutPredicate() != null && !itemButton.getPutPredicate().test(player, item)) { + return; + } + if (itemButton.getClickItemConsumer() != null) { + itemButton.getClickItemConsumer().accept(player, item); + } + player.setItemOnCursor(null); + if (itemButton.getPutItemFunction() != null) { + ItemStack itemStack1 = itemButton.getPutItemFunction().apply(item); + itemButton.setItem(itemStack1); + inventory.setItem(slot, itemStack1); + } else { + itemButton.setItem(item); + inventory.setItem(slot, item); + } + } + } else { + if (item == null || item.getType().equals(Material.AIR)) { + if (itemButton.getClickItemConsumer() != null) { + itemButton.getClickItemConsumer().accept(player, item); + } + player.setItemOnCursor(itemStack); + itemButton.setItem(null); + inventory.setItem(slot, button.getItemStack()); + } else { + if (itemButton.getPutPredicate() != null && !itemButton.getPutPredicate().test(player, item)) { + return; + } + player.setItemOnCursor(itemStack); + if (itemButton.getPutItemFunction() != null) { + ItemStack itemStack1 = itemButton.getPutItemFunction().apply(item); + if (itemButton.getClickItemConsumer() != null) { + itemButton.getClickItemConsumer().accept(player, itemStack1); + } + itemButton.setItem(itemStack1); + inventory.setItem(slot, itemStack1); + } else { + if (itemButton.getClickItemConsumer() != null) { + itemButton.getClickItemConsumer().accept(player, item); + } + itemButton.setItem(item); + inventory.setItem(slot, item); + } + } + } + } else if (button instanceof ItemSlotButton itemSlotButton) { + ItemStack itemStack = event.getCurrentItem(); + ItemStack item = event.getCursor(); + if (item == null || item.getType().equals(Material.AIR)) { + if (itemStack == null || itemStack.getType().equals(Material.AIR)) { + return; + } + inventory.setItem(slot, null); + itemSlotButton.setItemStack(null); + player.setItemOnCursor(itemStack); + if (itemSlotButton.getPickItemFunction() != null) { + itemSlotButton.getPickItemFunction().accept(itemStack); + } + } else { + if (itemStack == null || itemStack.getType().equals(Material.AIR)) { + if (!itemSlotButton.isCanPut()) { + return; + } + player.setItemOnCursor(null); + itemSlotButton.setItemStack(item); + inventory.setItem(slot, item); + if (itemSlotButton.getPutItemFunction() != null) { + itemSlotButton.getPutItemFunction().accept(item); + } + } else { + if (!itemSlotButton.isCanPut()) { + return; + } + player.setItemOnCursor(itemStack); + itemSlotButton.setItemStack(item); + inventory.setItem(slot, item); + if (itemSlotButton.getPutItemFunction() != null) { + itemSlotButton.getPutItemFunction().accept(item); + } + } + } + } else if (button instanceof ButtonHandler buttonHandler) { + buttonHandler.handler(event, slot, button); + } + } + + @NotNull + @Override + public Inventory getInventory() { + return inventory; + } + +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/gui/IGuiDrag.java b/src/main/java/com/io/yutian/elementoriginlib/gui/IGuiDrag.java new file mode 100644 index 0000000..b3f6a3c --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/gui/IGuiDrag.java @@ -0,0 +1,9 @@ +package com.io.yutian.elementoriginlib.gui; + +import org.bukkit.event.inventory.InventoryDragEvent; + +public interface IGuiDrag { + + void onInventoryDrag(InventoryDragEvent event); + +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/gui/PageGui.java b/src/main/java/com/io/yutian/elementoriginlib/gui/PageGui.java new file mode 100644 index 0000000..e63b1cb --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/gui/PageGui.java @@ -0,0 +1,174 @@ +package com.io.yutian.elementoriginlib.gui; + +import com.io.yutian.elementoriginlib.ElementOriginLib; +import com.io.yutian.elementoriginlib.gui.button.Button; +import com.io.yutian.elementoriginlib.gui.button.ClickType; +import com.io.yutian.elementoriginlib.gui.button.ItemButton; +import net.kyori.adventure.text.Component; +import org.bukkit.Sound; +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.InventoryAction; +import org.bukkit.event.inventory.InventoryClickEvent; + +import java.util.HashMap; +import java.util.Map; + +public class PageGui extends IGui { + + private int page = 1; + private int maxPage; + + private Map pages = new HashMap<>(); + + public PageGui(Player player, Component title, int size, int maxPage) { + super(player, title, size); + this.maxPage = maxPage; + } + + public void initButton() { + initButton(this.page); + } + + public void initButton(int page) { + if (page > pages.size()) { + return; + } + for (int i = 0; i < inventory.getSize(); i++) { + inventory.setItem(i, null); + } + if (pages.containsKey(page)) { + Page page1 = pages.get(page); + for (Map.Entry entry : page1.buttons.entrySet()) { + Button button = entry.getValue(); + if (button.isAsynchronous()) { + ElementOriginLib.inst().getServer().getScheduler().runTaskAsynchronously(ElementOriginLib.inst(), ()->{ + if (button instanceof ItemButton itemButton) { + if (itemButton.getItem() != null) { + inventory.setItem(entry.getKey(), itemButton.getItem()); + return; + } + } + inventory.setItem(entry.getKey(), button.getItemStack()); + }); + } else { + if (button instanceof ItemButton itemButton) { + if (itemButton.getItem() != null) { + inventory.setItem(entry.getKey(), itemButton.getItem()); + continue; + } + } + inventory.setItem(entry.getKey(), button.getItemStack()); + } + } + } + } + + @Override + public void init() { + } + + @Override + public void handler(Player player, int slot, InventoryClickEvent event) { + if (pages.containsKey(page)) { + Page page1 = getPage(page); + Button button = page1.getButton(slot); + if (button == null) { + if (slot < inventory.getSize()) { + event.setCancelled(true); + } else { + if (event.getAction().equals(InventoryAction.MOVE_TO_OTHER_INVENTORY)) { + event.setCancelled(true); + } + } + return; + } + event.setCancelled(true); + clickButton(event, slot, button); + if (button.isPlaySound()) { + player.playSound(player.getLocation(), Sound.UI_BUTTON_CLICK, 1.0f, 1.0f); + } + InventoryAction action = event.getAction(); + ClickType clickType = ClickType.LEFT_CLICK; + if (action.equals(InventoryAction.PICKUP_ALL)) { + clickType = ClickType.LEFT_CLICK; + } else if (action.equals(InventoryAction.PICKUP_HALF)) { + clickType = ClickType.RIGHT_CLICK; + } else if (action.equals(InventoryAction.MOVE_TO_OTHER_INVENTORY)) { + clickType = ClickType.SHIFT_CLICK; + } + if (button.getClickConsumer() != null) { + button.getClickConsumer().accept(player, clickType); + } + } + } + + public Page getPage(int page) { + if (page <= 0 || page > maxPage) { + return null; + } + if (!pages.containsKey(page)) { + pages.put(page, new Page()); + } + return pages.get(page); + } + + public void setPage(int page) { + this.page = page; + } + + public void next() { + if (page >= maxPage) { + return; + } + this.page ++; + initButton(); + } + + public void up() { + if (page <= 1) { + return; + } + this.page --; + initButton(); + } + + public int getPage() { + return page; + } + + public int getMaxPage() { + return maxPage; + } + + public void setMaxPage(int maxPage) { + this.maxPage = maxPage; + } + + public void addButton(int page, int index, Button button) { + Page page1 = pages.getOrDefault(page, new Page()); + page1.buttons.put(index, button); + pages.put(page, page1); + } + + public static class Page { + + private Map buttons = new HashMap<>(); + + public Page() { + } + + public void addButton(int index, Button button) { + buttons.put(index, button); + } + + public Button getButton(int index) { + return buttons.getOrDefault(index, null); + } + + public Map getButtons() { + return buttons; + } + + } + +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/gui/button/Button.java b/src/main/java/com/io/yutian/elementoriginlib/gui/button/Button.java new file mode 100644 index 0000000..2971b19 --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/gui/button/Button.java @@ -0,0 +1,56 @@ +package com.io.yutian.elementoriginlib.gui.button; + +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; + +import java.util.function.BiConsumer; + +public class Button { + + private ItemStack itemStack; + + private BiConsumer clickConsumer; + + private boolean asynchronous = false; + private boolean playSound = true; + + public Button(ItemStack itemStack) { + this.itemStack = itemStack; + } + + public void setItemStack(ItemStack itemStack) { + this.itemStack = itemStack; + } + + public Button click(BiConsumer consumer) { + this.clickConsumer = consumer; + return this; + } + + public Button asyn() { + this.asynchronous = true; + return this; + } + + public Button noSound() { + this.playSound = false; + return this; + } + + public ItemStack getItemStack() { + return itemStack; + } + + public BiConsumer getClickConsumer() { + return clickConsumer; + } + + public boolean isAsynchronous() { + return asynchronous; + } + + public boolean isPlaySound() { + return playSound; + } + +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/gui/button/ButtonHandler.java b/src/main/java/com/io/yutian/elementoriginlib/gui/button/ButtonHandler.java new file mode 100644 index 0000000..a858d29 --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/gui/button/ButtonHandler.java @@ -0,0 +1,9 @@ +package com.io.yutian.elementoriginlib.gui.button; + +import org.bukkit.event.inventory.InventoryClickEvent; + +public interface ButtonHandler { + + void handler(InventoryClickEvent event, int slot, Button button); + +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/gui/button/ClickType.java b/src/main/java/com/io/yutian/elementoriginlib/gui/button/ClickType.java new file mode 100644 index 0000000..cd7bb6d --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/gui/button/ClickType.java @@ -0,0 +1,9 @@ +package com.io.yutian.elementoriginlib.gui.button; + +public enum ClickType { + + LEFT_CLICK, + RIGHT_CLICK, + SHIFT_CLICK + +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/gui/button/ItemButton.java b/src/main/java/com/io/yutian/elementoriginlib/gui/button/ItemButton.java new file mode 100644 index 0000000..3ec04d6 --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/gui/button/ItemButton.java @@ -0,0 +1,75 @@ +package com.io.yutian.elementoriginlib.gui.button; + +import com.io.yutian.elementoriginlib.nbt.NBTItem; +import com.io.yutian.elementoriginlib.nbt.NBTString; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; + +import java.util.function.BiConsumer; +import java.util.function.BiPredicate; +import java.util.function.Function; + +public class ItemButton extends Button { + + private BiConsumer clickItemConsumer; + + private Function putItemFunction; + private BiPredicate putPredicate; + + private ItemStack item; + + public ItemButton(ItemStack itemStack) { + super(itemStack); + NBTItem nbtItem = new NBTItem(itemStack); + nbtItem.editTag((nbtCompound -> { + nbtCompound.putString("gui_meta", "item_button"); + })); + setItemStack(nbtItem.getItemStack()); + } + + public ItemButton setItem(ItemStack item) { + this.item = item; + return this; + } + + public ItemStack getItem() { + return item; + } + + public ItemButton itemPredicate(BiPredicate predicate) { + this.putPredicate = predicate; + return this; + } + + public ItemButton putItem(Function putItemFunction) { + this.putItemFunction = putItemFunction; + return this; + } + + public boolean isItem() { + return isItem(item); + } + + public boolean isItem(ItemStack item) { + NBTItem nbtItem = new NBTItem(item); + return nbtItem.has("gui_meta", NBTString.TYPE_ID) && ((NBTString) nbtItem.get("gui_meta")).getString().equals("item_button"); + } + + public ItemButton clickItem(BiConsumer consumer) { + this.clickItemConsumer = consumer; + return this; + } + + public BiConsumer getClickItemConsumer() { + return clickItemConsumer; + } + + public Function getPutItemFunction() { + return putItemFunction; + } + + public BiPredicate getPutPredicate() { + return putPredicate; + } + +} \ No newline at end of file diff --git a/src/main/java/com/io/yutian/elementoriginlib/gui/button/ItemSlotButton.java b/src/main/java/com/io/yutian/elementoriginlib/gui/button/ItemSlotButton.java new file mode 100644 index 0000000..ec57047 --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/gui/button/ItemSlotButton.java @@ -0,0 +1,46 @@ +package com.io.yutian.elementoriginlib.gui.button; + +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; + +import java.util.function.Consumer; + +public class ItemSlotButton extends Button { + + private Consumer putItemFunction; + private Consumer pickItemFunction; + + private boolean canPut = true; + + public ItemSlotButton() { + super(new ItemStack(Material.AIR)); + } + + public ItemSlotButton putItem(Consumer putItemConsumer) { + this.putItemFunction = putItemConsumer; + return this; + } + + public ItemSlotButton pickItem(Consumer pickItemConsumer) { + this.pickItemFunction = pickItemConsumer; + return this; + } + + public ItemSlotButton canPut(boolean can) { + this.canPut = can; + return this; + } + + public boolean isCanPut() { + return canPut; + } + + public Consumer getPickItemFunction() { + return pickItemFunction; + } + + public Consumer getPutItemFunction() { + return putItemFunction; + } + +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/lang/Lang.java b/src/main/java/com/io/yutian/elementoriginlib/lang/Lang.java new file mode 100644 index 0000000..188c355 --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/lang/Lang.java @@ -0,0 +1,81 @@ +package com.io.yutian.elementoriginlib.lang; + +import org.bukkit.ChatColor; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.plugin.Plugin; + +import java.io.File; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class Lang { + + private static Map> langFileMap = new HashMap<>(); + + private static Map langMap = new HashMap<>(); + + public static String get(String key) { + return langMap.getOrDefault(key, "§o"+key); + } + + public static String get(String key, Object... args) { + String s = langMap.getOrDefault(key, "§o"+key); + for (int i = 0; i < args.length; i++) { + s = s.replace("$"+i, String.valueOf(args[i])); + } + return s; + } + + public static void reload(Plugin plugin) { + if (!langFileMap.containsKey(plugin)) { + return; + } + List list = langFileMap.get(plugin); + list.forEach(Lang::loadFile); + } + + public static void reload() { + langMap.clear(); + for (List files : langFileMap.values()) { + files.forEach(Lang::loadFile); + } + } + + private static void loadFile(File file) { + FileConfiguration fileConfiguration = YamlConfiguration.loadConfiguration(file); + for (String key : fileConfiguration.getKeys(true)) { + if (!fileConfiguration.isString(key)) { + continue; + } + String string = fileConfiguration.getString(key); + string = ChatColor.translateAlternateColorCodes('&', string); + langMap.put(key, string); + } + } + + public static void registerLangFile(Plugin plugin) { + registerLangFile(plugin, getFile(plugin)); + } + + public static void registerLangFile(Plugin plugin, File file) { + if (!file.exists()) { + return; + } + List files = langFileMap.getOrDefault(plugin, new ArrayList<>()); + files.add(file); + langFileMap.put(plugin, files); + loadFile(file); + } + + public static File getFile(Plugin plugin) { + File file = new File(plugin.getDataFolder()+File.separator+ "lang.yml"); + if (!file.exists()) { + plugin.saveResource("lang.yml", false); + } + return file; + } + +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/list/PageList.java b/src/main/java/com/io/yutian/elementoriginlib/list/PageList.java new file mode 100644 index 0000000..90421b5 --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/list/PageList.java @@ -0,0 +1,53 @@ +package com.io.yutian.elementoriginlib.list; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class PageList { + + private List list; + private Map> map = new HashMap<>(); + private int amount; + + public PageList(List list, int amount) { + this.list = list; + this.amount = amount; + if (list == null) { + return; + } + if (list.size() <= amount) { + List newList = new ArrayList(); + list.forEach(o-> newList.add(o)); + map.put(1, newList); + } else { + int x = 0; + int c = list.size() / amount; + for (int j = 0; j <= c;j++) { + int min = j * amount; + int max = (j+1) * amount; + List newList = new ArrayList(); + for (int k = min; k < max; k++) { + if (k >= list.size()) { + break; + } + newList.add(list.get(k)); + } + map.put(j+1, newList); + } + } + } + + public int size() { + return map.size(); + } + + public List getList(int page) { + if (page <= 0) { + page = 1; + } + return map.get(page); + } + +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/listener/GuiHandlerListener.java b/src/main/java/com/io/yutian/elementoriginlib/listener/GuiHandlerListener.java new file mode 100644 index 0000000..fde5414 --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/listener/GuiHandlerListener.java @@ -0,0 +1,66 @@ +package com.io.yutian.elementoriginlib.listener; + +import com.io.yutian.elementoriginlib.gui.IGui; +import com.io.yutian.elementoriginlib.gui.IGuiDrag; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.inventory.*; +import org.bukkit.inventory.InventoryHolder; +import org.bukkit.plugin.Plugin; + +public final class GuiHandlerListener extends IListener { + + public GuiHandlerListener(Plugin plugin) { + super(plugin); + } + + @EventHandler(priority = EventPriority.HIGHEST) + public void onInventoryClick(InventoryClickEvent event) { + if (event.getInventory().getHolder() == null) { + return; + } + Player player = (Player) event.getWhoClicked(); + InventoryHolder holder = event.getInventory().getHolder(); + if (holder instanceof IGui iGui) { + if (event.getClickedInventory() != event.getInventory()) { + if (event.getAction().equals(InventoryAction.MOVE_TO_OTHER_INVENTORY) || event.getAction().equals(InventoryAction.COLLECT_TO_CURSOR)) { + event.setCancelled(true); + return; + } + } + if (event.getAction().equals(InventoryAction.HOTBAR_SWAP) || event.getAction().equals(InventoryAction.HOTBAR_MOVE_AND_READD)) { + event.setCancelled(true); + player.getInventory().setItemInOffHand(player.getInventory().getItemInOffHand()); + return; + } + int slot = event.getRawSlot(); + iGui.handler(player, slot, event); + } + } + + @EventHandler + public void onInventoryDrag(InventoryDragEvent event) { + if (event.getInventory().getHolder() != null) { + InventoryHolder inventoryHolder = event.getInventory().getHolder(); + if (inventoryHolder instanceof IGui) { + if (inventoryHolder instanceof IGuiDrag iGuiDrag) { + iGuiDrag.onInventoryDrag(event); + } else { + event.setCancelled(true); + } + } + } + } + + @EventHandler + public void onInventoryClose(InventoryCloseEvent event) { + if (event.getInventory().getHolder() != null) { + InventoryHolder inventoryHolder = event.getInventory().getHolder(); + if (inventoryHolder instanceof IGui iGui) { + iGui.close(event); + } + } + } + +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/listener/IListener.java b/src/main/java/com/io/yutian/elementoriginlib/listener/IListener.java new file mode 100644 index 0000000..99ea929 --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/listener/IListener.java @@ -0,0 +1,12 @@ +package com.io.yutian.elementoriginlib.listener; + +import org.bukkit.event.Listener; +import org.bukkit.plugin.Plugin; + +public class IListener implements Listener { + + public IListener(Plugin plugin) { + plugin.getServer().getPluginManager().registerEvents(this, plugin); + } + +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/nbt/INBT.java b/src/main/java/com/io/yutian/elementoriginlib/nbt/INBT.java new file mode 100644 index 0000000..489d664 --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/nbt/INBT.java @@ -0,0 +1,180 @@ +package com.io.yutian.elementoriginlib.nbt; + +import net.minecraft.nbt.*; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public interface INBT { + + Object getValue(); + + byte getTypeId(); + + private static String getNBTTag(byte typeByte) { + return switch (typeByte) { + case 1 -> "NBTTagByte"; + case 2 -> "NBTTagShort"; + case 3 -> "NBTTagInt"; + case 4 -> "NBTTagLong"; + case 5 -> "NBTTagFloat"; + case 6 -> "NBTTagDouble"; + case 7 -> "NBTTagByteArray"; + case 8 -> "NBTTagString"; + case 9 -> "NBTTagList"; + case 10 -> "NBTTagCompound"; + case 11 -> "NBTTagIntArray"; + case 12 -> "NBTTagLongArray"; + default -> null; + }; + } + + static INBT asObject(Object object) { + if (object instanceof Byte b) { + return new NBTByte(b); + } else if (object instanceof Short value) { + return new NBTShort(value); + } else if (object instanceof Integer value) { + return new NBTInt(value); + } else if (object instanceof Long value) { + return new NBTLong(value); + } else if (object instanceof Float value) { + return new NBTFloat(value); + } else if (object instanceof Double value) { + return new NBTDouble(value); + } else if (object instanceof byte[] value) { + return new NBTByteArray(value); + } else if (object instanceof String value) { + return new NBTString(value); + } else if (object instanceof int[] value) { + return new NBTIntArray(value); + } else if (object instanceof long[] value) { + return new NBTLongArray(value); + } else if (object instanceof Boolean b) { + return new NBTByte((byte) (b ? 1 : 0)); + } + return null; + } + + static net.minecraft.nbt.NBTBase asNMS(INBT nbt) { + byte type = nbt.getTypeId(); + switch (type) { + case 1 -> { + NBTByte nbtByte = (NBTByte) nbt; + return NBTTagByte.a(nbtByte.getByte()); + } + case 2 -> { + NBTShort nbtShort = (NBTShort) nbt; + return NBTTagShort.a(nbtShort.getShort()); + } + case 3 -> { + NBTInt nbtInt = (NBTInt) nbt; + return NBTTagInt.a(nbtInt.getInt()); + } + case 4 -> { + NBTLong nbtLong = (NBTLong) nbt; + return NBTTagLong.a(nbtLong.getLong()); + } + case 5 -> { + NBTFloat nbtfloat = (NBTFloat) nbt; + return NBTTagFloat.a(nbtfloat.getFloat()); + } + case 6 -> { + NBTDouble nbtDouble = (NBTDouble) nbt; + return NBTTagDouble.a(nbtDouble.getDouble()); + } + case 7 -> { + NBTByteArray nbtByteArray = (NBTByteArray) nbt; + return new NBTTagByteArray(nbtByteArray.getByteArray()); + } + case 8 -> { + NBTString nbtString = (NBTString) nbt; + return NBTTagString.a(nbtString.getString()); + } + case 9 -> { + NBTList nbtTagList = (NBTList) nbt; + List list = new ArrayList<>(); + for (Object base : nbtTagList.getList()) { + list.add(asNMS((INBT) base)); + } + NBTTagList nbtTagList1 = new NBTTagList(); + for (net.minecraft.nbt.NBTBase nbt1 : list) { + nbtTagList1.add(nbt1); + } + return nbtTagList1; + } + case 10 -> { + NBTCompound nbtCompound = (NBTCompound) nbt; + NBTTagCompound nbtTagCompound = new NBTTagCompound(); + for (String key : nbtCompound.keySet()) { + INBT nbt1 = nbtCompound.get(key); + nbtTagCompound.a(key, asNMS(nbt1)); + } + return nbtTagCompound; + } + case 11 -> { + NBTIntArray nbtIntArray = (NBTIntArray) nbt; + return new NBTTagIntArray(nbtIntArray.getIntArray()); + } + case 12 -> { + NBTLongArray nbtLongArray = (NBTLongArray) nbt; + return new NBTTagLongArray(nbtLongArray.getLongArray()); + } + } + return null; + } + + static INBT as(net.minecraft.nbt.NBTBase nbtBase) { + byte type = nbtBase.a(); + switch (type) { + case 1: + NBTTagByte nbtTagByte = (NBTTagByte) nbtBase; + return new NBTByte(nbtTagByte.h()); + case 2: + NBTTagShort nbtTagShort = (NBTTagShort) nbtBase; + return new NBTShort(nbtTagShort.g()); + case 3: + NBTTagInt nbtTagInt = (NBTTagInt) nbtBase; + return new NBTInt(nbtTagInt.f()); + case 4: + NBTTagLong nbtTagLong = (NBTTagLong) nbtBase; + return new NBTLong(nbtTagLong.e()); + case 5: + NBTTagFloat nbtTagFloat = (NBTTagFloat) nbtBase; + return new NBTFloat(nbtTagFloat.j()); + case 6: + NBTTagDouble nbtTagDouble = (NBTTagDouble) nbtBase; + return new NBTDouble(nbtTagDouble.i()); + case 7: + NBTTagByteArray tagByteArray = (NBTTagByteArray) nbtBase; + return new NBTByteArray(tagByteArray.d()); + case 8: + NBTTagString nbtTagString = (NBTTagString) nbtBase; + return new NBTString(nbtTagString.e_()); + case 9: + NBTTagList nbtTagList = (NBTTagList) nbtBase; + List list = new ArrayList<>(); + for (net.minecraft.nbt.NBTBase base : nbtTagList) { + list.add(as(base)); + } + return new NBTList(list); + case 10: + NBTTagCompound nbtTagCompound = (NBTTagCompound) nbtBase; + Map map = new HashMap<>(); + for (String key : nbtTagCompound.d()) { + map.put(key, as(nbtTagCompound.c(key))); + } + return new NBTCompound(map); + case 11: + NBTTagIntArray nbtTagIntArray = (NBTTagIntArray) nbtBase; + return new NBTIntArray(nbtTagIntArray.f()); + case 12: + NBTTagLongArray nbtTagLongArray = (NBTTagLongArray) nbtBase; + return new NBTLongArray(nbtTagLongArray.f()); + } + return null; + } + +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/nbt/NBTByte.java b/src/main/java/com/io/yutian/elementoriginlib/nbt/NBTByte.java new file mode 100644 index 0000000..016898d --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/nbt/NBTByte.java @@ -0,0 +1,81 @@ +package com.io.yutian.elementoriginlib.nbt; + +public class NBTByte extends NBTNumber { + + public static byte TYPE_ID = 1; + + private byte value; + + public NBTByte(byte value) { + this.value = value; + } + + public NBTByte() { + } + + @Override + public Object getValue() { + return this.value; + } + + @Override + public byte getTypeId() { + return TYPE_ID; + } + + @Override + public long getLong() { + return this.value; + } + + @Override + public int getInt() { + return this.value; + } + + @Override + public short getShort() { + return this.value; + } + + @Override + public byte getByte() { + return this.value; + } + + @Override + public double getDouble() { + return this.value; + } + + @Override + public float getFloat() { + return this.value; + } + + public static NBTByte a(boolean var0) { + return var0 ? new NBTByte((byte) 1) : new NBTByte((byte) 0); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + NBTByte nbtByte = (NBTByte) o; + return value == nbtByte.value; + } + + @Override + public int hashCode() { + return value; + } + + @Override + public String toString() { + return "NBTByte{" + + "value=" + value + + '}'; + } + +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/nbt/NBTByteArray.java b/src/main/java/com/io/yutian/elementoriginlib/nbt/NBTByteArray.java new file mode 100644 index 0000000..e8ce673 --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/nbt/NBTByteArray.java @@ -0,0 +1,49 @@ +package com.io.yutian.elementoriginlib.nbt; + +import java.util.Arrays; + +public class NBTByteArray implements INBT { + + public static byte TYPE_ID = 7; + + private byte[] value; + + public NBTByteArray(byte[] value) { + this.value = value; + } + + public byte[] getByteArray() { + return this.value; + } + + @Override + public Object getValue() { + return this.value; + } + + @Override + public byte getTypeId() { + return TYPE_ID; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + NBTByteArray that = (NBTByteArray) o; + return Arrays.equals(value, that.value); + } + + @Override + public int hashCode() { + return Arrays.hashCode(value); + } + + @Override + public String toString() { + return "NBTByteArray{" + + "value=" + Arrays.toString(value) + + '}'; + } +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/nbt/NBTCompound.java b/src/main/java/com/io/yutian/elementoriginlib/nbt/NBTCompound.java new file mode 100644 index 0000000..5214239 --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/nbt/NBTCompound.java @@ -0,0 +1,267 @@ +package com.io.yutian.elementoriginlib.nbt; + +import java.util.*; + +public class NBTCompound implements INBT { + + public static byte TYPE_ID = 10; + + private Map nbtMap = new HashMap<>(); + + public NBTCompound() { + } + + public NBTCompound(Map nbtMap) { + this.nbtMap = nbtMap; + } + + public Set keySet() { + return nbtMap.keySet(); + } + + public Map getMap() { + return nbtMap; + } + + public void remove(String key) { + nbtMap.remove(key); + } + + public boolean hasKey(String key) { + return this.nbtMap.containsKey(key); + } + + public boolean hasKey(String key, byte id) { + return this.nbtMap.containsKey(key) && this.nbtMap.get(key).getTypeId() == id; + } + + public boolean hasKey(String key, int id) { + return this.nbtMap.containsKey(key) && this.nbtMap.get(key).getTypeId() == id; + } + + public NBTList getList(String key) { + INBT list = this.nbtMap.get(key); + if (list == null || list.getTypeId() != 9) { + return null; + } + return (NBTList) list; + } + + public NBTCompound getCompound(String key) { + INBT comound = this.nbtMap.get(key); + if (comound == null || comound.getTypeId() != 10) { + return null; + } + return (NBTCompound) comound; + } + + public INBT get(String key) { + return this.nbtMap.get(key); + } + + public int getInt(String key) { + INBT nbt = this.nbtMap.get(key); + if (nbt == null || nbt.getTypeId() != 3) { + return 0; + } + return ((NBTInt) nbt).getInt(); + } + + public String getString(String key) { + INBT nbt = this.nbtMap.get(key); + if (nbt == null || nbt.getTypeId() != 8) { + return null; + } + return ((NBTString) nbt).getString(); + } + + public double getDouble(String key) { + INBT nbt = this.nbtMap.get(key); + if (nbt == null || nbt.getTypeId() != 6) { + return 0.0; + } + return ((NBTDouble) nbt).getDouble(); + } + + public float getFloat(String key) { + INBT nbt = this.nbtMap.get(key); + if (nbt == null || nbt.getTypeId() != 5) { + return 0.0F; + } + return ((NBTFloat) nbt).getFloat(); + } + + public short getShort(String key) { + INBT nbt = this.nbtMap.get(key); + if (nbt == null || nbt.getTypeId() != 2) { + return 0; + } + return ((NBTShort) nbt).getShort(); + } + + public long getLong(String key) { + INBT nbt = this.nbtMap.get(key); + if (nbt == null || nbt.getTypeId() != 4) { + return 0; + } + return ((NBTLong) nbt).getLong(); + } + + public byte getByte(String key) { + INBT nbt = this.nbtMap.get(key); + if (nbt == null || nbt.getTypeId() != 1) { + return 0; + } + return ((NBTByte) nbt).getByte(); + } + + public NBTByteArray getByteArray(String key) { + INBT nbt = this.nbtMap.get(key); + if (nbt == null || nbt.getTypeId() != 7) { + return null; + } + return (NBTByteArray) nbt; + } + + public NBTIntArray getIntArray(String key) { + INBT nbt = this.nbtMap.get(key); + if (nbt == null || nbt.getTypeId() != 11) { + return null; + } + return (NBTIntArray) nbt; + } + + public NBTLongArray getLongArray(String key) { + INBT nbt = this.nbtMap.get(key); + if (nbt == null || nbt.getTypeId() != 12) { + return null; + } + return (NBTLongArray) nbt; + } + + public boolean getBoolean(String key) { + INBT nbt = this.nbtMap.get(key); + if (nbt == null || nbt.getTypeId() != 1) { + return false; + } + return ((NBTByte) nbt).getByte() != 0; + } + + public UUID getUUID(String key) { + INBT nbt = this.nbtMap.get(key); + if (nbt == null || nbt.getTypeId() != 11) { + return null; + } + return a(((NBTIntArray) nbt).getIntArray()); + } + + public void put(String key, INBT nbt) { + this.nbtMap.put(key, nbt); + } + + public void putString(String key, String value) { + put(key, new NBTString(value)); + } + + public void putInt(String key, int value) { + put(key, new NBTInt(value)); + } + + public void putShort(String key, short value) { + put(key, new NBTShort(value)); + } + + public void putDouble(String key, double value) { + put(key, new NBTDouble(value)); + } + + public void putLong(String key, long value) { + put(key, new NBTLong(value)); + } + + public void putFloat(String key, float value) { + put(key, new NBTFloat(value)); + } + + public void putByte(String key, byte value) { + put(key, new NBTByte(value)); + } + + public void putBoolean(String key, boolean value) { + put(key, NBTByte.a(value)); + } + + public void putUUID(String key, UUID uuid) { + put(key, new NBTIntArray(a(uuid))); + } + + private static UUID a(int[] var0) { + return new UUID((long)var0[0] << 32 | (long)var0[1] & 4294967295L, (long)var0[2] << 32 | (long)var0[3] & 4294967295L); + } + + private static int[] a(UUID var0) { + long var1 = var0.getMostSignificantBits(); + long var3 = var0.getLeastSignificantBits(); + return a(var1, var3); + } + + private static int[] a(long var0, long var2) { + return new int[]{(int)(var0 >> 32), (int)var0, (int)(var2 >> 32), (int)var2}; + } + + @Override + public Object getValue() { + return nbtMap; + } + + @Override + public byte getTypeId() { + return TYPE_ID; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + NBTCompound that = (NBTCompound) o; + Map nbtMap1 = this.nbtMap; + Map nbtMap2 = that.nbtMap; + if (nbtMap1.size() != nbtMap2.size()) { + return false; + } + for (Map.Entry entry : nbtMap1.entrySet()) { + String key = entry.getKey(); + INBT value = entry.getValue(); + if (!nbtMap2.containsKey(key)) { + return false; + } + INBT nbt2 = nbtMap2.get(key); + Object object1 = value.getValue(); + Object object2 = nbt2.getValue(); + if (value == null && nbt2 != null) { + return false; + } else if (object1 instanceof Number && object2 instanceof Number) { + if (Double.compare(((Number) object1).doubleValue(), ((Number) object2).doubleValue()) != 0) { + return false; + } + } else if (!object1.equals(object2)) { + return false; + } + } + return true; + } + + @Override + public int hashCode() { + return Objects.hashCode(nbtMap); + } + + @Override + public String toString() { + return "NBTCompound{" + + "nbtMap=" + nbtMap + + '}'; + } + +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/nbt/NBTDouble.java b/src/main/java/com/io/yutian/elementoriginlib/nbt/NBTDouble.java new file mode 100644 index 0000000..8315ee3 --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/nbt/NBTDouble.java @@ -0,0 +1,76 @@ +package com.io.yutian.elementoriginlib.nbt; + +public class NBTDouble extends NBTNumber { + + public static byte TYPE_ID = 6; + + private double value; + + public NBTDouble(double value) { + this.value = value; + } + + public NBTDouble() { + } + + @Override + public Object getValue() { + return value; + } + + @Override + public byte getTypeId() { + return TYPE_ID; + } + + @Override + public long getLong() { + return (long) this.value; + } + + @Override + public int getInt() { + return floor(this.value); + } + + @Override + public short getShort() { + return (short)(floor(this.value) & 0xFFFF); + } + + @Override + public byte getByte() { + return (byte)(floor(this.value) & 0xFF); + } + + @Override + public double getDouble() { + return this.value; + } + + @Override + public float getFloat() { + return (float) this.value; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + NBTDouble nbtDouble = (NBTDouble) o; + return Double.compare(value, nbtDouble.value) == 0; + } + + @Override + public int hashCode() { + return Double.hashCode(value); + } + + @Override + public String toString() { + return "NBTDouble{" + + "value=" + value + + '}'; + } +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/nbt/NBTFloat.java b/src/main/java/com/io/yutian/elementoriginlib/nbt/NBTFloat.java new file mode 100644 index 0000000..2b63131 --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/nbt/NBTFloat.java @@ -0,0 +1,73 @@ +package com.io.yutian.elementoriginlib.nbt; + +public class NBTFloat extends NBTNumber { + + public static byte TYPE_ID = 5; + + private float value; + + public NBTFloat(float value) { + this.value = value; + } + + @Override + public Object getValue() { + return this.value; + } + + @Override + public byte getTypeId() { + return TYPE_ID; + } + + @Override + public long getLong() { + return (long) this.value; + } + + @Override + public int getInt() { + return floor(this.value); + } + + @Override + public short getShort() { + return (short)(floor(this.value) & 0xFFFF); + } + + @Override + public byte getByte() { + return (byte)(floor(this.value) & 0xFF); + } + + @Override + public double getDouble() { + return this.value; + } + + @Override + public float getFloat() { + return this.value; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + NBTFloat nbtFloat = (NBTFloat) o; + return Float.compare(value, nbtFloat.value) == 0; + } + + @Override + public int hashCode() { + return Float.hashCode(value); + } + + @Override + public String toString() { + return "NBTFloat{" + + "value=" + value + + '}'; + } +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/nbt/NBTHelper.java b/src/main/java/com/io/yutian/elementoriginlib/nbt/NBTHelper.java new file mode 100644 index 0000000..aeaf9fc --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/nbt/NBTHelper.java @@ -0,0 +1,39 @@ +package com.io.yutian.elementoriginlib.nbt; + +import org.jetbrains.annotations.Nullable; + +public class NBTHelper { + + @Nullable + public static INBT convert(Object object) { + if (object instanceof Byte) { + return new NBTByte((byte) object); + } else if (object instanceof Short) { + return new NBTShort((short) object); + } else if (object instanceof Integer) { + return new NBTInt((int) object); + } else if (object instanceof Long) { + return new NBTLong((long) object); + } else if (object instanceof Float) { + return new NBTFloat((float) object); + } else if (object instanceof Double) { + return new NBTDouble((double) object); + } else if (object instanceof byte[]) { + return new NBTByteArray((byte[]) object); + } else if (object instanceof String) { + return new NBTString((String) object); + } else if (object instanceof int[]) { + return new NBTIntArray((int[]) object); + } else if (object instanceof long[]) { + return new NBTLongArray((long[]) object); + } else if (object instanceof Boolean) { + return new NBTByte((byte) ((boolean)object ? 1 : 0)); + } + return null; + } + + public static Object convertNMSNBT(INBT nbt) { + return INBT.asNMS(nbt); + } + +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/nbt/NBTInt.java b/src/main/java/com/io/yutian/elementoriginlib/nbt/NBTInt.java new file mode 100644 index 0000000..0e1ff96 --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/nbt/NBTInt.java @@ -0,0 +1,73 @@ +package com.io.yutian.elementoriginlib.nbt; + +public class NBTInt extends NBTNumber { + + public static byte TYPE_ID = 3; + + private int value; + + public NBTInt(int value) { + this.value = value; + } + + @Override + public Object getValue() { + return this.value; + } + + @Override + public byte getTypeId() { + return TYPE_ID; + } + + @Override + public long getLong() { + return this.value; + } + + @Override + public int getInt() { + return this.value; + } + + @Override + public short getShort() { + return (short)(this.value & 0xFFFF); + } + + @Override + public byte getByte() { + return (byte)(this.value & 0xFF); + } + + @Override + public double getDouble() { + return this.value; + } + + @Override + public float getFloat() { + return this.value; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + NBTInt nbtInt = (NBTInt) o; + return value == nbtInt.value; + } + + @Override + public int hashCode() { + return value; + } + + @Override + public String toString() { + return "NBTInt{" + + "value=" + value + + '}'; + } +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/nbt/NBTIntArray.java b/src/main/java/com/io/yutian/elementoriginlib/nbt/NBTIntArray.java new file mode 100644 index 0000000..ed8130a --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/nbt/NBTIntArray.java @@ -0,0 +1,42 @@ +package com.io.yutian.elementoriginlib.nbt; + +import java.util.Arrays; + +public class NBTIntArray implements INBT { + + public static byte TYPE_ID = 11; + + private int[] value; + + public NBTIntArray(int[] value) { + this.value = value; + } + + public int[] getIntArray() { + return this.value; + } + + @Override + public Object getValue() { + return this.value; + } + + @Override + public byte getTypeId() { + return TYPE_ID; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + NBTIntArray that = (NBTIntArray) o; + return Arrays.equals(value, that.value); + } + + @Override + public int hashCode() { + return Arrays.hashCode(value); + } +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/nbt/NBTItem.java b/src/main/java/com/io/yutian/elementoriginlib/nbt/NBTItem.java new file mode 100644 index 0000000..f1672da --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/nbt/NBTItem.java @@ -0,0 +1,80 @@ +package com.io.yutian.elementoriginlib.nbt; + +import net.minecraft.nbt.NBTTagCompound; +import org.bukkit.craftbukkit.v1_18_R2.inventory.CraftItemStack; +import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.NotNull; + +import javax.annotation.Nullable; +import java.util.function.Consumer; + +public class NBTItem { + + private ItemStack originalItemStack; + private net.minecraft.world.item.ItemStack nmsItemStack; + + protected ItemStack resultItemStack; + + public NBTItem(ItemStack itemStack) { + this.originalItemStack = itemStack; + this.nmsItemStack = CraftItemStack.asNMSCopy(itemStack); + } + + @NotNull + public NBTCompound getTag() { + NBTTagCompound tag = nmsItemStack.u(); + if (tag == null) { + tag = new NBTTagCompound(); + } + return (NBTCompound) INBT.as(tag); + } + + public boolean has(String key) { + NBTCompound nbtCompound = getTag(); + return nbtCompound.hasKey(key); + } + + public boolean has(String key, int type) { + NBTCompound nbtCompound = getTag(); + return nbtCompound.hasKey(key, type); + } + + @Nullable + public INBT get(String key) { + NBTCompound nbtCompound = getTag(); + return nbtCompound.hasKey(key) ? nbtCompound.get(key) : null; + } + + public void editTag(Consumer consumer) { + NBTCompound tag = getTag(); + consumer.accept(tag); + setTag(tag); + } + + public void setTag(NBTCompound nbtCompound) { + nmsItemStack.c((NBTTagCompound) INBT.asNMS(nbtCompound)); + resultItemStack = CraftItemStack.asBukkitCopy(nmsItemStack); + } + + public ItemStack getOriginalItemStack() { + return originalItemStack; + } + + public ItemStack getItemStack() { + return resultItemStack; + } + + public net.minecraft.world.item.ItemStack getNMSItemStack() { + return nmsItemStack; + } + + public static NBTItem build(ItemStack itemStack) { + return new NBTItem(itemStack); + } + + public static NBTItem build(NBTCompound nbtCompound) { + net.minecraft.world.item.ItemStack nmsItemStack = net.minecraft.world.item.ItemStack.a((NBTTagCompound) INBT.asNMS(nbtCompound)); + return new NBTItem(CraftItemStack.asBukkitCopy(nmsItemStack)); + } + +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/nbt/NBTList.java b/src/main/java/com/io/yutian/elementoriginlib/nbt/NBTList.java new file mode 100644 index 0000000..c3a36cf --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/nbt/NBTList.java @@ -0,0 +1,94 @@ +package com.io.yutian.elementoriginlib.nbt; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +public class NBTList implements INBT { + + public static byte TYPE_ID = 9; + + private List value = new ArrayList<>(); + private byte type = 0; + + public NBTList(List value) { + this.value = value; + } + + public NBTList() { + } + + public List getList() { + return value; + } + + public int size() { + return value.size(); + } + + public T get(int index) { + if (index > value.size()) { + return null; + } + return value.get(index); + } + + public void add(T nbtBase) { + if (nbtBase.getTypeId() == 0) { + return; + } + if (this.type == 0) { + this.type = nbtBase.getTypeId(); + } else if (this.type != nbtBase.getTypeId()) { + return; + } + this.value.add(nbtBase); + } + + public void remove(int index) { + this.value.remove(index); + } + + public boolean isEmpty() { + return value.isEmpty(); + } + + @Override + public Object getValue() { + return this.value; + } + + @Override + public byte getTypeId() { + return TYPE_ID; + } + + public byte getType() { + return type; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + NBTList nbtList = (NBTList) o; + return type == nbtList.type && Objects.equals(value, nbtList.value); + } + + @Override + public int hashCode() { + int result = Objects.hashCode(value); + result = 31 * result + type; + return result; + } + + @Override + public String toString() { + return "NBTList{" + + "value=" + value + + ", type=" + type + + '}'; + } + +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/nbt/NBTLong.java b/src/main/java/com/io/yutian/elementoriginlib/nbt/NBTLong.java new file mode 100644 index 0000000..e794dff --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/nbt/NBTLong.java @@ -0,0 +1,76 @@ +package com.io.yutian.elementoriginlib.nbt; + +public class NBTLong extends NBTNumber { + + public static byte TYPE_ID = 4; + + private long value; + + public NBTLong(long value) { + this.value = value; + } + + public NBTLong() { + } + + @Override + public Object getValue() { + return this.value; + } + + @Override + public byte getTypeId() { + return TYPE_ID; + } + + @Override + public long getLong() { + return this.value; + } + + @Override + public int getInt() { + return (int)(this.value & 0x7fffffffffffffffL); + } + + @Override + public short getShort() { + return (short)(this.value & 0xFFFF); + } + + @Override + public byte getByte() { + return (byte)(this.value & 0xFF); + } + + @Override + public double getDouble() { + return this.value; + } + + @Override + public float getFloat() { + return (float) this.value; + } + + @Override + public String toString() { + return "NBTLong{" + + "value=" + value + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + NBTLong nbtLong = (NBTLong) o; + return value == nbtLong.value; + } + + @Override + public int hashCode() { + return Long.hashCode(value); + } +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/nbt/NBTLongArray.java b/src/main/java/com/io/yutian/elementoriginlib/nbt/NBTLongArray.java new file mode 100644 index 0000000..2e597f6 --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/nbt/NBTLongArray.java @@ -0,0 +1,42 @@ +package com.io.yutian.elementoriginlib.nbt; + +import java.util.Arrays; + +public class NBTLongArray implements INBT { + + public static byte TYPE_ID = 12; + + private long[] value; + + public NBTLongArray(long[] value) { + this.value = value; + } + + public long[] getLongArray() { + return this.value; + } + + @Override + public Object getValue() { + return this.value; + } + + @Override + public byte getTypeId() { + return TYPE_ID; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + NBTLongArray that = (NBTLongArray) o; + return Arrays.equals(value, that.value); + } + + @Override + public int hashCode() { + return Arrays.hashCode(value); + } +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/nbt/NBTNumber.java b/src/main/java/com/io/yutian/elementoriginlib/nbt/NBTNumber.java new file mode 100644 index 0000000..2dcf822 --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/nbt/NBTNumber.java @@ -0,0 +1,22 @@ +package com.io.yutian.elementoriginlib.nbt; + +public abstract class NBTNumber implements INBT { + + public abstract long getLong(); + + public abstract int getInt(); + + public abstract short getShort(); + + public abstract byte getByte(); + + public abstract double getDouble(); + + public abstract float getFloat(); + + public static int floor(double paramDouble) { + int i = (int)paramDouble; + return paramDouble < i ? i - 1 : i; + } + +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/nbt/NBTShort.java b/src/main/java/com/io/yutian/elementoriginlib/nbt/NBTShort.java new file mode 100644 index 0000000..0fbca3e --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/nbt/NBTShort.java @@ -0,0 +1,77 @@ +package com.io.yutian.elementoriginlib.nbt; + +public class NBTShort extends NBTNumber { + + public static byte TYPE_ID = 2; + + private short value; + + public NBTShort(short value) { + this.value = value; + } + + public NBTShort() { + } + + @Override + public Object getValue() { + return this.value; + } + + @Override + public byte getTypeId() { + return TYPE_ID; + } + + @Override + public long getLong() { + return this.value; + } + + @Override + public int getInt() { + return this.value; + } + + @Override + public short getShort() { + return this.value; + } + + @Override + public byte getByte() { + return (byte)(this.value & 0xFF); + } + + @Override + public double getDouble() { + return this.value; + } + + @Override + public float getFloat() { + return this.value; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + NBTShort nbtShort = (NBTShort) o; + return value == nbtShort.value; + } + + @Override + public int hashCode() { + return value; + } + + @Override + public String toString() { + return "NBTShort{" + + "value=" + value + + '}'; + } + +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/nbt/NBTString.java b/src/main/java/com/io/yutian/elementoriginlib/nbt/NBTString.java new file mode 100644 index 0000000..60d4d24 --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/nbt/NBTString.java @@ -0,0 +1,52 @@ +package com.io.yutian.elementoriginlib.nbt; + +import java.util.Objects; + +public class NBTString implements INBT { + + public static byte TYPE_ID = 8; + + private String value; + + public NBTString(String value) { + this.value = value; + } + + public NBTString() { + } + + public String getString() { + return this.value; + } + + @Override + public Object getValue() { + return this.value; + } + + @Override + public byte getTypeId() { + return TYPE_ID; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + NBTString nbtString = (NBTString) o; + return Objects.equals(value, nbtString.value); + } + + @Override + public int hashCode() { + return Objects.hashCode(value); + } + + @Override + public String toString() { + return "NBTString{" + + "value='" + value + '\'' + + '}'; + } +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/point/DirectionPoint.java b/src/main/java/com/io/yutian/elementoriginlib/point/DirectionPoint.java new file mode 100644 index 0000000..4181ef7 --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/point/DirectionPoint.java @@ -0,0 +1,84 @@ +package com.io.yutian.elementoriginlib.point; + +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.configuration.ConfigurationSection; + +import java.util.Objects; + +public class DirectionPoint extends Point { + + private float yaw; + private float pitch; + + public DirectionPoint(int x, int y, int z, float yaw, float pitch) { + super(x, y, z); + this.yaw = yaw; + this.pitch = pitch; + } + + public DirectionPoint(double x, double y, double z) { + this(x, y, z, 0f, 0f); + } + + public DirectionPoint(double x, double y, double z, float yaw, float pitch) { + super(x, y, z); + this.yaw = yaw; + this.pitch = pitch; + } + + public float getPitch() { + return pitch; + } + + public float getYaw() { + return yaw; + } + + public void setYaw(float yaw) { + this.yaw = yaw; + } + + public void setPitch(float pitch) { + this.pitch = pitch; + } + + @Override + public Location toLocation(World world) { + return new Location(world, getX(), getY(), getZ(), yaw, pitch); + } + + public static DirectionPoint of(Location location) { + return new DirectionPoint(location.getX(), location.getY(), location.getZ(), location.getYaw(), location.getPitch()); + } + + @Override + public String toString() { + return "DirectionPoint{" + + "x=" + getX() + + ", y=" + getY() + + ", z=" + getZ() + + ", yaw=" + yaw + + ", pitch=" + pitch + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + if (!super.equals(o)) return false; + DirectionPoint that = (DirectionPoint) o; + return super.equals(o) && Float.compare(that.yaw, yaw) == 0 && Float.compare(that.pitch, pitch) == 0; + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), yaw, pitch); + } + + public static DirectionPoint deserialize(ConfigurationSection section) { + return new DirectionPoint(section.getDouble("x"), section.getDouble("y"), section.getDouble("z"), (float) section.getDouble("yaw"), (float) section.getDouble("pitch")); + } + +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/point/Point.java b/src/main/java/com/io/yutian/elementoriginlib/point/Point.java new file mode 100644 index 0000000..1141f21 --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/point/Point.java @@ -0,0 +1,101 @@ +package com.io.yutian.elementoriginlib.point; + +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.configuration.ConfigurationSection; + +import java.util.Objects; + +public class Point { + + private double x; + private double y; + private double z; + + public Point(int x, int y, int z) { + this.x = x; + this.y = y; + this.z = z; + } + + public Point(double x, double y, double z) { + this.x = x; + this.y = y; + this.z = z; + } + + public int getBlockX() { + return (int) Math.floor(x); + } + + public int getBlockY() { + return (int) Math.floor(y); + } + + public int getBlockZ() { + return (int) Math.floor(z); + } + + public double getX() { + return x; + } + + public double getY() { + return y; + } + + public double getZ() { + return z; + } + + public void setX(double x) { + this.x = x; + } + + public void setY(double y) { + this.y = y; + } + + public void setZ(double z) { + this.z = z; + } + + public Point clone() { + return new Point(x, y, z); + } + + public Location toLocation(World world) { + return new Location(world, x, y, z, 0, 0); + } + + public static Point of(Location location) { + return new Point(location.getX(), location.getY(), location.getZ()); + } + + public static Point deserialize(ConfigurationSection section) { + return new Point(section.getDouble("x"), section.getDouble("y"), section.getDouble("z")); + } + + @Override + public String toString() { + return "Point{" + + "x=" + x + + ", y=" + y + + ", z=" + z + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Point point = (Point) o; + return Double.compare(point.x, x) == 0 && Double.compare(point.y, y) == 0 && Double.compare(point.z, z) == 0; + } + + @Override + public int hashCode() { + return Objects.hash(x, y, z); + } + +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/point/Region.java b/src/main/java/com/io/yutian/elementoriginlib/point/Region.java new file mode 100644 index 0000000..d872f5c --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/point/Region.java @@ -0,0 +1,51 @@ +package com.io.yutian.elementoriginlib.point; + +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.configuration.ConfigurationSection; + +public class Region { + + private World world; + private Point min; + private Point max; + + public Region(World world, Point min, Point max) { + this.world = world; + this.min = min; + this.max = max; + } + + public World getWorld() { + return world; + } + + public Point getMin() { + return min; + } + + public Point getMax() { + return max; + } + + public static Region deserialize(ConfigurationSection section) { + World world1 = Bukkit.getWorld(section.getString("world")); + Point point1 = Point.deserialize(section.getConfigurationSection("min")); + Point point2 = Point.deserialize(section.getConfigurationSection("max")); + return new Region(world1, point1, point2); + } + + public boolean isInRegion(Location location) { + if (!location.getWorld().getName().equalsIgnoreCase(world.getName())) { + return false; + } + return (location.getBlockX() >= this.min.getX() + && location.getBlockX() <= this.max.getX() + && location.getBlockY() >= this.min.getY() + && location.getBlockY() <= this.max.getY() + && location.getBlockZ() >= this.min.getZ() + && location.getBlockZ() <= this.max.getZ()); + } + +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/redis/IJedisGetter.java b/src/main/java/com/io/yutian/elementoriginlib/redis/IJedisGetter.java new file mode 100644 index 0000000..015f9e1 --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/redis/IJedisGetter.java @@ -0,0 +1,9 @@ +package com.io.yutian.elementoriginlib.redis; + +import redis.clients.jedis.Jedis; + +public interface IJedisGetter { + + Jedis getRedis(); + +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/redis/RedisCacheSyncTimer.java b/src/main/java/com/io/yutian/elementoriginlib/redis/RedisCacheSyncTimer.java new file mode 100644 index 0000000..0a08af2 --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/redis/RedisCacheSyncTimer.java @@ -0,0 +1,108 @@ +package com.io.yutian.elementoriginlib.redis; + +import com.io.yutian.elementoriginlib.ElementOriginLib; +import com.io.yutian.elementoriginlib.util.Pair; +import org.bukkit.plugin.Plugin; +import org.bukkit.scheduler.BukkitRunnable; +import org.bukkit.scheduler.BukkitTask; +import redis.clients.jedis.Jedis; + +import java.util.HashMap; +import java.util.Map; + +public class RedisCacheSyncTimer { + + private static final String LOCK_KEY = "sync_lock"; + private static final int LOCK_EXPIRE_SECONDS = 290; + private final Map pluginInfos = new HashMap<>(); + + private BukkitTask task; + + public RedisCacheSyncTimer() { + task = new BukkitRunnable() { + @Override + public void run() { + if (pluginInfos.isEmpty()) { + return; + } + for (Map.Entry entry : pluginInfos.entrySet()) { + Plugin plugin = entry.getKey(); + if (!plugin.isEnabled()) { + continue; + } + PluginInfo pluginInfo = entry.getValue(); + String lockKey = LOCK_KEY + "_" + plugin.getName(); + IJedisGetter jedisGetter = pluginInfo.getJedisGetter(); + try (Jedis jedis = jedisGetter.getRedis()) { + long lockResult = jedis.setnx(lockKey, "locked"); + if (lockResult != 1) { + continue; + } + jedis.expire(lockKey, LOCK_EXPIRE_SECONDS); + for (Map.Entry, RedisCacheSynchronizer> entry1 : entry.getValue().getSynchronizers().entrySet()) { + Pair key = entry1.getKey(); + RedisCacheSynchronizer synchronizer = entry1.getValue(); + String k1 = key.first(); + String k2 = key.second(); + for (String k : jedis.keys(k1+":*")) { + String data = null; + if (k2 == null) { + data = jedis.get(k); + } else { + data = jedis.hget(k, k2); + } + if (data != null) { + String finalData = data; + synchronizer.sync(k, finalData); + if (k2 == null) { + jedis.del(k); + } else { + jedis.hdel(k, k2); + } + } + } + } + } + } + } + }.runTaskTimerAsynchronously(ElementOriginLib.inst(), 1200L, 5 * 60 * 20L); + } + + public void registerSynchronizer(Plugin plugin, IJedisGetter jedisGetter, Pair key, RedisCacheSynchronizer synchronizer) { + PluginInfo pluginInfo = pluginInfos.computeIfAbsent(plugin, k -> new PluginInfo(jedisGetter)); + pluginInfo.addSynchronizer(key, synchronizer); + pluginInfos.put(plugin, pluginInfo); + } + + class PluginInfo { + + private IJedisGetter jedisGetter; + private Map, RedisCacheSynchronizer> synchronizers; + + public PluginInfo(IJedisGetter jedisGetter) { + this.jedisGetter = jedisGetter; + this.synchronizers = new HashMap<>(); + } + + public void addSynchronizer(Pair key, RedisCacheSynchronizer synchronizer) { + if (key == null || synchronizer == null) { + return; + } + if (synchronizers.containsKey(key)) { + throw new IllegalArgumentException("Key already registered: " + key); + } + synchronizers.put(key, synchronizer); + } + + public IJedisGetter getJedisGetter() { + return jedisGetter; + } + + public Map, RedisCacheSynchronizer> getSynchronizers() { + return synchronizers; + } + + } + +} + diff --git a/src/main/java/com/io/yutian/elementoriginlib/redis/RedisCacheSynchronizer.java b/src/main/java/com/io/yutian/elementoriginlib/redis/RedisCacheSynchronizer.java new file mode 100644 index 0000000..21b91e4 --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/redis/RedisCacheSynchronizer.java @@ -0,0 +1,8 @@ +package com.io.yutian.elementoriginlib.redis; + +@FunctionalInterface +public interface RedisCacheSynchronizer { + + void sync(String key, String data); + +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/redis/RedisIO.java b/src/main/java/com/io/yutian/elementoriginlib/redis/RedisIO.java new file mode 100644 index 0000000..20e5704 --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/redis/RedisIO.java @@ -0,0 +1,87 @@ +package com.io.yutian.elementoriginlib.redis; + +import com.io.yutian.elementoriginlib.util.FileUtil; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.plugin.Plugin; +import redis.clients.jedis.Jedis; +import redis.clients.jedis.JedisPool; +import redis.clients.jedis.JedisPoolConfig; + +import java.io.File; +import java.time.Duration; +import java.util.Set; + +public class RedisIO implements IJedisGetter { + + private JedisPool jedisPool; + + public void init(Plugin plugin) { + File file = FileUtil.getFile(plugin, "", "redis.yml"); + if (!file.exists()) { + plugin.saveResource("redis.yml", false); + } + FileConfiguration configuration = YamlConfiguration.loadConfiguration(file); + String redisServer = configuration.getString("server", "localhost"); + int redisPort = configuration.getInt("port", 6379); + String redisPassword = configuration.getString("password"); + if (redisPassword != null && (redisPassword.isEmpty() || redisPassword.equals("none"))) { + redisPassword = null; + } + try { + String finalRedisPassword = redisPassword; + JedisPoolConfig config = new JedisPoolConfig(); + config.setMaxTotal(1024); + config.setMaxWait(Duration.ofMillis(10000)); + jedisPool = new JedisPool(config, redisServer, redisPort, 0, finalRedisPassword); + } catch (Exception e) { + e.printStackTrace(); + } + } + + public void close() { + if (jedisPool != null && !jedisPool.isClosed()) { + jedisPool.close(); + jedisPool.destroy(); + } + } + + public JedisPool getJedisPool() { + return jedisPool; + } + + public Set getKeys(String arg) { + try (Jedis resource = jedisPool.getResource()) { + return resource.keys(arg); + } + } + + public void remove(String key) { + try (Jedis resource = jedisPool.getResource()) { + resource.del(key); + } + } + + public void remove(String key, String field) { + try (Jedis resource = jedisPool.getResource()) { + resource.hdel(key, field); + } + } + + public boolean has(String key) { + try (Jedis resource = jedisPool.getResource()) { + return resource.exists(key); + } + } + + public boolean has(String key, String field) { + try (Jedis resource = jedisPool.getResource()) { + return resource.hexists(key, field); + } + } + + @Override + public Jedis getRedis() { + return jedisPool.getResource(); + } +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/serialize/ISerializable.java b/src/main/java/com/io/yutian/elementoriginlib/serialize/ISerializable.java new file mode 100644 index 0000000..8147e93 --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/serialize/ISerializable.java @@ -0,0 +1,11 @@ +package com.io.yutian.elementoriginlib.serialize; + +import org.json.JSONObject; + +public interface ISerializable { + + JSONObject serialize(); + + T deserialize(JSONObject jsonObject); + +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/serialize/SerializeHelper.java b/src/main/java/com/io/yutian/elementoriginlib/serialize/SerializeHelper.java new file mode 100644 index 0000000..38236dc --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/serialize/SerializeHelper.java @@ -0,0 +1,189 @@ +package com.io.yutian.elementoriginlib.serialize; + +import com.io.yutian.elementoriginlib.exception.SerializeException; +import com.io.yutian.elementoriginlib.serialize.serializers.ItemStackSerializer; +import com.io.yutian.elementoriginlib.serialize.serializers.UUIDSerializer; +import com.io.yutian.elementoriginlib.util.ReflectionUtil; +import org.bukkit.craftbukkit.v1_18_R2.inventory.CraftItemStack; +import org.bukkit.inventory.ItemStack; +import org.json.JSONArray; +import org.json.JSONObject; +import sun.misc.Unsafe; + +import java.lang.reflect.Array; +import java.lang.reflect.Field; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.*; + +public class SerializeHelper { + + private static Map, Serializer> serializers = new HashMap<>(); + + public static void registerSerializer(Class clazz, Serializer serializer) { + serializers.put(clazz, serializer); + } + + public static JSONObject serialize(T obj) throws SerializeException { + try { + JSONObject jsonObject = new JSONObject(); + Class clazz = obj.getClass(); + for (Field field : clazz.getDeclaredFields()) { + field.setAccessible(true); + if (field.isAnnotationPresent(SerializeIgnore.class)) { + continue; + } + Object value = field.get(obj); + if (value == null) { + continue; + } + Object serializeValue = serializeValue(value); + jsonObject.put(field.getName(), serializeValue); + } + return jsonObject; + } catch (Exception e) { + e.printStackTrace(); + throw new SerializeException(obj.getClass(), e); + } + } + + public static T deserialize(Class clazz, JSONObject jsonObject) throws SerializeException { + try { + Unsafe unsafe = ReflectionUtil.getUnsafe(); + T instance = (T) unsafe.allocateInstance(clazz); + for (Field field : clazz.getDeclaredFields()) { + field.setAccessible(true); + if (field.isAnnotationPresent(SerializeIgnore.class)) { + continue; + } + String name = field.getName(); + if (name.equalsIgnoreCase("this$0")) { + continue; + } + if (!jsonObject.has(name)) { + continue; + } + Object value = jsonObject.get(name); + if (value == null) { + continue; + } + Object deserializeValue = deserializeValue(field, field.getType(), value); + ReflectionUtil.setFieldUsingUnsafe(field, instance, deserializeValue); + } + return instance; + } catch (Exception e) { + e.printStackTrace(); + throw new SerializeException(clazz, e); + } + } + + private static Object deserializeValue(Field field, Class clazz0, Object value) throws SerializeException { + Class clazz = clazz0; + if (WRAPPER_TO_PRIMITIVE.containsKey(clazz)) { + clazz = WRAPPER_TO_PRIMITIVE.get(clazz); + } + if (serializers.containsKey(clazz)) { + Serializer serializer = serializers.get(clazz); + return serializer.deserialize(value); + } + if (ISerializable.class.isAssignableFrom(clazz)) { + ISerializable iSerializable = (ISerializable) value; + JSONObject jsonObject = (JSONObject) value; + return iSerializable.deserialize(jsonObject); + } else if (clazz.isPrimitive() || clazz.equals(String.class)) { + return value; + } else if (clazz.isEnum()) { + return Enum.valueOf((Class) clazz, (String) value); + } else if (clazz.isArray()) { + JSONArray jsonArray = (JSONArray) value; + int length = jsonArray.length(); + Object object = Array.newInstance(clazz.getComponentType(), length); + for (int i = 0; i < length; i++) { + Array.set(object, i, deserializeValue(field, clazz.getComponentType(), jsonArray.get(i))); + } + return object; + } else if (List.class.isAssignableFrom(clazz)) { + List list = new ArrayList(); + JSONArray jsonArray = (JSONArray) value; + Type genericType = field.getGenericType(); + Class elementType = (Class) ((ParameterizedType) genericType).getActualTypeArguments()[0]; + for (int i = 0; i < jsonArray.length(); i++) { + list.add(deserializeValue(field, elementType, jsonArray.get(i))); + } + return list; + } else if (Map.class.isAssignableFrom(clazz)) { + Map map = new HashMap(); + Type genericType = field.getGenericType(); + Class valueType = (Class) ((ParameterizedType) genericType).getActualTypeArguments()[1]; + JSONObject jsonObject = (JSONObject) value; + for (String key : jsonObject.keySet()) { + map.put(key, deserializeValue(field, valueType, jsonObject.get(key))); + } + return map; + } else { + return deserialize(clazz, (JSONObject) value); + } + } + + private static Object serializeValue(Object value) throws SerializeException { + Class clazz = value.getClass(); + if (WRAPPER_TO_PRIMITIVE.containsKey(clazz)) { + clazz = WRAPPER_TO_PRIMITIVE.get(clazz); + } + if (serializers.containsKey(clazz)) { + Serializer serializer = serializers.get(clazz); + return serializer.serialize(value); + } + if (ISerializable.class.isAssignableFrom(clazz)) { + ISerializable iSerializable = (ISerializable) value; + return iSerializable.serialize(); + } else if (clazz.isPrimitive() || clazz.equals(String.class)) { + return value; + } else if (clazz.isEnum()) { + return ((Enum) value).name(); + } else if (clazz.isArray()) { + Class elementType = clazz.getComponentType(); + JSONArray jsonArray = new JSONArray(); + int length = Array.getLength(value); + for (int i = 0; i < length; i++) { + Object element = Array.get(value, i); + jsonArray.put(serializeValue(element)); + } + return jsonArray; + } else if (List.class.isAssignableFrom(clazz)) { + List list = (List) value; + JSONArray jsonArray = new JSONArray(); + for (Object element : list) { + jsonArray.put(serializeValue(element)); + } + return jsonArray; + } else if (Map.class.isAssignableFrom(clazz)) { + Map map = (Map) value; + JSONObject jsonObject = new JSONObject(); + for (Object key : map.keySet()) { + jsonObject.put(key.toString(), serializeValue(map.get(key))); + } + return jsonObject; + } else { + return serialize(value); + } + } + + public static final Map, Class> WRAPPER_TO_PRIMITIVE = new HashMap<>(); + + static { + WRAPPER_TO_PRIMITIVE.put(Boolean.class, boolean.class); + WRAPPER_TO_PRIMITIVE.put(Byte.class, byte.class); + WRAPPER_TO_PRIMITIVE.put(Short.class, short.class); + WRAPPER_TO_PRIMITIVE.put(Character.class, char.class); + WRAPPER_TO_PRIMITIVE.put(Integer.class, int.class); + WRAPPER_TO_PRIMITIVE.put(Long.class, long.class); + WRAPPER_TO_PRIMITIVE.put(Float.class, float.class); + WRAPPER_TO_PRIMITIVE.put(Double.class, double.class); + WRAPPER_TO_PRIMITIVE.put(CraftItemStack.class, ItemStack.class); + + registerSerializer(UUID.class, new UUIDSerializer()); + registerSerializer(ItemStack.class, new ItemStackSerializer()); + } + +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/serialize/SerializeIgnore.java b/src/main/java/com/io/yutian/elementoriginlib/serialize/SerializeIgnore.java new file mode 100644 index 0000000..d258377 --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/serialize/SerializeIgnore.java @@ -0,0 +1,11 @@ +package com.io.yutian.elementoriginlib.serialize; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +public @interface SerializeIgnore { +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/serialize/Serializer.java b/src/main/java/com/io/yutian/elementoriginlib/serialize/Serializer.java new file mode 100644 index 0000000..e46c763 --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/serialize/Serializer.java @@ -0,0 +1,9 @@ +package com.io.yutian.elementoriginlib.serialize; + +public interface Serializer { + + Object serialize(T value); + + T deserialize(Object value); + +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/serialize/serializers/ItemStackSerializer.java b/src/main/java/com/io/yutian/elementoriginlib/serialize/serializers/ItemStackSerializer.java new file mode 100644 index 0000000..c79c0ac --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/serialize/serializers/ItemStackSerializer.java @@ -0,0 +1,95 @@ +package com.io.yutian.elementoriginlib.serialize.serializers; + +import com.io.yutian.elementoriginlib.serialize.Serializer; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import net.minecraft.nbt.*; +import org.bukkit.craftbukkit.v1_20_R1.inventory.CraftItemStack; +import org.bukkit.inventory.ItemStack; +import org.json.JSONArray; +import org.json.JSONObject; + +public class ItemStackSerializer implements Serializer { + + @Override + public Object serialize(ItemStack value) { + JSONObject jsonObject = new JSONObject(); + net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(value); + jsonObject.put("id", nmsItemStack.c().d().j().g()); + jsonObject.put("Count", nmsItemStack.J()); + NBTTagCompound nbtTagCompound = nmsItemStack.w(); + JSONObject tagJsonObject = new JSONObject(); + for (String key : nbtTagCompound.e()) { + NBTBase nbtBase = nbtTagCompound.c(key); + tagJsonObject.put(key, serializeNBT(nbtBase)); + } + jsonObject.put("tag", tagJsonObject); + return jsonObject; + } + + private Object serializeNBT(NBTBase nbtBase) { + byte type = nbtBase.b(); + switch (type) { + case 1: + NBTTagByte nbtTagByte = (NBTTagByte) nbtBase; + return nbtTagByte.i(); + case 2: + NBTTagShort nbtTagShort = (NBTTagShort) nbtBase; + return nbtTagShort.h(); + case 3: + NBTTagInt nbtTagInt = (NBTTagInt) nbtBase; + return nbtTagInt.g(); + case 4: + NBTTagLong nbtTagLong = (NBTTagLong) nbtBase; + return nbtTagLong.f(); + case 5: + NBTTagFloat nbtTagFloat = (NBTTagFloat) nbtBase; + return nbtTagFloat.k(); + case 6: + NBTTagDouble nbtTagDouble = (NBTTagDouble) nbtBase; + return nbtTagDouble.j(); + case 7: + NBTTagByteArray tagByteArray = (NBTTagByteArray) nbtBase; + return tagByteArray.e(); + case 8: + NBTTagString nbtTagString = (NBTTagString) nbtBase; + return nbtTagString.m_(); + case 9: + NBTTagList nbtTagList = (NBTTagList) nbtBase; + JSONArray jsonArray = new JSONArray(); + for (net.minecraft.nbt.NBTBase base : nbtTagList) { + jsonArray.put(serializeNBT(base)); + } + return jsonArray; + case 10: + NBTTagCompound nbtTagCompound = (NBTTagCompound) nbtBase; + JSONObject jsonObject = new JSONObject(); + for (String key : nbtTagCompound.e()) { + jsonObject.put(key, serializeNBT(nbtTagCompound.c(key))); + } + return jsonObject; + case 11: + NBTTagIntArray nbtTagIntArray = (NBTTagIntArray) nbtBase; + return nbtTagIntArray.g(); + case 12: + NBTTagLongArray nbtTagLongArray = (NBTTagLongArray) nbtBase; + return nbtTagLongArray.g(); + } + return null; + } + + @Override + public ItemStack deserialize(Object value) { + JSONObject jsonObject = (JSONObject) value; + String arg = jsonObject.toString(); + NBTTagCompound nbtTagCompound; + try { + nbtTagCompound = net.minecraft.nbt.MojangsonParser.a(arg); + } catch (CommandSyntaxException e) { + e.printStackTrace(); + return null; + } + net.minecraft.world.item.ItemStack nmsItemStack = net.minecraft.world.item.ItemStack.a(nbtTagCompound); + return CraftItemStack.asCraftMirror(nmsItemStack); + } + +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/serialize/serializers/UUIDSerializer.java b/src/main/java/com/io/yutian/elementoriginlib/serialize/serializers/UUIDSerializer.java new file mode 100644 index 0000000..bd1d54f --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/serialize/serializers/UUIDSerializer.java @@ -0,0 +1,17 @@ +package com.io.yutian.elementoriginlib.serialize.serializers; + +import com.io.yutian.elementoriginlib.serialize.Serializer; + +import java.util.UUID; + +public class UUIDSerializer implements Serializer { + @Override + public Object serialize(UUID value) { + return value.toString(); + } + + @Override + public UUID deserialize(Object value) { + return UUID.fromString((String) value); + } +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/sql/SQLHelper.java b/src/main/java/com/io/yutian/elementoriginlib/sql/SQLHelper.java new file mode 100644 index 0000000..968856c --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/sql/SQLHelper.java @@ -0,0 +1,55 @@ +package com.io.yutian.elementoriginlib.sql; +import com.io.yutian.elementoriginlib.sql.api.SQLManager; +import com.io.yutian.elementoriginlib.sql.api.SQLQuery; +import com.io.yutian.elementoriginlib.sql.api.util.TimeDateUtils; +import com.io.yutian.elementoriginlib.sql.manager.SQLManagerImpl; +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Properties; + +public class SQLHelper { + + public static SQLManagerImpl createManager( + @NotNull String driver, @NotNull String url, + @NotNull String username, @Nullable String password) { + HikariConfig config = new HikariConfig(); + config.setDriverClassName(driver); + config.setJdbcUrl(url); + config.setUsername(username); + config.setPassword(password); + return createManager(config); + } + + public static SQLManagerImpl createManager(@NotNull Properties properties) { + return createManager(new HikariConfig(properties)); + } + + public static SQLManagerImpl createManager(@NotNull HikariConfig config) { + return new SQLManagerImpl(new HikariDataSource(config)); + } + + public static void shutdownManager(SQLManager manager, boolean forceClose, boolean outputActiveQuery) { + if (!manager.getActiveQuery().isEmpty()) { + manager.getLogger().warn("There are " + manager.getActiveQuery().size() + " connections still running"); + for (SQLQuery value : manager.getActiveQuery().values()) { + if (outputActiveQuery) { + manager.getLogger().warn(String.format("#%s -> %s", value.getAction().getShortID(), value.getSQLContent())); + manager.getLogger().warn(String.format("- execute at %s", TimeDateUtils.getTimeString(value.getExecuteTime()))); + } + if (forceClose) value.close(); + } + } + if (manager.getDataSource() instanceof HikariDataSource) { + //Close hikari pool + ((HikariDataSource) manager.getDataSource()).close(); + } + } + + public static void shutdownManager(SQLManager manager) { + shutdownManager(manager, true, manager.isDebugMode()); + } + +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/sql/action/AbstractSQLAction.java b/src/main/java/com/io/yutian/elementoriginlib/sql/action/AbstractSQLAction.java new file mode 100644 index 0000000..30daee4 --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/sql/action/AbstractSQLAction.java @@ -0,0 +1,102 @@ +package com.io.yutian.elementoriginlib.sql.action; + +import com.io.yutian.elementoriginlib.sql.api.SQLAction; +import com.io.yutian.elementoriginlib.sql.api.function.SQLExceptionHandler; +import com.io.yutian.elementoriginlib.sql.api.function.SQLFunction; +import com.io.yutian.elementoriginlib.sql.api.function.SQLHandler; +import com.io.yutian.elementoriginlib.sql.manager.SQLManagerImpl; +import org.jetbrains.annotations.NotNull; + +import java.sql.SQLException; +import java.util.List; +import java.util.Objects; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +public abstract class AbstractSQLAction implements SQLAction { + + protected final @NotNull String sqlContent; + private final @NotNull SQLManagerImpl sqlManager; + private final @NotNull UUID uuid; + private final long createNanoTime; + + public AbstractSQLAction(@NotNull SQLManagerImpl manager, @NotNull String sql) { + this(manager, sql, System.nanoTime()); + } + + public AbstractSQLAction(@NotNull SQLManagerImpl manager, @NotNull String sql, @NotNull UUID uuid) { + this(manager, sql, uuid, System.nanoTime()); + } + + public AbstractSQLAction(@NotNull SQLManagerImpl manager, @NotNull String sql, long createNanoTime) { + this(manager, sql, UUID.randomUUID(), createNanoTime); + } + + public AbstractSQLAction(@NotNull SQLManagerImpl manager, @NotNull String sql, + @NotNull UUID uuid, long createNanoTime) { + Objects.requireNonNull(manager); + Objects.requireNonNull(sql); + Objects.requireNonNull(uuid); + this.sqlManager = manager; + this.sqlContent = sql; + this.uuid = uuid; + this.createNanoTime = createNanoTime; + } + + + @Override + public @NotNull UUID getActionUUID() { + return this.uuid; + } + + @Override + public @NotNull String getShortID() { + return getActionUUID().toString().substring(0, 8); + } + + @Override + public long getCreateTime(TimeUnit unit) { + return unit.convert(createNanoTime, TimeUnit.NANOSECONDS); + } + + @Override + public @NotNull String getSQLContent() { + return this.sqlContent.trim(); + } + + @Override + public @NotNull SQLManagerImpl getManager() { + return this.sqlManager; + } + + protected void debugMessage(List params) { + if (getManager().isDebugMode()) { + try { + getManager().getDebugHandler().beforeExecute(this, params); + } catch (Exception exception) { + exception.printStackTrace(); + } + } + } + + @Override + @SuppressWarnings("FutureReturnValueIgnored") + public void executeAsync(SQLHandler success, SQLExceptionHandler failure) { + getManager().getExecutorPool().submit(() -> { + try { + T returnedValue = execute(); + if (success != null) success.accept(returnedValue); + } catch (SQLException e) { + handleException(failure, e); + } + }); + } + + @Override + public @NotNull CompletableFuture executeFuture(@NotNull SQLFunction handler) { + CompletableFuture future = new CompletableFuture<>(); + executeAsync((t -> future.complete(handler.apply(t))), (e, q) -> future.completeExceptionally(e)); + return future; + } +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/sql/action/PreparedSQLBatchUpdateActionImpl.java b/src/main/java/com/io/yutian/elementoriginlib/sql/action/PreparedSQLBatchUpdateActionImpl.java new file mode 100644 index 0000000..751d3d9 --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/sql/action/PreparedSQLBatchUpdateActionImpl.java @@ -0,0 +1,92 @@ +package com.io.yutian.elementoriginlib.sql.action; + +import com.io.yutian.elementoriginlib.sql.api.action.PreparedSQLUpdateBatchAction; +import com.io.yutian.elementoriginlib.sql.manager.SQLManagerImpl; +import com.io.yutian.elementoriginlib.sql.util.StatementUtil; +import org.jetbrains.annotations.NotNull; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; + +public class PreparedSQLBatchUpdateActionImpl + extends AbstractSQLAction> + implements PreparedSQLUpdateBatchAction { + + boolean returnKeys = false; + @NotNull List allParams = new ArrayList<>(); + + protected final @NotNull Class numberClass; + + public PreparedSQLBatchUpdateActionImpl(@NotNull SQLManagerImpl manager, @NotNull Class numberClass, + @NotNull String sql) { + super(manager, sql); + this.numberClass = numberClass; + this.allParams = new ArrayList<>(); + } + + public PreparedSQLBatchUpdateActionImpl(@NotNull SQLManagerImpl manager, @NotNull Class numberClass, + @NotNull UUID uuid, @NotNull String sql) { + super(manager, sql, uuid); + this.numberClass = numberClass; + } + + @Override + public PreparedSQLBatchUpdateActionImpl setAllParams(Iterable allParams) { + List paramsList = new ArrayList<>(); + allParams.forEach(paramsList::add); + this.allParams = paramsList; + return this; + } + + @Override + public PreparedSQLBatchUpdateActionImpl addParamsBatch(Object... params) { + this.allParams.add(params); + return this; + } + + @Override + public PreparedSQLBatchUpdateActionImpl returnGeneratedKeys() { + this.returnKeys = true; + return this; + } + + @Override + public PreparedSQLBatchUpdateActionImpl returnGeneratedKeys(Class keyTypeClass) { + return new PreparedSQLBatchUpdateActionImpl<>(getManager(), keyTypeClass, getActionUUID(), getSQLContent()) + .setAllParams(allParams).returnGeneratedKeys(); + } + + @Override + public @NotNull List execute() throws SQLException { + debugMessage(allParams); + + try (Connection connection = getManager().getConnection()) { + try (PreparedStatement statement = StatementUtil.createPrepareStatementBatch( + connection, getSQLContent(), allParams, returnKeys + )) { + int[] executed = statement.executeBatch(); + + if (!returnKeys) { + return Arrays.stream(executed).mapToObj(numberClass::cast).collect(Collectors.toList()); + } else { + try (ResultSet resultSet = statement.getGeneratedKeys()) { + List generatedKeys = new ArrayList<>(); + while (resultSet.next()) { + generatedKeys.add(resultSet.getObject(1, numberClass)); + } + return generatedKeys; + } + } + } + + } + } + +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/sql/action/PreparedSQLUpdateActionImpl.java b/src/main/java/com/io/yutian/elementoriginlib/sql/action/PreparedSQLUpdateActionImpl.java new file mode 100644 index 0000000..65d7a8d --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/sql/action/PreparedSQLUpdateActionImpl.java @@ -0,0 +1,94 @@ +package com.io.yutian.elementoriginlib.sql.action; + +import com.io.yutian.elementoriginlib.sql.api.action.PreparedSQLUpdateAction; +import com.io.yutian.elementoriginlib.sql.api.action.SQLUpdateAction; +import com.io.yutian.elementoriginlib.sql.manager.SQLManagerImpl; +import com.io.yutian.elementoriginlib.sql.util.StatementUtil; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.UUID; + +public class PreparedSQLUpdateActionImpl + extends SQLUpdateActionImpl + implements PreparedSQLUpdateAction { + + Object[] params; + + public PreparedSQLUpdateActionImpl(@NotNull SQLManagerImpl manager, @NotNull Class numberClass, + @NotNull String sql) { + this(manager, numberClass, sql, (Object[]) null); + } + + public PreparedSQLUpdateActionImpl(@NotNull SQLManagerImpl manager, @NotNull Class numberClass, + @NotNull String sql, @Nullable List params) { + this(manager, numberClass, sql, params == null ? null : params.toArray()); + } + + public PreparedSQLUpdateActionImpl(@NotNull SQLManagerImpl manager, @NotNull Class numberClass, + @NotNull String sql, @Nullable Object[] params) { + super(manager, numberClass, sql); + this.params = params; + } + + public PreparedSQLUpdateActionImpl(@NotNull SQLManagerImpl manager, @NotNull Class numberClass, + @NotNull UUID uuid, @NotNull String sql, + Object[] params) { + super(manager, numberClass, uuid, sql); + this.params = params; + } + + @Override + public PreparedSQLUpdateActionImpl setParams(Object... params) { + this.params = params; + return this; + } + + @Override + public PreparedSQLUpdateActionImpl setParams(@Nullable Iterable params) { + if (params == null) { + return setParams((Object[]) null); + } else { + List paramsList = new ArrayList<>(); + params.forEach(paramsList::add); + return setParams(paramsList.toArray()); + } + } + + @Override + public @NotNull T execute() throws SQLException { + debugMessage(Collections.singletonList(params)); + + try (Connection connection = getManager().getConnection()) { + + try (PreparedStatement statement = StatementUtil.createPrepareStatement( + connection, getSQLContent(), params, returnGeneratedKeys + )) { + + int changes = statement.executeUpdate(); + if (!returnGeneratedKeys) return numberClass.cast(changes); + else { + try (ResultSet resultSet = statement.getGeneratedKeys()) { + return resultSet.next() ? resultSet.getObject(1, numberClass) : numberClass.cast(0); + } + } + + } + } + + } + + @Override + public SQLUpdateAction returnGeneratedKey(Class keyTypeClass) { + PreparedSQLUpdateActionImpl newAction = new PreparedSQLUpdateActionImpl<>(getManager(), keyTypeClass, getActionUUID(), getSQLContent(), params); + newAction.returnGeneratedKey(); + return newAction; + } +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/sql/action/SQLUpdateActionImpl.java b/src/main/java/com/io/yutian/elementoriginlib/sql/action/SQLUpdateActionImpl.java new file mode 100644 index 0000000..2d5ea5b --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/sql/action/SQLUpdateActionImpl.java @@ -0,0 +1,65 @@ +package com.io.yutian.elementoriginlib.sql.action; + +import com.io.yutian.elementoriginlib.sql.api.action.SQLUpdateAction; +import com.io.yutian.elementoriginlib.sql.manager.SQLManagerImpl; +import org.jetbrains.annotations.NotNull; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.UUID; + +public class SQLUpdateActionImpl + extends AbstractSQLAction + implements SQLUpdateAction { + + protected final @NotNull Class numberClass; + + protected boolean returnGeneratedKeys = false; + + public SQLUpdateActionImpl(@NotNull SQLManagerImpl manager, @NotNull Class numberClass, + @NotNull String sql) { + super(manager, sql); + this.numberClass = numberClass; + } + + public SQLUpdateActionImpl(@NotNull SQLManagerImpl manager, @NotNull Class numberClass, + @NotNull UUID uuid, @NotNull String sql) { + super(manager, sql, uuid); + this.numberClass = numberClass; + } + + @Override + public @NotNull T execute() throws SQLException { + debugMessage(new ArrayList<>()); + + try (Connection connection = getManager().getConnection()) { + try (Statement statement = connection.createStatement()) { + + if (!returnGeneratedKeys) { + return numberClass.cast(statement.executeUpdate(getSQLContent())); + } else { + statement.executeUpdate(getSQLContent(), Statement.RETURN_GENERATED_KEYS); + + try (ResultSet resultSet = statement.getGeneratedKeys()) { + return resultSet.next() ? resultSet.getObject(1, numberClass) : numberClass.cast(0); + } + } + } + } + } + + @Override + public SQLUpdateAction returnGeneratedKey() { + this.returnGeneratedKeys = true; + return this; + } + + @Override + public SQLUpdateAction returnGeneratedKey(Class keyTypeClass) { + return new SQLUpdateActionImpl<>(getManager(), keyTypeClass, getActionUUID(), getSQLContent()).returnGeneratedKey(); + } + +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/sql/action/SQLUpdateBatchActionImpl.java b/src/main/java/com/io/yutian/elementoriginlib/sql/action/SQLUpdateBatchActionImpl.java new file mode 100644 index 0000000..4defb50 --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/sql/action/SQLUpdateBatchActionImpl.java @@ -0,0 +1,59 @@ +package com.io.yutian.elementoriginlib.sql.action; + +import com.io.yutian.elementoriginlib.sql.api.action.SQLUpdateBatchAction; +import com.io.yutian.elementoriginlib.sql.manager.SQLManagerImpl; +import org.jetbrains.annotations.NotNull; + +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +public class SQLUpdateBatchActionImpl + extends AbstractSQLAction> + implements SQLUpdateBatchAction { + + protected final List sqlContents = new ArrayList<>(); + + public SQLUpdateBatchActionImpl(@NotNull SQLManagerImpl manager, @NotNull String sql) { + super(manager, sql); + this.sqlContents.add(sql); + } + + @Override + public @NotNull List getSQLContents() { + return this.sqlContents; + } + + @Override + public SQLUpdateBatchAction addBatch(@NotNull String sql) { + Objects.requireNonNull(sql, "sql could not be null"); + this.sqlContents.add(sql); + return this; + } + + @Override + public @NotNull List execute() throws SQLException { + debugMessage(new ArrayList<>()); + + try (Connection connection = getManager().getConnection()) { + + try (Statement statement = connection.createStatement()) { + + for (String content : this.sqlContents) { + statement.addBatch(content); + } + + int[] executed = statement.executeBatch(); + + return Arrays.stream(executed).boxed().collect(Collectors.toList()); + } + + } + } + +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/sql/action/query/PreparedQueryActionImpl.java b/src/main/java/com/io/yutian/elementoriginlib/sql/action/query/PreparedQueryActionImpl.java new file mode 100644 index 0000000..df9ec6b --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/sql/action/query/PreparedQueryActionImpl.java @@ -0,0 +1,83 @@ +package com.io.yutian.elementoriginlib.sql.action.query; + +import com.io.yutian.elementoriginlib.sql.api.action.query.PreparedQueryAction; +import com.io.yutian.elementoriginlib.sql.manager.SQLManagerImpl; +import com.io.yutian.elementoriginlib.sql.util.StatementUtil; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.function.Consumer; + +public class PreparedQueryActionImpl extends QueryActionImpl implements PreparedQueryAction { + + Consumer handler; + Object[] params; + + public PreparedQueryActionImpl(@NotNull SQLManagerImpl manager, @NotNull String sql) { + super(manager, sql); + } + + @Override + public PreparedQueryActionImpl setParams(@Nullable Object... params) { + this.params = params; + return this; + } + + @Override + public PreparedQueryActionImpl setParams(@Nullable Iterable params) { + if (params == null) { + return setParams((Object[]) null); + } else { + List paramsList = new ArrayList<>(); + params.forEach(paramsList::add); + return setParams(paramsList.toArray()); + } + } + + @Override + public PreparedQueryActionImpl handleStatement(@Nullable Consumer statement) { + this.handler = statement; + return this; + } + + + @Override + public @NotNull SQLQueryImpl execute() throws SQLException { + debugMessage(Collections.singletonList(params)); + + Connection connection = getManager().getConnection(); + PreparedStatement preparedStatement; + try { + if (handler == null) { + preparedStatement = StatementUtil.createPrepareStatement(connection, getSQLContent(), this.params); + } else { + preparedStatement = connection.prepareStatement(getSQLContent()); + handler.accept(preparedStatement); + } + } catch (SQLException exception) { + connection.close(); + throw exception; + } + + try { + SQLQueryImpl query = new SQLQueryImpl( + getManager(), this, + connection, preparedStatement, + preparedStatement.executeQuery() + ); + getManager().getActiveQuery().put(getActionUUID(), query); + return query; + } catch (SQLException exception) { + preparedStatement.close(); + connection.close(); + throw exception; + } + + } +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/sql/action/query/QueryActionImpl.java b/src/main/java/com/io/yutian/elementoriginlib/sql/action/query/QueryActionImpl.java new file mode 100644 index 0000000..528b32e --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/sql/action/query/QueryActionImpl.java @@ -0,0 +1,63 @@ +package com.io.yutian.elementoriginlib.sql.action.query; + +import com.io.yutian.elementoriginlib.sql.action.AbstractSQLAction; +import com.io.yutian.elementoriginlib.sql.api.SQLQuery; +import com.io.yutian.elementoriginlib.sql.api.action.query.QueryAction; +import com.io.yutian.elementoriginlib.sql.api.function.SQLExceptionHandler; +import com.io.yutian.elementoriginlib.sql.api.function.SQLHandler; +import com.io.yutian.elementoriginlib.sql.manager.SQLManagerImpl; +import org.jetbrains.annotations.NotNull; + +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.ArrayList; + +public class QueryActionImpl extends AbstractSQLAction implements QueryAction { + + public QueryActionImpl(@NotNull SQLManagerImpl manager, @NotNull String sql) { + super(manager, sql); + } + + @Override + public @NotNull SQLQueryImpl execute() throws SQLException { + debugMessage(new ArrayList<>()); + + Connection connection = getManager().getConnection(); + Statement statement; + + try { + statement = connection.createStatement(); + } catch (SQLException ex) { + connection.close(); + throw ex; + } + + try { + SQLQueryImpl query = new SQLQueryImpl( + getManager(), this, + connection, statement, + statement.executeQuery(getSQLContent()) + ); + getManager().getActiveQuery().put(getActionUUID(), query); + + return query; + } catch (SQLException exception) { + statement.close(); + connection.close(); + throw exception; + } + } + + + @Override + public void executeAsync(SQLHandler success, SQLExceptionHandler failure) { + getManager().getExecutorPool().submit(() -> { + try (SQLQueryImpl query = execute()) { + if (success != null) success.accept(query); + } catch (SQLException exception) { + handleException(failure, exception); + } + }); + } +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/sql/action/query/SQLQueryImpl.java b/src/main/java/com/io/yutian/elementoriginlib/sql/action/query/SQLQueryImpl.java new file mode 100644 index 0000000..c32b671 --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/sql/action/query/SQLQueryImpl.java @@ -0,0 +1,97 @@ +package com.io.yutian.elementoriginlib.sql.action.query; + +import com.io.yutian.elementoriginlib.sql.api.SQLQuery; +import com.io.yutian.elementoriginlib.sql.manager.SQLManagerImpl; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.concurrent.TimeUnit; + +public class SQLQueryImpl implements SQLQuery { + + protected final long executeTime; + + protected final SQLManagerImpl sqlManager; + protected final Connection connection; + protected final Statement statement; + protected final ResultSet resultSet; + protected QueryActionImpl queryAction; + + public SQLQueryImpl( + SQLManagerImpl sqlManager, QueryActionImpl queryAction, + Connection connection, Statement statement, ResultSet resultSet + ) { + this(sqlManager, queryAction, connection, statement, resultSet, System.nanoTime()); + } + + public SQLQueryImpl( + SQLManagerImpl sqlManager, QueryActionImpl queryAction, + Connection connection, Statement statement, ResultSet resultSet, + long executeTime + ) { + this.executeTime = executeTime; + this.sqlManager = sqlManager; + this.queryAction = queryAction; + this.connection = connection; + this.statement = statement; + this.resultSet = resultSet; + } + + @Override + public long getExecuteTime(TimeUnit timeUnit) { + return timeUnit.convert(this.executeTime, TimeUnit.NANOSECONDS); + } + + @Override + public SQLManagerImpl getManager() { + return this.sqlManager; + } + + @Override + public QueryActionImpl getAction() { + return this.queryAction; + } + + @Override + public ResultSet getResultSet() { + return this.resultSet; + } + + @Override + public String getSQLContent() { + return getAction().getSQLContent(); + } + + @Override + public void close() { + try { + if (getResultSet() != null && !getResultSet().isClosed()) getResultSet().close(); + if (getStatement() != null && !getStatement().isClosed()) getStatement().close(); + if (getConnection() != null && !getConnection().isClosed()) getConnection().close(); + + if (getManager().isDebugMode()) { + try { + getManager().getDebugHandler().afterQuery(this, getExecuteTime(TimeUnit.NANOSECONDS), System.nanoTime()); + } catch (Exception ex) { + ex.printStackTrace(); + } + } + getManager().getActiveQuery().remove(getAction().getActionUUID()); + } catch (SQLException e) { + getAction().handleException(getAction().defaultExceptionHandler(), e); + } + this.queryAction = null; + } + + @Override + public Statement getStatement() { + return this.statement; + } + + @Override + public Connection getConnection() { + return this.connection; + } +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/sql/api/SQLAction.java b/src/main/java/com/io/yutian/elementoriginlib/sql/api/SQLAction.java new file mode 100644 index 0000000..e7f9ae8 --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/sql/api/SQLAction.java @@ -0,0 +1,254 @@ +package com.io.yutian.elementoriginlib.sql.api; + +import com.io.yutian.elementoriginlib.sql.api.function.SQLExceptionHandler; +import com.io.yutian.elementoriginlib.sql.api.function.SQLFunction; +import com.io.yutian.elementoriginlib.sql.api.function.SQLHandler; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; + +import java.sql.SQLException; +import java.util.Collections; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +/** + * SQLAction 是用于承载SQL语句并进行处理、返回的基本类。 + * + *
    + *
  • 同步执行 {@link #execute()}, {@link #execute(SQLFunction, SQLExceptionHandler)} + *
    同步执行方法中有会抛出异常的方法与不抛出异常的方法, + *
    若选择不抛出异常,则返回值可能为空,需要特殊处理。
  • + * + *
  • 异步执行 {@link #executeAsync(SQLHandler, SQLExceptionHandler)} + *
    异步执行时将提供成功与异常两种处理方式 + *
    可自行选择是否对数据或异常进行处理 + *
    默认的异常处理器为 {@link #defaultExceptionHandler()} + *
    若有特殊需要,可通过{@link #setExceptionHandler(SQLExceptionHandler)} 方法修改默认的处理器
  • + *
+ * + * @param 需要返回的类型 + * @author CarmJos + * @since 0.0.1 + */ +public interface SQLAction { + + /** + * 得到该Action的UUID + * + * @return UUID + */ + @NotNull UUID getActionUUID(); + + /** + * 得到短八位格式的UUID + * + * @return UUID(8) + */ + @NotNull String getShortID(); + + /** + * 得到该Action的创建时间。 + *
注意,此处获得的时间非时间戳毫秒数,仅用于计算耗时。 + * + * @return 创建时间 (毫秒) + */ + default long getCreateTime() { + return getCreateTime(TimeUnit.MILLISECONDS); + } + + /** + * 得到该Action的创建时间 + *
注意,此处获得的时间非时间戳毫秒数,仅用于计算耗时。 + * + * @param unit 时间单位 + * @return 创建时间 + */ + long getCreateTime(TimeUnit unit); + + /** + * 得到该Action所要执行的源SQL语句 + * + * @return 源SQL语句 + */ + @NotNull String getSQLContent(); + + /** + * 得到该Action所要执行的源SQL语句列表。 + * + * @return 源SQL语句列表 + */ + default @NotNull List getSQLContents() { + return Collections.singletonList(getSQLContent()); + } + + /** + * 得到承载该Action的对应{@link SQLManager} + * + * @return {@link SQLManager} + */ + @NotNull SQLManager getManager(); + + /** + * 执行该Action对应的SQL语句 + * + * @return 指定数据类型 + * @throws SQLException 当SQL操作出现问题时抛出 + */ + @NotNull T execute() throws SQLException; + + + /** + * 执行语句并返回值 + * + * @param exceptionHandler 异常处理器 默认为 {@link #defaultExceptionHandler()} + * @return 指定类型数据 + */ + @Nullable + default T execute(@Nullable SQLExceptionHandler exceptionHandler) { + return execute(t -> t, exceptionHandler); + } + + /** + * 执行语句并处理返回值 + * + * @param function 处理方法 + * @param exceptionHandler 异常处理器 默认为 {@link #defaultExceptionHandler()} + * @param 需要返回的内容 + * @return 指定类型数据 + */ + @Nullable + default R execute(@NotNull SQLFunction function, + @Nullable SQLExceptionHandler exceptionHandler) { + return execute(function, null, exceptionHandler); + } + + /** + * 执行语句并处理返回值 + * + * @param function 处理方法 + * @param defaultResult 默认结果,若处理后的结果为null,则返回该值 + * @param exceptionHandler 异常处理器 默认为 {@link #defaultExceptionHandler()} + * @param 需要返回的内容 + * @return 指定类型数据 + */ + @Nullable + @Contract("_,!null,_ -> !null") + default R execute(@NotNull SQLFunction function, + @Nullable R defaultResult, + @Nullable SQLExceptionHandler exceptionHandler) { + try { + return executeFunction(function, defaultResult); + } catch (SQLException exception) { + handleException(exceptionHandler, exception); + return null; + } + } + + /** + * 执行语句并处理返回值 + * + * @param function 处理方法 + * @param 需要返回的内容 + * @return 指定类型数据 + * @throws SQLException 当SQL操作出现问题时抛出 + */ + @Nullable + default R executeFunction(@NotNull SQLFunction<@NotNull T, R> function) throws SQLException { + return executeFunction(function, null); + } + + /** + * 执行语句并处理返回值 + * + * @param function 处理方法 + * @param defaultResult 默认结果,若处理后的结果为null,则返回该值 + * @param 需要返回的内容 + * @return 指定类型数据 + * @throws SQLException 当SQL操作出现问题时抛出 + */ + @Nullable + @Contract("_,!null -> !null") + default R executeFunction(@NotNull SQLFunction<@NotNull T, R> function, + @Nullable R defaultResult) throws SQLException { + try { + R result = function.apply(execute()); + return result == null ? defaultResult : result; + } catch (SQLException exception) { + throw new SQLException(exception); + } + } + + /** + * 异步执行SQL语句,采用默认异常处理,无需返回值。 + */ + default void executeAsync() { + executeAsync(null); + } + + /** + * 异步执行SQL语句 + * + * @param success 成功时的操作 + */ + default void executeAsync(@Nullable SQLHandler success) { + executeAsync(success, null); + } + + /** + * 异步执行SQL语句 + * + * @param success 成功时的操作 + * @param failure 异常处理器 默认为 {@link SQLAction#defaultExceptionHandler()} + */ + void executeAsync(@Nullable SQLHandler success, + @Nullable SQLExceptionHandler failure); + + /** + * 以异步Future方式执行SQL语句。 + * + * @return 异步执行的Future实例,可通过 {@link Future#get()} 阻塞并等待结果。 + */ + default @NotNull CompletableFuture executeFuture() { + return executeFuture((t -> null)); + } + + /** + * 以异步Future方式执行SQL语句。 + * + * @return 异步执行的Future实例,可通过 {@link Future#get()} 阻塞并等待结果。 + */ + @NotNull CompletableFuture executeFuture(@NotNull SQLFunction handler); + + default void handleException(@Nullable SQLExceptionHandler handler, SQLException exception) { + if (handler == null) handler = defaultExceptionHandler(); + handler.accept(exception, this); + } + + /** + * 获取管理器提供的默认异常处理器。 + * 若未使用过 {@link #setExceptionHandler(SQLExceptionHandler)} 方法, + * 则默认返回 {@link SQLExceptionHandler#detailed(Logger)} 。 + * + * @return {@link SQLExceptionHandler} + */ + default SQLExceptionHandler defaultExceptionHandler() { + return getManager().getExceptionHandler(); + } + + /** + * 设定通用的异常处理器。 + *
在使用 {@link #execute(SQLExceptionHandler)} 等相关方法时,若传入的处理器为null,则会采用此处理器。 + *
若该方法传入参数为 null,则会使用 {@link #defaultExceptionHandler()} 。 + * + * @param handler 异常处理器 + */ + default void setExceptionHandler(@Nullable SQLExceptionHandler handler) { + getManager().setExceptionHandler(handler); + } + +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/sql/api/SQLBuilder.java b/src/main/java/com/io/yutian/elementoriginlib/sql/api/SQLBuilder.java new file mode 100644 index 0000000..8af5776 --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/sql/api/SQLBuilder.java @@ -0,0 +1,31 @@ +package com.io.yutian.elementoriginlib.sql.api; + +import org.jetbrains.annotations.NotNull; + +/** + * SQLBuilder 是用于构建SQL语句以生成SQLAction执行操作的中间类。 + *
其连接了{@link SQLManager} 与 {@link SQLAction} ,避免大量的代码堆积 + *
也是本接口的核心功能所在 + * + * @author CarmJos + */ +public interface SQLBuilder { + + static @NotNull String withBackQuote(@NotNull String str) { + str = str.trim(); + return !str.isEmpty() && str.charAt(0) == '`' && str.charAt(str.length() - 1) == '`' ? str : "`" + str + "`"; + } + + static @NotNull String withQuote(@NotNull String str) { + str = str.trim(); + return !str.isEmpty() && str.charAt(0) == '\'' && str.charAt(str.length() - 1) == '\'' ? str : "'" + str + "'"; + } + + /** + * 得到承载该Builder的对应{@link SQLManager} + * + * @return {@link SQLManager} + */ + @NotNull SQLManager getManager(); + +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/sql/api/SQLManager.java b/src/main/java/com/io/yutian/elementoriginlib/sql/api/SQLManager.java new file mode 100644 index 0000000..bb5563b --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/sql/api/SQLManager.java @@ -0,0 +1,316 @@ +package com.io.yutian.elementoriginlib.sql.api; + +import com.io.yutian.elementoriginlib.sql.api.action.PreparedSQLUpdateAction; +import com.io.yutian.elementoriginlib.sql.api.action.PreparedSQLUpdateBatchAction; +import com.io.yutian.elementoriginlib.sql.api.action.SQLUpdateAction; +import com.io.yutian.elementoriginlib.sql.api.action.SQLUpdateBatchAction; +import com.io.yutian.elementoriginlib.sql.api.builder.*; +import com.io.yutian.elementoriginlib.sql.api.function.SQLBiFunction; +import com.io.yutian.elementoriginlib.sql.api.function.SQLDebugHandler; +import com.io.yutian.elementoriginlib.sql.api.function.SQLExceptionHandler; +import com.io.yutian.elementoriginlib.sql.api.function.SQLFunction; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.function.Supplier; + +/** + * SQLManager 是EasySQL的核心类,用于管理数据库连接,提供数据库操作的方法。 + * + * @author CarmJos + */ +public interface SQLManager { + + Logger getLogger(); + + boolean isDebugMode(); + + + /** + * 获取用于执行 {@link SQLAction#executeAsync()} 的线程池。 + *
默认线程池为 {@link #defaultExecutorPool(String)} 。 + * + * @return {@link ExecutorService} + */ + @NotNull ExecutorService getExecutorPool(); + + /** + * 设定用于执行 {@link SQLAction#executeAsync()} 的线程池. + *
默认线程池为 {@link #defaultExecutorPool(String)} 。 + * + * @param executorPool {@link ExecutorService} + */ + void setExecutorPool(@NotNull ExecutorService executorPool); + + static ExecutorService defaultExecutorPool(String threadName) { + return Executors.newFixedThreadPool(4, r -> { + Thread thread = new Thread(r, threadName); + thread.setDaemon(true); + return thread; + }); + } + + + /** + * 设定是否启用调试模式。 + * 启用调试模式后,会在每次执行SQL语句时,调用 {@link #getDebugHandler()} 来输出调试信息。 + * + * @param debugMode 是否启用调试模式 + */ + void setDebugMode(@NotNull Supplier<@NotNull Boolean> debugMode); + + /** + * 设定是否启用调试模式。 + * 启用调试模式后,会在每次执行SQL语句时,调用 {@link #getDebugHandler()} 来输出调试信息。 + * + * @param enable 是否启用调试模式 + */ + default void setDebugMode(boolean enable) { + setDebugMode(() -> enable); + } + + /** + * 获取调试处理器,用于处理调试信息。 + * + * @return {@link SQLDebugHandler} + */ + @NotNull SQLDebugHandler getDebugHandler(); + + /** + * 设定调试处理器,默认为 {@link SQLDebugHandler#defaultHandler(Logger)} 。 + * + * @param debugHandler {@link SQLDebugHandler} + */ + void setDebugHandler(@NotNull SQLDebugHandler debugHandler); + + /** + * 得到连接池源 + * + * @return DataSource + */ + @NotNull DataSource getDataSource(); + + /** + * 得到一个数据库连接实例 + * + * @return Connection + * @throws SQLException 见 {@link DataSource#getConnection()} + */ + @NotNull Connection getConnection() throws SQLException; + + /** + * 得到正使用的查询。 + * + * @return 查询列表 + */ + @NotNull Map getActiveQuery(); + + /** + * 获取改管理器提供的默认异常处理器。 + * 若未使用过 {@link #setExceptionHandler(SQLExceptionHandler)} 方法, + * 则默认返回 {@link SQLExceptionHandler#detailed(Logger)} 。 + * + * @return {@link SQLExceptionHandler} + */ + @NotNull SQLExceptionHandler getExceptionHandler(); + + /** + * 设定通用的异常处理器。 + *
在使用 {@link SQLAction#execute(SQLExceptionHandler)} 等相关方法时,若传入的处理器为null,则会采用此处理器。 + *
若该方法传入参数为 null,则会使用 {@link SQLExceptionHandler#detailed(Logger)} 。 + * + * @param handler 异常处理器 + */ + void setExceptionHandler(@Nullable SQLExceptionHandler handler); + + /** + * 执行一条不需要返回结果的SQL语句(多用于UPDATE、REPLACE、DELETE方法) + * 该方法使用 Statement 实现,请注意SQL注入风险! + * + * @param sql SQL语句内容 + * @return 更新的行数 + * @see SQLUpdateAction + */ + @Nullable Integer executeSQL(String sql); + + /** + * 执行一条不需要返回结果的预处理SQL更改(UPDATE、REPLACE、DELETE) + * + * @param sql SQL语句内容 + * @param params SQL语句中 ? 的对应参数 + * @return 更新的行数 + * @see PreparedSQLUpdateAction + */ + @Nullable Integer executeSQL(String sql, Object[] params); + + /** + * 执行多条不需要返回结果的SQL更改(UPDATE、REPLACE、DELETE) + * + * @param sql SQL语句内容 + * @param paramsBatch SQL语句中对应?的参数组 + * @return 对应参数返回的行数 + * @see PreparedSQLUpdateBatchAction + */ + @Nullable List executeSQLBatch(String sql, Iterable paramsBatch); + + + /** + * 执行多条不需要返回结果的SQL。 + * 该方法使用 Statement 实现,请注意SQL注入风险! + * + * @param sql SQL语句内容 + * @param moreSQL 更多SQL语句内容 + * @return 对应参数返回的行数 + * @see SQLUpdateBatchAction + */ + @Nullable List executeSQLBatch(@NotNull String sql, String... moreSQL); + + /** + * 执行多条不需要返回结果的SQL。 + * + * @param sqlBatch SQL语句内容 + * @return 对应参数返回的行数 + */ + @Nullable List executeSQLBatch(@NotNull Iterable sqlBatch); + + /** + * 获取并操作 {@link DatabaseMetaData} 以得到需要的数据库消息。 + * + * @param reader 操作与读取的方法 + * @param 最终结果的返回类型 + * @return 最终结果,通过 {@link CompletableFuture#get()} 可阻塞并等待结果返回。 + */ + default CompletableFuture fetchMetadata(@NotNull SQLFunction reader) { + return fetchMetadata((meta, conn) -> reader.apply(meta)); + } + + /** + * 获取并操作 {@link DatabaseMetaData} 提供的指定 {@link ResultSet} 以得到需要的数据库消息。 + *
该方法会自动关闭 {@link ResultSet} 。 + * + * @param supplier 操作 {@link DatabaseMetaData} 以提供信息所在的 {@link ResultSet} + * @param reader 读取 {@link ResultSet} 中指定信息的方法 + * @param 最终结果的返回类型 + * @return 最终结果,通过 {@link CompletableFuture#get()} 可阻塞并等待结果返回。 + * @throws NullPointerException 当 supplier 提供的 {@link ResultSet} 为NULL时抛出 + */ + default CompletableFuture fetchMetadata(@NotNull SQLFunction supplier, + @NotNull SQLFunction<@NotNull ResultSet, R> reader) { + return fetchMetadata((meta, conn) -> supplier.apply(meta), reader); + } + + /** + * 获取并操作 {@link DatabaseMetaData} 以得到需要的数据库消息。 + * + * @param reader 操作与读取的方法 + * @param 最终结果的返回类型 + * @return 最终结果,通过 {@link CompletableFuture#get()} 可阻塞并等待结果返回。 + */ + CompletableFuture fetchMetadata(@NotNull SQLBiFunction reader); + + /** + * 获取并操作 {@link DatabaseMetaData} 提供的指定 {@link ResultSet} 以得到需要的数据库消息。 + *
该方法会自动关闭 {@link ResultSet} 。 + * + * @param supplier 操作 {@link DatabaseMetaData} 以提供信息所在的 {@link ResultSet} + * @param reader 读取 {@link ResultSet} 中指定信息的方法 + * @param 最终结果的返回类型 + * @return 最终结果,通过 {@link CompletableFuture#get()} 可阻塞并等待结果返回。 + * @throws NullPointerException 当 supplier 提供的 {@link ResultSet} 为NULL时抛出 + */ + CompletableFuture fetchMetadata(@NotNull SQLBiFunction supplier, + @NotNull SQLFunction<@NotNull ResultSet, R> reader); + + /** + * 在库中创建一个表。 + * + * @param tableName 表名 + * @return {@link TableCreateBuilder} + */ + TableCreateBuilder createTable(@NotNull String tableName); + + /** + * 对库中的某个表执行更改。 + * + * @param tableName 表名 + * @return {@link TableAlterBuilder} + */ + TableAlterBuilder alterTable(@NotNull String tableName); + + /** + * 快速获取表的部分元数据。 + *
当需要获取其他元数据时,请使用 {@link #fetchMetadata(SQLFunction, SQLFunction)} 方法。 + * + * @param tablePattern 表名通配符 + * @return {@link TableMetadataBuilder} + */ + TableMetadataBuilder fetchTableMetadata(@NotNull String tablePattern); + + /** + * 新建一个查询。 + * + * @return {@link QueryBuilder} + */ + QueryBuilder createQuery(); + + /** + * 创建一条插入操作。 + * + * @param tableName 目标表名 + * @return {@link InsertBuilder} + */ + InsertBuilder> createInsert(@NotNull String tableName); + + /** + * 创建支持多组数据的插入操作。 + * + * @param tableName 目标表名 + * @return {@link InsertBuilder} + */ + InsertBuilder> createInsertBatch(@NotNull String tableName); + + /** + * 创建一条替换操作。 + * + * @param tableName 目标表名 + * @return {@link ReplaceBuilder} + */ + ReplaceBuilder> createReplace(@NotNull String tableName); + + /** + * 创建支持多组数据的替换操作。 + * + * @param tableName 目标表名 + * @return {@link ReplaceBuilder} + */ + ReplaceBuilder> createReplaceBatch(@NotNull String tableName); + + /** + * 创建更新操作。 + * + * @param tableName 目标表名 + * @return {@link UpdateBuilder} + */ + UpdateBuilder createUpdate(@NotNull String tableName); + + /** + * 创建删除操作。 + * + * @param tableName 目标表名 + * @return {@link DeleteBuilder} + */ + DeleteBuilder createDelete(@NotNull String tableName); + +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/sql/api/SQLQuery.java b/src/main/java/com/io/yutian/elementoriginlib/sql/api/SQLQuery.java new file mode 100644 index 0000000..1bf3a7c --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/sql/api/SQLQuery.java @@ -0,0 +1,75 @@ +package com.io.yutian.elementoriginlib.sql.api; + +import com.io.yutian.elementoriginlib.sql.api.action.query.PreparedQueryAction; +import com.io.yutian.elementoriginlib.sql.api.action.query.QueryAction; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.concurrent.TimeUnit; + +/** + * SQLQuery 是一个查询中间接口,用于查询操作的封装。 + * + * @author CarmJos + */ +public interface SQLQuery extends AutoCloseable { + + /** + * 获取该查询创建的时间 + *
注意,此处获得的时间非时间戳毫秒数,仅用于计算耗时。 + * + * @return 创建时间 + */ + default long getExecuteTime() { + return getExecuteTime(TimeUnit.MILLISECONDS); + } + + /** + * 获取该查询创建的时间 + *
注意,此处获得的时间非时间戳毫秒数,仅用于计算耗时。 + * + * @param timeUnit 时间单位 + * @return 创建时间 + */ + long getExecuteTime(TimeUnit timeUnit); + + /** + * 得到承载该SQLQuery的对应{@link SQLManager} + * + * @return {@link SQLManager} + */ + SQLManager getManager(); + + /** + * 得到承载该SQLQuery的对应{@link QueryAction} + * + * @return {@link QueryAction} 或 {@link PreparedQueryAction} + */ + QueryAction getAction(); + + ResultSet getResultSet(); + + default boolean containsResult(String columnName) throws SQLException { + return getResultSet() != null && getResultSet().getObject(columnName) != null; + } + + /** + * 得到设定的SQL语句 + * + * @return SQL语句 + */ + String getSQLContent(); + + /** + * 关闭所有内容 + */ + @Override + void close(); + + Statement getStatement(); + + Connection getConnection(); + +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/sql/api/SQLTable.java b/src/main/java/com/io/yutian/elementoriginlib/sql/api/SQLTable.java new file mode 100644 index 0000000..a046ea7 --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/sql/api/SQLTable.java @@ -0,0 +1,149 @@ +package com.io.yutian.elementoriginlib.sql.api; + +import com.io.yutian.elementoriginlib.sql.api.action.PreparedSQLUpdateAction; +import com.io.yutian.elementoriginlib.sql.api.action.PreparedSQLUpdateBatchAction; +import com.io.yutian.elementoriginlib.sql.api.builder.*; +import com.io.yutian.elementoriginlib.sql.api.function.SQLHandler; +import com.io.yutian.elementoriginlib.sql.api.table.NamedSQLTable; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.sql.SQLException; +import java.util.Optional; + +/** + * SQLTable 基于 {@link TableCreateBuilder} 构建表,用于快速创建与该表相关的操作。 + *
    + *
  • 1. 调用 {@link NamedSQLTable#of(String, String[])} 方法创建一个 SQLTable 对象;
  • + *
  • 2. 在应用初始化阶段调用 {@link NamedSQLTable#create(SQLManager)} 方法初始化 SQLTable 对象;
  • + *
  • 3. 获取已创建的{@link NamedSQLTable} 实例,直接调用对应方法进行关于表的相关操作。
  • + *
+ * + * @author CarmJos + * @since 0.3.10 + */ +public interface SQLTable { + + static @NotNull NamedSQLTable of(@NotNull String tableName, @Nullable SQLHandler table) { + return new NamedSQLTable(tableName) { + @Override + public boolean create(@NotNull SQLManager sqlManager, String tablePrefix) throws SQLException { + if (this.manager == null) this.manager = sqlManager; + this.tablePrefix = tablePrefix; + + TableCreateBuilder tableBuilder = sqlManager.createTable(getTableName()); + if (table != null) table.accept(tableBuilder); + return tableBuilder.build().executeFunction(l -> l > 0, false); + } + }; + } + + static @NotNull NamedSQLTable of(@NotNull String tableName, @NotNull String[] columns) { + return of(tableName, columns, null); + } + + static @NotNull NamedSQLTable of(@NotNull String tableName, + @NotNull String[] columns, @Nullable String tableSettings) { + return of(tableName, builder -> { + builder.setColumns(columns); + if (tableSettings != null) builder.setTableSettings(tableSettings); + }); + } + + /** + * 以指定的 {@link SQLManager} 实例初始化并创建该表 + * + * @param sqlManager {@link SQLManager} 实例 + * @return 是否新创建了本表 (若已创建或创建失败则返回false) + * @throws SQLException 当数据库返回异常时抛出 + */ + boolean create(SQLManager sqlManager) throws SQLException; + + /** + * 得到 {@link #create(SQLManager)} 用于初始化本实例的 {@link SQLManager} 实例 + * + * @return {@link SQLManager} 实例 + */ + @Nullable SQLManager getSQLManager(); + + /** + * 得到本表表名,不得为空。 + * + * @return 本表表名 + */ + @NotNull String getTableName(); + + default @NotNull TableQueryBuilder createQuery() { + return Optional.ofNullable(getSQLManager()).map(this::createQuery) + .orElseThrow(() -> new NullPointerException("This table doesn't have a SQLManger.")); + } + + default @NotNull TableQueryBuilder createQuery(@NotNull SQLManager sqlManager) { + return sqlManager.createQuery().inTable(getTableName()); + } + + default @NotNull DeleteBuilder createDelete() { + return Optional.ofNullable(getSQLManager()).map(this::createDelete) + .orElseThrow(() -> new NullPointerException("This table doesn't have a SQLManger.")); + } + + default @NotNull DeleteBuilder createDelete(@NotNull SQLManager sqlManager) { + return sqlManager.createDelete(getTableName()); + } + + default @NotNull UpdateBuilder createUpdate() { + return Optional.ofNullable(getSQLManager()).map(this::createUpdate) + .orElseThrow(() -> new NullPointerException("This table doesn't have a SQLManger.")); + } + + default @NotNull UpdateBuilder createUpdate(@NotNull SQLManager sqlManager) { + return sqlManager.createUpdate(getTableName()); + } + + default @NotNull InsertBuilder> createInsert() { + return Optional.ofNullable(getSQLManager()).map(this::createInsert) + .orElseThrow(() -> new NullPointerException("This table doesn't have a SQLManger.")); + } + + default @NotNull InsertBuilder> createInsert(@NotNull SQLManager sqlManager) { + return sqlManager.createInsert(getTableName()); + } + + default @NotNull InsertBuilder> createInsertBatch() { + return Optional.ofNullable(getSQLManager()).map(this::createInsertBatch) + .orElseThrow(() -> new NullPointerException("This table doesn't have a SQLManger.")); + } + + default @NotNull InsertBuilder> createInsertBatch(@NotNull SQLManager sqlManager) { + return sqlManager.createInsertBatch(getTableName()); + } + + default @NotNull ReplaceBuilder> createReplace() { + return Optional.ofNullable(getSQLManager()).map(this::createReplace) + .orElseThrow(() -> new NullPointerException("This table doesn't have a SQLManger.")); + + } + + default @NotNull ReplaceBuilder> createReplace(@NotNull SQLManager sqlManager) { + return sqlManager.createReplace(getTableName()); + } + + default @NotNull ReplaceBuilder> createReplaceBatch() { + return Optional.ofNullable(getSQLManager()).map(this::createReplaceBatch) + .orElseThrow(() -> new NullPointerException("This table doesn't have a SQLManger.")); + } + + default @NotNull ReplaceBuilder> createReplaceBatch(@NotNull SQLManager sqlManager) { + return sqlManager.createReplaceBatch(getTableName()); + } + + default @NotNull TableAlterBuilder alter() { + return Optional.ofNullable(getSQLManager()).map(this::alter) + .orElseThrow(() -> new NullPointerException("This table doesn't have a SQLManger.")); + } + + default @NotNull TableAlterBuilder alter(@NotNull SQLManager sqlManager) { + return sqlManager.alterTable(getTableName()); + } + +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/sql/api/action/PreparedSQLUpdateAction.java b/src/main/java/com/io/yutian/elementoriginlib/sql/api/action/PreparedSQLUpdateAction.java new file mode 100644 index 0000000..cd68638 --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/sql/api/action/PreparedSQLUpdateAction.java @@ -0,0 +1,24 @@ +package com.io.yutian.elementoriginlib.sql.api.action; + +import org.jetbrains.annotations.Nullable; + +public interface PreparedSQLUpdateAction extends SQLUpdateAction { + + /** + * 设定SQL语句中所有 ? 对应的参数 + * + * @param params 参数内容 + * @return {@link PreparedSQLUpdateAction} + */ + PreparedSQLUpdateAction setParams(Object... params); + + /** + * 设定SQL语句中所有 ? 对应的参数 + * + * @param params 参数内容 + * @return {@link PreparedSQLUpdateAction} + * @since 0.4.0 + */ + PreparedSQLUpdateAction setParams(@Nullable Iterable params); + +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/sql/api/action/PreparedSQLUpdateBatchAction.java b/src/main/java/com/io/yutian/elementoriginlib/sql/api/action/PreparedSQLUpdateBatchAction.java new file mode 100644 index 0000000..3673636 --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/sql/api/action/PreparedSQLUpdateBatchAction.java @@ -0,0 +1,42 @@ +package com.io.yutian.elementoriginlib.sql.api.action; + +import com.io.yutian.elementoriginlib.sql.api.SQLAction; + +import java.util.List; + +public interface PreparedSQLUpdateBatchAction extends SQLAction> { + + /** + * 设定多组SQL语句中所有 ? 对应的参数 + * + * @param allParams 所有参数内容 + * @return {@link PreparedSQLUpdateBatchAction} + */ + PreparedSQLUpdateBatchAction setAllParams(Iterable allParams); + + /** + * 添加一组SQL语句中所有 ? 对应的参数 + * + * @param params 参数内容 + * @return {@link PreparedSQLUpdateBatchAction} + */ + PreparedSQLUpdateBatchAction addParamsBatch(Object... params); + + /** + * 设定该操作返回自增键序列。 + * + * @return {@link SQLUpdateAction} + */ + PreparedSQLUpdateBatchAction returnGeneratedKeys(); + + /** + * 设定该操作返回自增键序列。 + * + * @param keyTypeClass 自增序列的数字类型 + * @param 自增键序列类型 {@link Number} + * @return {@link SQLUpdateAction} + * @since 0.4.0 + */ + PreparedSQLUpdateBatchAction returnGeneratedKeys(Class keyTypeClass); + +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/sql/api/action/SQLUpdateAction.java b/src/main/java/com/io/yutian/elementoriginlib/sql/api/action/SQLUpdateAction.java new file mode 100644 index 0000000..714fe50 --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/sql/api/action/SQLUpdateAction.java @@ -0,0 +1,26 @@ +package com.io.yutian.elementoriginlib.sql.api.action; + +import com.io.yutian.elementoriginlib.sql.api.SQLAction; + +public interface SQLUpdateAction extends SQLAction { + + + /** + * 设定该操作返回自增键序列。 + * + * @return {@link SQLUpdateAction} + */ + SQLUpdateAction returnGeneratedKey(); + + /** + * 设定该操作返回自增键序列。 + * + * @param keyTypeClass 自增序列的数字类型 + * @param 自增键序列类型 {@link Number} + * @return {@link SQLUpdateAction} + * @since 0.4.0 + */ + SQLUpdateAction returnGeneratedKey(Class keyTypeClass); + + +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/sql/api/action/SQLUpdateBatchAction.java b/src/main/java/com/io/yutian/elementoriginlib/sql/api/action/SQLUpdateBatchAction.java new file mode 100644 index 0000000..c2123f6 --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/sql/api/action/SQLUpdateBatchAction.java @@ -0,0 +1,27 @@ +package com.io.yutian.elementoriginlib.sql.api.action; + +import com.io.yutian.elementoriginlib.sql.api.SQLAction; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +@SuppressWarnings("UnusedReturnValue") +public interface SQLUpdateBatchAction extends SQLAction> { + + /** + * 添加一条批量执行的SQL语句 + * + * @param sql SQL语句 + * @return {@link SQLUpdateBatchAction} + */ + SQLUpdateBatchAction addBatch(@NotNull String sql); + + @Override + default @NotNull String getSQLContent() { + return getSQLContents().get(0); + } + + @Override + @NotNull List getSQLContents(); + +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/sql/api/action/query/PreparedQueryAction.java b/src/main/java/com/io/yutian/elementoriginlib/sql/api/action/query/PreparedQueryAction.java new file mode 100644 index 0000000..eef4f96 --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/sql/api/action/query/PreparedQueryAction.java @@ -0,0 +1,35 @@ +package com.io.yutian.elementoriginlib.sql.api.action.query; + +import org.jetbrains.annotations.Nullable; + +import java.sql.PreparedStatement; +import java.util.function.Consumer; + +public interface PreparedQueryAction extends QueryAction { + + /** + * 设定SQL语句中所有 ? 对应的参数 + * + * @param params 参数内容 + * @return {@link PreparedQueryAction} + */ + PreparedQueryAction setParams(@Nullable Object... params); + + /** + * 设定SQL语句中所有 ? 对应的参数 + * + * @param params 参数内容 + * @return {@link PreparedQueryAction} + */ + PreparedQueryAction setParams(@Nullable Iterable params); + + /** + * 直接对 {@link PreparedStatement} 进行处理 + * + * @param statement {@link Consumer} 处理操作 + * 若为空则不进行处理 + * @return {@link PreparedQueryAction} + */ + PreparedQueryAction handleStatement(@Nullable Consumer statement); + +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/sql/api/action/query/QueryAction.java b/src/main/java/com/io/yutian/elementoriginlib/sql/api/action/query/QueryAction.java new file mode 100644 index 0000000..9d22f81 --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/sql/api/action/query/QueryAction.java @@ -0,0 +1,45 @@ +package com.io.yutian.elementoriginlib.sql.api.action.query; + +import com.io.yutian.elementoriginlib.sql.api.SQLAction; +import com.io.yutian.elementoriginlib.sql.api.SQLQuery; +import com.io.yutian.elementoriginlib.sql.api.function.SQLExceptionHandler; +import com.io.yutian.elementoriginlib.sql.api.function.SQLFunction; +import com.io.yutian.elementoriginlib.sql.api.function.SQLHandler; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.sql.SQLException; + +/** + * SQLQueryAction 是用于承载SQL查询语句并进行处理、返回并自动关闭连接的基本类。 + * + *
    + *
  • 同步执行 {@link #execute()}, {@link #execute(SQLFunction, SQLExceptionHandler)} + *
    同步执行方法中有会抛出异常的方法与不抛出异常的方法, + *
    若选择不抛出异常,则返回值可能为空,需要特殊处理。
  • + * + *
  • 异步执行 {@link #executeAsync(SQLHandler, SQLExceptionHandler)} + *
    异步执行时将提供成功与异常两种处理方式 + *
    可自行选择是否对数据或异常进行处理 + *
    默认的异常处理器为 {@link #defaultExceptionHandler()}
  • + *
+ * + * 注意: 无论是否异步,都不需要自行关闭ResultSet,本API已自动关闭 + * + * @author CarmJos + * @since 0.2.6 + */ +public interface QueryAction extends SQLAction { + + @Override + @Contract("_,!null -> !null") + default @Nullable R executeFunction(@NotNull SQLFunction<@NotNull SQLQuery, R> function, + @Nullable R defaultResult) throws SQLException { + try (SQLQuery value = execute()) { + R result = function.apply(value); + return result == null ? defaultResult : result; + } + } + +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/sql/api/builder/ConditionalBuilder.java b/src/main/java/com/io/yutian/elementoriginlib/sql/api/builder/ConditionalBuilder.java new file mode 100644 index 0000000..3abd450 --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/sql/api/builder/ConditionalBuilder.java @@ -0,0 +1,82 @@ +package com.io.yutian.elementoriginlib.sql.api.builder; + +import com.io.yutian.elementoriginlib.sql.api.SQLAction; +import com.io.yutian.elementoriginlib.sql.api.SQLBuilder; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Date; +import java.util.LinkedHashMap; + +public interface ConditionalBuilder, T extends SQLAction> extends SQLBuilder { + + /** + * 将现有条件构建完整的SQL语句用于执行。 + * + * @return {@link SQLAction} + */ + T build(); + + /** + * 设定限定的条目数 + * + * @param limit 条数限制 + * @return {@link B} + */ + B setLimit(int limit); + + /** + * 直接设定条件的源文本,不需要以WHERE开头。 + *
如 {@code id = 1 AND name = 'test' OR name = 'test2'} 。 + * + * @param condition 条件文本,不需要以WHERE开头。 + * @return {@link B} + */ + B setConditions(@Nullable String condition); + + /** + * 直接设定每个条件的文本与其对应数值,将以AND链接,且不需要以WHERE开头。 + *
条件如 {@code id = ? },问号将被以对应的数值填充。。 + * + * @param conditionSQLs 条件内容,将以AND链接,且不需要以WHERE开头。 + * @return {@link B} + */ + B setConditions(LinkedHashMap<@NotNull String, @Nullable Object> conditionSQLs); + + B addCondition(@Nullable String condition); + + B addCondition(@NotNull String columnName, @NotNull String operator, @Nullable Object queryValue); + + B addCondition(@NotNull String columnName, @Nullable Object queryValue); + + B addCondition(@NotNull String[] columnNames, @Nullable Object[] queryValues); + + B addNotNullCondition(@NotNull String columnName); + + /** + * 添加时间的限定条件。 若设定了开始时间,则限定条件为 {@code endMillis >= startMillis}; + * + * @param columnName 判断的行 + * @param startMillis 开始时间戳,若{@code <0}则不作限定 + * @param endMillis 结束时间戳,若{@code <0}则不作限定 + * @return {@link B} + */ + default B addTimeCondition(@NotNull String columnName, long startMillis, long endMillis) { + return addTimeCondition(columnName, + startMillis > 0 ? new Date(startMillis) : null, + endMillis > 0 ? new Date(endMillis) : null + ); + } + + /** + * 添加时间的限定条件。 若设定了开始时间,则限定条件为 {@code endDate >= startTime}; + * + * @param columnName 判断的行 + * @param startDate 开始时间,若为null则不作限定 + * @param endDate 结束时间,若为null则不作限定 + * @return {@link B} + */ + B addTimeCondition(@NotNull String columnName, @Nullable Date startDate, @Nullable Date endDate); + + +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/sql/api/builder/DeleteBuilder.java b/src/main/java/com/io/yutian/elementoriginlib/sql/api/builder/DeleteBuilder.java new file mode 100644 index 0000000..db6090e --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/sql/api/builder/DeleteBuilder.java @@ -0,0 +1,9 @@ +package com.io.yutian.elementoriginlib.sql.api.builder; + +import com.io.yutian.elementoriginlib.sql.api.SQLAction; + +public interface DeleteBuilder extends ConditionalBuilder> { + + String getTableName(); + +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/sql/api/builder/InsertBuilder.java b/src/main/java/com/io/yutian/elementoriginlib/sql/api/builder/InsertBuilder.java new file mode 100644 index 0000000..5d233bf --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/sql/api/builder/InsertBuilder.java @@ -0,0 +1,23 @@ +package com.io.yutian.elementoriginlib.sql.api.builder; + +import com.io.yutian.elementoriginlib.sql.api.SQLAction; + +import java.util.Arrays; +import java.util.List; + +public interface InsertBuilder> { + + String getTableName(); + + boolean isIgnore(); + + InsertBuilder setIgnore(boolean ignore); + + T setColumnNames(List columnNames); + + default T setColumnNames(String... columnNames) { + return setColumnNames(columnNames == null ? null : Arrays.asList(columnNames)); + } + + +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/sql/api/builder/QueryBuilder.java b/src/main/java/com/io/yutian/elementoriginlib/sql/api/builder/QueryBuilder.java new file mode 100644 index 0000000..5348627 --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/sql/api/builder/QueryBuilder.java @@ -0,0 +1,37 @@ +package com.io.yutian.elementoriginlib.sql.api.builder; + +import com.io.yutian.elementoriginlib.sql.api.SQLBuilder; +import com.io.yutian.elementoriginlib.sql.api.action.query.PreparedQueryAction; +import com.io.yutian.elementoriginlib.sql.api.action.query.QueryAction; +import org.jetbrains.annotations.NotNull; + +public interface QueryBuilder extends SQLBuilder { + + /** + * 通过一条 SQL语句创建查询。 + * 该方法使用 Statement 实现,请注意SQL注入风险! + * + * @param sql SQL语句 + * @return {@link QueryAction} + * @deprecated 存在SQL注入风险,建议使用 {@link QueryBuilder#withPreparedSQL(String)} + */ + @Deprecated + QueryAction withSQL(@NotNull String sql); + + /** + * 通过一条 SQL语句创建预查询 + * + * @param sql SQL语句 + * @return {@link PreparedQueryAction} + */ + PreparedQueryAction withPreparedSQL(@NotNull String sql); + + /** + * 创建表查询 + * + * @param tableName 表名 + * @return {@link TableQueryBuilder} + */ + TableQueryBuilder inTable(@NotNull String tableName); + +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/sql/api/builder/ReplaceBuilder.java b/src/main/java/com/io/yutian/elementoriginlib/sql/api/builder/ReplaceBuilder.java new file mode 100644 index 0000000..5b66b3c --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/sql/api/builder/ReplaceBuilder.java @@ -0,0 +1,25 @@ +package com.io.yutian.elementoriginlib.sql.api.builder; + +import com.io.yutian.elementoriginlib.sql.api.SQLAction; + +import java.util.Arrays; +import java.util.List; + +/** + * REPLACE 语句用于将一组值更新进数据表中。 + *
执行后,将通过表中键判断该数据是否存在,若存在则用新数据替换原来的值,若不存在则会插入该数据。 + *
在使用REPLACE时,表与所给行列数据中必须包含唯一索引(或主键),且索引不得为空值,否则将等同于插入语句。 + * + * @param 最终构建出的 {@link SQLAction} 类型 + */ +public interface ReplaceBuilder> { + + String getTableName(); + + T setColumnNames(List columnNames); + + default T setColumnNames(String... columnNames) { + return setColumnNames(columnNames == null ? null : Arrays.asList(columnNames)); + } + +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/sql/api/builder/TableAlterBuilder.java b/src/main/java/com/io/yutian/elementoriginlib/sql/api/builder/TableAlterBuilder.java new file mode 100644 index 0000000..bdef7a5 --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/sql/api/builder/TableAlterBuilder.java @@ -0,0 +1,129 @@ +package com.io.yutian.elementoriginlib.sql.api.builder; + +import com.io.yutian.elementoriginlib.sql.api.SQLAction; +import com.io.yutian.elementoriginlib.sql.api.SQLBuilder; +import com.io.yutian.elementoriginlib.sql.api.action.SQLUpdateAction; +import com.io.yutian.elementoriginlib.sql.api.enums.IndexType; +import com.io.yutian.elementoriginlib.sql.api.enums.NumberType; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public interface TableAlterBuilder extends SQLBuilder { + + SQLAction renameTo(@NotNull String newTableName); + + SQLAction changeComment(@NotNull String newTableComment); + + SQLAction setAutoIncrementIndex(int index); + + SQLAction addIndex(@NotNull IndexType indexType, @Nullable String indexName, + @NotNull String columnName, @NotNull String... moreColumns); + + /** + * 为该表移除一个索引 + * + * @param indexName 索引名 + * @return {@link SQLUpdateAction} + */ + SQLAction dropIndex(@NotNull String indexName); + + /** + * 为该表移除一个外键 + * + * @param keySymbol 外键名 + * @return {@link SQLUpdateAction} + */ + SQLAction dropForeignKey(@NotNull String keySymbol); + + /** + * 为该表移除主键(须添加新主键) + * + * @return {@link SQLUpdateAction} + */ + SQLAction dropPrimaryKey(); + + /** + * 为表添加一列 + * + * @param columnName 列名 + * @param settings 列的相关设定 + * @return {@link SQLUpdateAction} + */ + default SQLAction addColumn(@NotNull String columnName, @NotNull String settings) { + return addColumn(columnName, settings, null); + } + + /** + * 为表添加一列 + * + * @param columnName 列名 + * @param settings 列的相关设定 + * @param afterColumn 该列增添到哪个列的后面, + *

该参数若省缺则放于最后一行 + *

若为 "" 则置于首行。 + * @return {@link SQLUpdateAction} + */ + SQLAction addColumn(@NotNull String columnName, @NotNull String settings, @Nullable String afterColumn); + + SQLAction renameColumn(@NotNull String columnName, @NotNull String newName); + + SQLAction modifyColumn(@NotNull String columnName, @NotNull String settings); + + default SQLAction modifyColumn(@NotNull String columnName, @NotNull String columnSettings, @NotNull String afterColumn) { + return modifyColumn(columnName, columnSettings + " AFTER `" + afterColumn + "`"); + } + + SQLAction removeColumn(@NotNull String columnName); + + SQLAction setColumnDefault(@NotNull String columnName, @NotNull String defaultValue); + + SQLAction removeColumnDefault(@NotNull String columnName); + + /** + * 为该表添加一个自增列 + *

自增列强制要求为数字类型,非空,且为UNIQUE。 + *

注意:一个表只允许有一个自增列! + * + * @param columnName 列名 + * @param numberType 数字类型,若省缺则为 {@link NumberType#INT} + * @param primary 是否为主键,若否则只为唯一键 + * @param unsigned 是否采用 UNSIGNED (即无负数,可以增加自增键的最高数,建议为true) + * @return {@link TableCreateBuilder} + */ + default SQLAction addAutoIncrementColumn(@NotNull String columnName, @Nullable NumberType numberType, + boolean primary, boolean unsigned) { + return addColumn(columnName, + (numberType == null ? NumberType.INT : numberType).name() + + (unsigned ? " UNSIGNED " : " ") + + "NOT NULL AUTO_INCREMENT " + (primary ? "PRIMARY KEY" : "UNIQUE KEY"), + "" + ); + } + + /** + * 为该表添加一个自增列 + *
自增列强制要求为数字类型,非空,且为UNIQUE。 + *

注意:一个表只允许有一个自增列! + * + * @param columnName 列名 + * @param numberType 数字类型,若省缺则为 {@link NumberType#INT} + * @return {@link TableAlterBuilder} + */ + default SQLAction addAutoIncrementColumn(@NotNull String columnName, @NotNull NumberType numberType) { + return addAutoIncrementColumn(columnName, numberType, false, true); + } + + + /** + * 为该表添加一个自增列 + *
自增列强制要求为数字类型,非空,且为UNIQUE。 + *

注意:一个表只允许有一个自增列! + * + * @param columnName 列名 + * @return {@link TableAlterBuilder} + */ + default SQLAction addAutoIncrementColumn(@NotNull String columnName) { + return addAutoIncrementColumn(columnName, NumberType.INT); + } + +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/sql/api/builder/TableCreateBuilder.java b/src/main/java/com/io/yutian/elementoriginlib/sql/api/builder/TableCreateBuilder.java new file mode 100644 index 0000000..c9368d4 --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/sql/api/builder/TableCreateBuilder.java @@ -0,0 +1,256 @@ +package com.io.yutian.elementoriginlib.sql.api.builder; + +import com.io.yutian.elementoriginlib.sql.api.SQLBuilder; +import com.io.yutian.elementoriginlib.sql.api.action.SQLUpdateAction; +import com.io.yutian.elementoriginlib.sql.api.enums.ForeignKeyRule; +import com.io.yutian.elementoriginlib.sql.api.enums.IndexType; +import com.io.yutian.elementoriginlib.sql.api.enums.NumberType; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Objects; + +import static com.io.yutian.elementoriginlib.sql.api.SQLBuilder.withBackQuote; +import static com.io.yutian.elementoriginlib.sql.api.SQLBuilder.withQuote; + + +public interface TableCreateBuilder extends SQLBuilder { + + /** + * 将现有条件构建完整的SQL语句用于执行。 + * + * @return {@link SQLUpdateAction} + */ + SQLUpdateAction build(); + + @NotNull String getTableName(); + + /** + * 得到表的设定。 + *

若未使用 {@link #setTableSettings(String)} 方法则会采用 {@link #defaultTablesSettings()} 。 + * + * @return TableSettings + */ + @NotNull String getTableSettings(); + + TableCreateBuilder setTableSettings(@NotNull String settings); + + /** + * 设定表的标注,一般用于解释该表的作用。 + * + * @param comment 表标注 + * @return {@link TableCreateBuilder} + */ + TableCreateBuilder setTableComment(@Nullable String comment); + + /** + * 直接设定表的所有列信息 + * + * @param columns 列的相关信息 (包括列设定) + * @return {@link TableCreateBuilder} + */ + TableCreateBuilder setColumns(@NotNull String... columns); + + /** + * 为该表添加一个列 + * + * @param column 列的相关信息 + *
如 `uuid` VARCHAR(36) NOT NULL UNIQUE KEY + * @return {@link TableCreateBuilder} + */ + TableCreateBuilder addColumn(@NotNull String column); + + /** + * 为该表添加一个列 + * + * @param columnName 列名 + * @param settings 列的设定 + *
如 VARCHAR(36) NOT NULL UNIQUE KEY + * @return {@link TableCreateBuilder} + */ + default TableCreateBuilder addColumn(@NotNull String columnName, @NotNull String settings) { + Objects.requireNonNull(columnName, "columnName could not be null"); + return addColumn(withBackQuote(columnName) + " " + settings); + } + + /** + * 为该表添加一个列 + * + * @param columnName 列名 + * @param settings 列的设定 + *
如 VARCHAR(36) NOT NULL UNIQUE KEY + * @param comments 列的注解,用于解释该列数据的作用 + * @return {@link TableCreateBuilder} + */ + default TableCreateBuilder addColumn(@NotNull String columnName, @NotNull String settings, @NotNull String comments) { + return addColumn(columnName, settings + " COMMENT " + withQuote(comments)); + } + + /** + * 为该表添加一个自增列 + *

自增列强制要求为数字类型,非空,且为UNIQUE。 + *

注意:一个表只允许有一个自增列! + * + * @param columnName 列名 + * @param numberType 数字类型,若省缺则为 {@link NumberType#INT} + * @param asPrimaryKey 是否为主键,若为false则设定为唯一键 + * @param unsigned 是否采用 UNSIGNED (即无负数,可以增加自增键的最高数,建议为true) + * @return {@link TableCreateBuilder} + */ + TableCreateBuilder addAutoIncrementColumn(@NotNull String columnName, @Nullable NumberType numberType, + boolean asPrimaryKey, boolean unsigned); + + /** + * 为该表添加一个INT类型的自增主键列 + *

自增列强制要求为数字类型,非空,且为UNIQUE。 + *

注意:一个表只允许有一个自增列! + * + * @param columnName 列名 + * @param asPrimaryKey 是否为主键,若为false则设定为唯一键 + * @param unsigned 是否采用 UNSIGNED (即无负数,可以增加自增键的最高数,建议为true) + * @return {@link TableCreateBuilder} + */ + default TableCreateBuilder addAutoIncrementColumn(@NotNull String columnName, + boolean asPrimaryKey, boolean unsigned) { + return addAutoIncrementColumn(columnName, NumberType.INT, asPrimaryKey, unsigned); + } + + + /** + * 为该表添加一个INT类型的自增列 + *

自增列强制要求为数字类型,非空,且为UNIQUE。 + *

注意:一个表只允许有一个自增列! + * + * @param columnName 列名 + * @param asPrimaryKey 是否为主键,若为false则设定为唯一键 + * @return {@link TableCreateBuilder} + */ + default TableCreateBuilder addAutoIncrementColumn(@NotNull String columnName, boolean asPrimaryKey) { + return addAutoIncrementColumn(columnName, asPrimaryKey, true); + } + + /** + * 为该表添加一个INT类型的自增主键列 + *

自增列强制要求为数字类型,非空,且为UNIQUE。 + *

注意:一个表只允许有一个自增列! + * + * @param columnName 列名 + * @return {@link TableCreateBuilder} + */ + default TableCreateBuilder addAutoIncrementColumn(@NotNull String columnName) { + return addAutoIncrementColumn(columnName, true); + } + + /** + * 设定表中的某列为索引或键。 + * + *

创建索引时,你需要确保该索引是应用在 SQL 查询语句的条件(一般作为 WHERE 子句的条件)。 + *
虽然索引大大提高了查询速度,同时却会降低更新表的速度,如对表进行INSERT、UPDATE 和DELETE。 + *
因此,请合理的设计索引。 + * + * @param type 索引类型 + * @param columnName 索引包含的列 + * @return {@link TableCreateBuilder} + */ + default TableCreateBuilder setIndex(@NotNull String columnName, + @NotNull IndexType type) { + return setIndex(type, null, columnName); + } + + /** + * 设定表中的某列为索引或键。 + * + *

创建索引时,你需要确保该索引是应用在 SQL 查询语句的条件(一般作为 WHERE 子句的条件)。 + *
虽然索引大大提高了查询速度,同时却会降低更新表的速度,如对表进行INSERT、UPDATE 和DELETE。 + *
因此,请合理的设计索引。 + * + * @param type 索引类型 + * @param indexName 索引名称,缺省时将根据第一个索引列赋一个名称 + * @param columnName 索引包含的列 + * @param moreColumns 联合索引需要包含的列 + * @return {@link TableCreateBuilder} + */ + TableCreateBuilder setIndex(@NotNull IndexType type, @Nullable String indexName, + @NotNull String columnName, @NotNull String... moreColumns); + + + /** + * 以本表位从表,为表中某列设定自参照外键(即自参照完整性)。 + * + *

外键约束(FOREIGN KEY)是表的一个特殊字段,经常与主键约束一起使用。 + *
外键用来建立主表与从表的关联关系,为两个表的数据建立连接,约束两个表中数据的一致性和完整性。 + *
主表删除某条记录时,从表中与之对应的记录也必须有相应的改变。 + * + * @param tableColumn 本表中的列 + * @param foreignColumn 外键关联表中对应的关联列,必须为目标表的主键,即 {@link IndexType#PRIMARY_KEY} + * @return {@link TableCreateBuilder} + */ + default TableCreateBuilder addForeignKey(@NotNull String tableColumn, @NotNull String foreignColumn) { + return addForeignKey(tableColumn, getTableName(), foreignColumn); + } + + /** + * 以本表位从表,为表中某列设定外键。 + * + *

外键约束(FOREIGN KEY)是表的一个特殊字段,经常与主键约束一起使用。 + *
外键用来建立主表与从表的关联关系,为两个表的数据建立连接,约束两个表中数据的一致性和完整性。 + *
主表删除某条记录时,从表中与之对应的记录也必须有相应的改变。 + * + * @param tableColumn 本表中的列 + * @param foreignTable 外键关联主表,必须为已存在的表或本表,且必须有主键。 + * @param foreignColumn 外键关联主表中对应的关联列,须满足 + *

1. 为主表的主键,即 {@link IndexType#PRIMARY_KEY} + *

2. 数据类型必须和所要建立主键的列的数据类型相同。 + * @return {@link TableCreateBuilder} + */ + default TableCreateBuilder addForeignKey(@NotNull String tableColumn, + @NotNull String foreignTable, @NotNull String foreignColumn) { + return addForeignKey(tableColumn, null, foreignTable, foreignColumn); + } + + /** + * 以本表位从表,为表中某列设定外键。 + * + *

外键约束(FOREIGN KEY)是表的一个特殊字段,经常与主键约束一起使用。 + *
外键用来建立主表与从表的关联关系,为两个表的数据建立连接,约束两个表中数据的一致性和完整性。 + *
主表删除某条记录时,从表中与之对应的记录也必须有相应的改变。 + * + * @param tableColumn 本表中的列 + * @param constraintName 约束名,缺省时将使用参数自动生成,如 fk_[tableColumn]_[foreignTable] + * @param foreignTable 外键关联主表,必须为已存在的表或本表,且必须有主键。 + * @param foreignColumn 外键关联主表中对应的关联列,须满足 + *

1. 为主表的主键,即 {@link IndexType#PRIMARY_KEY} + *

2. 数据类型必须和所要建立主键的列的数据类型相同。 + * @return {@link TableCreateBuilder} + */ + default TableCreateBuilder addForeignKey(@NotNull String tableColumn, @Nullable String constraintName, + @NotNull String foreignTable, @NotNull String foreignColumn) { + return addForeignKey(tableColumn, constraintName, foreignTable, foreignColumn, null, null); + } + + /** + * 以本表位从表,为表中某列设定外键。 + * + *

外键约束(FOREIGN KEY)是表的一个特殊字段,经常与主键约束一起使用。 + *
外键用来建立主表与从表的关联关系,为两个表的数据建立连接,约束两个表中数据的一致性和完整性。 + *
主表删除某条记录时,从表中与之对应的记录也必须有相应的改变。 + * + * @param tableColumn 本表中的列 + * @param constraintName 约束名,缺省时将使用参数自动生成,如 fk_[tableColumn]_[foreignTable] + * @param foreignTable 外键关联主表,必须为已存在的表或本表,且必须有主键。 + * @param foreignColumn 外键关联主表中对应的关联列,须满足 + *

1. 为主表的主键,即 {@link IndexType#PRIMARY_KEY} + *

2. 数据类型必须和所要建立主键的列的数据类型相同。 + * @param updateRule 在外键被更新时采用的规则,缺省时默认为{@link ForeignKeyRule#RESTRICT} + * @param deleteRule 在外键被删除时采用的规则,缺省时默认为{@link ForeignKeyRule#RESTRICT} + * @return {@link TableCreateBuilder} + */ + TableCreateBuilder addForeignKey(@NotNull String tableColumn, @Nullable String constraintName, + @NotNull String foreignTable, @NotNull String foreignColumn, + @Nullable ForeignKeyRule updateRule, @Nullable ForeignKeyRule deleteRule); + + default String defaultTablesSettings() { + return "ENGINE=InnoDB DEFAULT CHARSET=utf8"; + } + +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/sql/api/builder/TableMetadataBuilder.java b/src/main/java/com/io/yutian/elementoriginlib/sql/api/builder/TableMetadataBuilder.java new file mode 100644 index 0000000..5bcd922 --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/sql/api/builder/TableMetadataBuilder.java @@ -0,0 +1,56 @@ +package com.io.yutian.elementoriginlib.sql.api.builder; + +import com.io.yutian.elementoriginlib.sql.api.SQLBuilder; +import com.io.yutian.elementoriginlib.sql.api.function.SQLFunction; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.Unmodifiable; + +import java.sql.ResultSet; +import java.util.Set; +import java.util.concurrent.CompletableFuture; + +public interface TableMetadataBuilder extends SQLBuilder { + + /** + * @return 本表是否存在 + */ + CompletableFuture validateExist(); + + /** + * 对表内的数据列元数据进行读取 + * + * @param columnPattern 列的名称匹配表达式, 为空则匹配所有列 + * @param reader 读取的方法 + * @param 结果类型 + * @return 读取结果 + */ + CompletableFuture fetchColumns(@Nullable String columnPattern, + @NotNull SQLFunction reader); + + /** + * @param columnPattern 需要判断的列名表达式 + * @return 对应列是否存在 + */ + CompletableFuture isColumnExists(@NotNull String columnPattern); + + /** + * 列出所有表内的全部列。 + * + * @return 表内全部数据列的列名 + */ + default CompletableFuture<@Unmodifiable Set> listColumns() { + return listColumns(null); + } + + /** + * 列出所有满足表达式的列。 + * + * @param columnPattern 列名表达式,为空则列出全部 + * @return 所有满足表达式的列名 + */ + CompletableFuture<@Unmodifiable Set> listColumns(@Nullable String columnPattern); + + // More coming soon. + +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/sql/api/builder/TableQueryBuilder.java b/src/main/java/com/io/yutian/elementoriginlib/sql/api/builder/TableQueryBuilder.java new file mode 100644 index 0000000..f227418 --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/sql/api/builder/TableQueryBuilder.java @@ -0,0 +1,37 @@ +package com.io.yutian.elementoriginlib.sql.api.builder; + +import com.io.yutian.elementoriginlib.sql.api.action.query.PreparedQueryAction; +import org.jetbrains.annotations.NotNull; + +public interface TableQueryBuilder extends ConditionalBuilder { + + @NotNull String getTableName(); + + /** + * 选定用于查询的列名 + * + * @param columnNames 列名 + * @return {@link TableQueryBuilder} + */ + TableQueryBuilder selectColumns(@NotNull String... columnNames); + + /** + * 对结果进行排序 + * + * @param columnName 排序使用的列名 + * @param asc 是否为正序排序 (为false则倒序排序) + * @return {@link TableQueryBuilder} + */ + TableQueryBuilder orderBy(@NotNull String columnName, boolean asc); + + /** + * 限制查询条数,用于分页查询。 + * + * @param start 开始数 + * @param end 结束条数 + * @return {@link TableQueryBuilder} + * @since 0.2.6 + */ + TableQueryBuilder setPageLimit(int start, int end); + +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/sql/api/builder/UpdateBuilder.java b/src/main/java/com/io/yutian/elementoriginlib/sql/api/builder/UpdateBuilder.java new file mode 100644 index 0000000..c1c82a5 --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/sql/api/builder/UpdateBuilder.java @@ -0,0 +1,56 @@ +package com.io.yutian.elementoriginlib.sql.api.builder; + +import com.io.yutian.elementoriginlib.sql.api.SQLAction; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.LinkedHashMap; + +public interface UpdateBuilder extends ConditionalBuilder> { + + String getTableName(); + + /** + * 添加一条需要更新的字段名与值 + * + * @param columnName 字段名 + * @param columnValue 字段名对应的值 + * @return {@link UpdateBuilder} + * @since 0.3.7 + */ + UpdateBuilder addColumnValue(@NotNull String columnName, @Nullable Object columnValue); + + /** + * 设定更新的全部字段值 (此操作会覆盖之前的设定) + *

此操作会覆盖之前的设定 + * + * @param columnData 字段名和值的键值对 + * @return {@link UpdateBuilder} + */ + UpdateBuilder setColumnValues(LinkedHashMap<@NotNull String, @Nullable Object> columnData); + + /** + * 设定更新的全部字段值 (此操作会覆盖之前的设定) + *

此操作会覆盖之前的设定 + * + * @param columnNames 字段名 + * @param columnValues 字段名对应的值 + * @return {@link UpdateBuilder} + */ + UpdateBuilder setColumnValues(@NotNull String[] columnNames, @Nullable Object[] columnValues); + + /** + * 设定更新的全部字段值 (此操作会覆盖之前的设定) + *

如需同时更新多条字段,请使用 {@link #setColumnValues(String[], Object[])} 或 {@link #setColumnValues(LinkedHashMap)} + *
也可以使用 {@link #addColumnValue(String, Object)} 一条条的添加字段 + * + * @param columnName 字段名 + * @param columnValue 字段名对应的值 + * @return {@link UpdateBuilder} + */ + default UpdateBuilder setColumnValues(@NotNull String columnName, @Nullable Object columnValue) { + return setColumnValues(new String[]{columnName}, new Object[]{columnValue}); + } + + +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/sql/api/builder/UpsertBuilder.java b/src/main/java/com/io/yutian/elementoriginlib/sql/api/builder/UpsertBuilder.java new file mode 100644 index 0000000..3262529 --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/sql/api/builder/UpsertBuilder.java @@ -0,0 +1,17 @@ +package com.io.yutian.elementoriginlib.sql.api.builder; + +/** + * 存在则更新,不存在则插入。 + * + * @see ReplaceBuilder + */ +@Deprecated +public interface UpsertBuilder { + + String getTableName(); + + default UpsertBuilder setColumnNames(String[] columnNames, String updateColumn) { + throw new UnsupportedOperationException("Please use REPLACE ."); + } + +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/sql/api/enums/ForeignKeyRule.java b/src/main/java/com/io/yutian/elementoriginlib/sql/api/enums/ForeignKeyRule.java new file mode 100644 index 0000000..2b1cb4a --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/sql/api/enums/ForeignKeyRule.java @@ -0,0 +1,41 @@ +package com.io.yutian.elementoriginlib.sql.api.enums; + +public enum ForeignKeyRule { + + /** + * 啥也不做 + *

注意: 在Mysql中该选项实际上等同于采用默认的 {@link #RESTRICT} 设定! + */ + NO_ACTION("NO ACTION"), + + /** + * 拒绝删除要求,直到使用删除键值的辅助表被手工删除,并且没有参照时(这是默认设置,也是最安全的设置) + */ + RESTRICT("RESTRICT"), + + /** + * 修改包含与已删除键值有参照关系的所有记录,使用NULL值替换(只能用于已标记为NOT NULL的字段) + */ + SET_NULL("SET NULL"), + + /** + * 修改包含与已删除键值有参照关系的所有记录,使用默认值替换(只能用于设定了DEFAULT的字段) + */ + SET_DEFAULT("SET DEFAULT"), + + /** + * 级联删除,删除包含与已删除键值有参照关系的所有记录 + */ + CASCADE("CASCADE"); + + final String ruleName; + + ForeignKeyRule(String ruleName) { + this.ruleName = ruleName; + } + + public String getRuleName() { + return ruleName; + } + +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/sql/api/enums/IndexType.java b/src/main/java/com/io/yutian/elementoriginlib/sql/api/enums/IndexType.java new file mode 100644 index 0000000..5296875 --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/sql/api/enums/IndexType.java @@ -0,0 +1,41 @@ +package com.io.yutian.elementoriginlib.sql.api.enums; + +public enum IndexType { + + + /** + * 普通索引(由关键字KEY或INDEX定义的索引)的唯一任务是加快对数据的访问速度。 + *
因此,应该只为那些最经常出现在查询条件(WHERE column=)或排序条件(ORDER BY column)中的数据列创建索引。 + *
只要有可能,就应该选择一个数据最整齐、最紧凑的数据列(如一个整数类型的数据列)来创建索引。 + */ + INDEX("INDEX"), + + + /** + * 唯一索引 是在表上一个或者多个字段组合建立的索引,这个或者这些字段的值组合起来在表中不可以重复,用于保证数据的唯一性。 + */ + UNIQUE_KEY("UNIQUE KEY"), + + /** + * 主键索引 是唯一索引的特定类型。表中创建主键时自动创建的索引 。一个表只能建立一个主索引。 + */ + PRIMARY_KEY("PRIMARY KEY"), + + /** + * 全文索引 主要用来查找文本中的关键字,而不是直接与索引中的值相比较。 + *
请搭配 MATCH 等语句使用,而不是使用 WHERE - LIKE 。 + *
全文索引只可用于 CHAR、 VARCHAR 与 TEXT 系列类型。 + */ + FULLTEXT_INDEX("FULLTEXT"); + + + final String name; + + IndexType(String name) { + this.name = name; + } + + public String getName() { + return name; + } +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/sql/api/enums/NumberType.java b/src/main/java/com/io/yutian/elementoriginlib/sql/api/enums/NumberType.java new file mode 100644 index 0000000..3e65506 --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/sql/api/enums/NumberType.java @@ -0,0 +1,11 @@ +package com.io.yutian.elementoriginlib.sql.api.enums; + +public enum NumberType { + + TINYINT, + SMALLINT, + MEDIUMINT, + INT, + BIGINT + +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/sql/api/function/SQLBiFunction.java b/src/main/java/com/io/yutian/elementoriginlib/sql/api/function/SQLBiFunction.java new file mode 100644 index 0000000..7ae9f0a --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/sql/api/function/SQLBiFunction.java @@ -0,0 +1,24 @@ +package com.io.yutian.elementoriginlib.sql.api.function; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.sql.SQLException; +import java.util.Objects; + +@FunctionalInterface +public interface SQLBiFunction { + + @Nullable + R apply(@NotNull T t, @NotNull U u) throws SQLException; + + default SQLBiFunction then(@NotNull SQLFunction after) { + Objects.requireNonNull(after); + return (T t, U u) -> { + R r = apply(t, u); + if (r == null) return null; + else return after.apply(r); + }; + } + +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/sql/api/function/SQLDebugHandler.java b/src/main/java/com/io/yutian/elementoriginlib/sql/api/function/SQLDebugHandler.java new file mode 100644 index 0000000..a7783f8 --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/sql/api/function/SQLDebugHandler.java @@ -0,0 +1,100 @@ +package com.io.yutian.elementoriginlib.sql.api.function; + +import com.io.yutian.elementoriginlib.sql.api.SQLAction; +import com.io.yutian.elementoriginlib.sql.api.SQLQuery; +import com.io.yutian.elementoriginlib.sql.api.action.PreparedSQLUpdateAction; +import com.io.yutian.elementoriginlib.sql.api.action.PreparedSQLUpdateBatchAction; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * 异常处理器。 + *
在使用 {@link SQLAction#execute(SQLExceptionHandler)} 等相关方法时, + * 如果发生异常,则会调用错误处理器进行错误内容的输出提示。 + */ + +public interface SQLDebugHandler { + /** + * 该方法将在 {@link SQLAction#execute()} 执行前调用。 + * + * @param action {@link SQLAction} 对象 + * @param params 执行传入的参数列表。 + * 实际上,仅有 {@link PreparedSQLUpdateAction} 和 {@link PreparedSQLUpdateBatchAction} 才会有传入参数。 + */ + void beforeExecute(@NotNull SQLAction action, @NotNull List<@Nullable Object[]> params); + + /** + * 该方法将在 {@link SQLQuery#close()} 执行后调用。 + * + * @param query {@link SQLQuery} 对象 + * @param executeNanoTime 该次查询开始执行的时间 (单位:纳秒) + * @param closeNanoTime 该次查询彻底关闭的时间 (单位:纳秒) + */ + void afterQuery(@NotNull SQLQuery query, long executeNanoTime, long closeNanoTime); + + default String parseParams(@Nullable Object[] params) { + if (params == null) return "<#NULL>"; + else if (params.length == 0) return "<#EMPTY>"; + + List paramsString = new ArrayList<>(); + for (Object param : params) { + if (param == null) paramsString.add("NULL"); + else paramsString.add(param.toString()); + } + return String.join(", ", paramsString); + } + + @SuppressWarnings("DuplicatedCode") + static SQLDebugHandler defaultHandler(Logger logger) { + return new SQLDebugHandler() { + + @Override + public void beforeExecute(@NotNull SQLAction action, @NotNull List<@Nullable Object[]> params) { + logger.info("┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); + logger.info("┣# ActionUUID: {}", action.getActionUUID()); + logger.info("┣# ActionType: {}", action.getClass().getSimpleName()); + if (action.getSQLContents().size() == 1) { + logger.info("┣# SQLContent: {}", action.getSQLContents().get(0)); + } else { + logger.info("┣# SQLContents: "); + int i = 0; + for (String sqlContent : action.getSQLContents()) { + logger.info("┃ - [{}] {}", ++i, sqlContent); + } + } + if (params.size() == 1) { + Object[] param = params.get(0); + if (param != null) { + logger.info("┣# SQLParam: {}", parseParams(param)); + } + } else if (params.size() > 1) { + logger.info("┣# SQLParams: "); + int i = 0; + for (Object[] param : params) { + logger.info("┃ - [{}] {}", ++i, parseParams(param)); + } + } + logger.info("┣# CreateTime: {}", action.getCreateTime(TimeUnit.MILLISECONDS)); + logger.info("┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); + } + + @Override + public void afterQuery(@NotNull SQLQuery query, long executeNanoTime, long closeNanoTime) { + logger.info("┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); + logger.info("┣# ActionUUID: {}", query.getAction().getActionUUID()); + logger.info("┣# SQLContent: {}", query.getSQLContent()); + logger.info("┣# CloseTime: {} (cost {} ms)", + TimeUnit.NANOSECONDS.toMillis(closeNanoTime), + ((double) (closeNanoTime - executeNanoTime) / 1000000) + ); + logger.info("┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); + } + }; + } + +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/sql/api/function/SQLExceptionHandler.java b/src/main/java/com/io/yutian/elementoriginlib/sql/api/function/SQLExceptionHandler.java new file mode 100644 index 0000000..159062c --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/sql/api/function/SQLExceptionHandler.java @@ -0,0 +1,46 @@ +package com.io.yutian.elementoriginlib.sql.api.function; + +import com.io.yutian.elementoriginlib.sql.api.SQLAction; +import org.slf4j.Logger; + +import java.sql.SQLException; +import java.util.function.BiConsumer; + +/** + * 异常处理器。 + *
在使用 {@link SQLAction#execute(SQLExceptionHandler)} 等相关方法时, + * 如果发生异常,则会调用错误处理器进行错误内容的输出提示。 + */ +@FunctionalInterface +public interface SQLExceptionHandler extends BiConsumer> { + + /** + * 默认的异常处理器,将详细的输出相关错误与错误来源。 + * + * @param logger 用于输出错误信息的Logger。 + * @return 输出详细信息的错误处理器。 + */ + static SQLExceptionHandler detailed(Logger logger) { + return (exception, sqlAction) -> { + logger.error("Error occurred while executing SQL: "); + int i = 1; + for (String content : sqlAction.getSQLContents()) { + logger.error(String.format("#%d {%s}", i, content)); + i++; + } + exception.printStackTrace(); + }; + } + + /** + * “安静“ 的错误处理器,发生错误什么都不做。 + * 强烈不建议把此处理器作为默认处理器使用! + * + * @return 无输出的处理器。 + */ + static SQLExceptionHandler silent() { + return (exception, sqlAction) -> { + }; + } + +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/sql/api/function/SQLFunction.java b/src/main/java/com/io/yutian/elementoriginlib/sql/api/function/SQLFunction.java new file mode 100644 index 0000000..2f2369c --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/sql/api/function/SQLFunction.java @@ -0,0 +1,34 @@ +package com.io.yutian.elementoriginlib.sql.api.function; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.sql.SQLException; +import java.util.Objects; + +@FunctionalInterface +public interface SQLFunction { + + @Nullable + R apply(@NotNull T t) throws SQLException; + + default SQLFunction compose(@NotNull SQLFunction before) { + Objects.requireNonNull(before); + return (V v) -> { + T t = before.apply(v); + if (t == null) return null; + else return apply(t); + }; + } + + default SQLFunction then(@NotNull SQLFunction after) { + Objects.requireNonNull(after); + return (T t) -> { + R r = apply(t); + if (r == null) return null; + else return after.apply(r); + }; + } + + +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/sql/api/function/SQLHandler.java b/src/main/java/com/io/yutian/elementoriginlib/sql/api/function/SQLHandler.java new file mode 100644 index 0000000..18f13d9 --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/sql/api/function/SQLHandler.java @@ -0,0 +1,23 @@ +package com.io.yutian.elementoriginlib.sql.api.function; + +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; + +import java.sql.SQLException; +import java.util.Objects; + +@FunctionalInterface +public interface SQLHandler { + + void accept(@NotNull T t) throws SQLException; + + @NotNull + @Contract(pure = true) + default SQLHandler andThen(@NotNull SQLHandler after) { + Objects.requireNonNull(after); + return (T t) -> { + accept(t); + after.accept(t); + }; + } +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/sql/api/table/NamedSQLTable.java b/src/main/java/com/io/yutian/elementoriginlib/sql/api/table/NamedSQLTable.java new file mode 100644 index 0000000..5f2541f --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/sql/api/table/NamedSQLTable.java @@ -0,0 +1,50 @@ +package com.io.yutian.elementoriginlib.sql.api.table; + +import com.io.yutian.elementoriginlib.sql.api.SQLManager; +import com.io.yutian.elementoriginlib.sql.api.SQLTable; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.sql.SQLException; + + +public abstract class NamedSQLTable implements SQLTable { + + private final @NotNull String tableName; + + protected @Nullable String tablePrefix; + protected @Nullable SQLManager manager; + + /** + * 请调用 {@link NamedSQLTable} 下的静态方法进行对象的初始化。 + * + * @param tableName 该表的名称 + */ + public NamedSQLTable(@NotNull String tableName) { + this.tableName = tableName; + } + + public @NotNull String getTableName() { + return (tablePrefix != null ? tablePrefix : "") + tableName; + } + + @Override + public @Nullable SQLManager getSQLManager() { + return this.manager; + } + + /** + * 使用指定 SQLManager 进行本示例的初始化。 + * + * @param sqlManager {@link SQLManager} + * @param tablePrefix 表名前缀 + * @return 本表是否为首次创建 + * @throws SQLException 出现任何错误时抛出 + */ + public abstract boolean create(@NotNull SQLManager sqlManager, @Nullable String tablePrefix) throws SQLException; + + public boolean create(@NotNull SQLManager sqlManager) throws SQLException { + return create(sqlManager, null); + } + +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/sql/api/util/TimeDateUtils.java b/src/main/java/com/io/yutian/elementoriginlib/sql/api/util/TimeDateUtils.java new file mode 100644 index 0000000..5910599 --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/sql/api/util/TimeDateUtils.java @@ -0,0 +1,108 @@ +package com.io.yutian.elementoriginlib.sql.api.util; + +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; + +public class TimeDateUtils { + public static final DateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + + public TimeDateUtils() { + } + + /** + * 得到当前时间文本。 + * + * @return 时间文本 格式{@link TimeDateUtils#getFormat()} + */ + public static String getCurrentTime() { + return getTimeString(System.currentTimeMillis()); + } + + /** + * 得到一个时间戳的文本 + * + * @param timeMillis 时间戳 + * @return 时间文本 格式{@link TimeDateUtils#getFormat()} + */ + public static String getTimeString(long timeMillis) { + return getFormat().format(new Date(timeMillis)); + } + + /** + * 得到一个日期时间的文本 + * + * @param time 日期时间 + * @return 时间文本 格式{@link TimeDateUtils#getFormat()} + */ + public static String getTimeString(Date time) { + return getFormat().format(time); + } + + /** + * 得到一个时间文本的时间戳 + * + * @param timeString 时间文本 + * @return 时间戳 格式{@link TimeDateUtils#getFormat()} + */ + public static long parseTimeMillis(String timeString) { + if (timeString == null) { + return -1L; + } else { + try { + return format.parse(timeString).getTime(); + } catch (ParseException var2) { + return -1L; + } + } + } + + + /** + * 得到一个时间文本的对应日期实例 + * + * @param timeString 时间文本 + * @return 日期实例 格式{@link TimeDateUtils#getFormat()} + */ + public static Date getTimeDate(String timeString) { + if (timeString == null) { + return null; + } else { + try { + return format.parse(timeString); + } catch (ParseException var2) { + return null; + } + } + } + + /** + * 将秒数转化为 DD:hh:mm:ss 格式 + * + * @param allSeconds 秒数 + * @return DD:hh:mm:ss格式文本 + */ + public static String toDHMSStyle(long allSeconds) { + long days = allSeconds / 86400L; + long hours = allSeconds % 86400L / 3600L; + long minutes = allSeconds % 3600L / 60L; + long seconds = allSeconds % 60L; + String DateTimes; + if (days > 0L) { + DateTimes = days + "天" + (hours > 0L ? hours + "小时" : "") + (minutes > 0L ? minutes + "分钟" : "") + (seconds > 0L ? seconds + "秒" : ""); + } else if (hours > 0L) { + DateTimes = hours + "小时" + (minutes > 0L ? minutes + "分钟" : "") + (seconds > 0L ? seconds + "秒" : ""); + } else if (minutes > 0L) { + DateTimes = minutes + "分钟" + (seconds > 0L ? seconds + "秒" : ""); + } else { + DateTimes = seconds + "秒"; + } + + return DateTimes; + } + + public static DateFormat getFormat() { + return format; + } +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/sql/api/util/UUIDUtil.java b/src/main/java/com/io/yutian/elementoriginlib/sql/api/util/UUIDUtil.java new file mode 100644 index 0000000..7f07f6d --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/sql/api/util/UUIDUtil.java @@ -0,0 +1,28 @@ +package com.io.yutian.elementoriginlib.sql.api.util; + +import java.util.UUID; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class UUIDUtil { + + private static final Pattern COMPILE = Pattern.compile("-", Pattern.LITERAL); + + public static UUID random() { + return UUID.randomUUID(); + } + + public static String toString(UUID uuid, boolean withDash) { + if (withDash) return uuid.toString(); + else return COMPILE.matcher(uuid.toString()).replaceAll(Matcher.quoteReplacement("")); + } + + public static UUID toUUID(String s) { + if (s.length() == 36) { + return UUID.fromString(s); + } else { + return UUID.fromString(s.substring(0, 8) + '-' + s.substring(8, 12) + '-' + s.substring(12, 16) + '-' + s.substring(16, 20) + '-' + s.substring(20)); + } + } + +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/sql/builder/AbstractSQLBuilder.java b/src/main/java/com/io/yutian/elementoriginlib/sql/builder/AbstractSQLBuilder.java new file mode 100644 index 0000000..987eda1 --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/sql/builder/AbstractSQLBuilder.java @@ -0,0 +1,23 @@ +package com.io.yutian.elementoriginlib.sql.builder; + +import com.io.yutian.elementoriginlib.sql.api.SQLBuilder; +import com.io.yutian.elementoriginlib.sql.manager.SQLManagerImpl; +import org.jetbrains.annotations.NotNull; + +import java.util.Objects; + +public abstract class AbstractSQLBuilder implements SQLBuilder { + + @NotNull + final SQLManagerImpl sqlManager; + + public AbstractSQLBuilder(@NotNull SQLManagerImpl manager) { + Objects.requireNonNull(manager, "SQLManager must not be null"); + this.sqlManager = manager; + } + + @Override + public @NotNull SQLManagerImpl getManager() { + return this.sqlManager; + } +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/sql/builder/impl/AbstractConditionalBuilder.java b/src/main/java/com/io/yutian/elementoriginlib/sql/builder/impl/AbstractConditionalBuilder.java new file mode 100644 index 0000000..c186d87 --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/sql/builder/impl/AbstractConditionalBuilder.java @@ -0,0 +1,159 @@ +package com.io.yutian.elementoriginlib.sql.builder.impl; + +import com.io.yutian.elementoriginlib.sql.api.SQLAction; +import com.io.yutian.elementoriginlib.sql.api.builder.ConditionalBuilder; +import com.io.yutian.elementoriginlib.sql.builder.AbstractSQLBuilder; +import com.io.yutian.elementoriginlib.sql.manager.SQLManagerImpl; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.sql.Time; +import java.sql.Timestamp; +import java.util.*; + +import static com.io.yutian.elementoriginlib.sql.api.SQLBuilder.withBackQuote; + +public abstract class AbstractConditionalBuilder, T extends SQLAction> + extends AbstractSQLBuilder implements ConditionalBuilder { + + ArrayList conditionSQLs = new ArrayList<>(); + ArrayList conditionParams = new ArrayList<>(); + int limit = -1; + + public AbstractConditionalBuilder(@NotNull SQLManagerImpl manager) { + super(manager); + } + + protected abstract B getThis(); + + @Override + public B setConditions(@Nullable String condition) { + this.conditionSQLs = new ArrayList<>(); + this.conditionParams = new ArrayList<>(); + if (condition != null) this.conditionSQLs.add(condition); + return getThis(); + } + + @Override + public B setConditions( + LinkedHashMap<@NotNull String, @Nullable Object> conditions + ) { + conditions.forEach(this::addCondition); + return getThis(); + } + + @Override + public B addCondition(@Nullable String condition) { + this.conditionSQLs.add(condition); + return getThis(); + } + + @Override + public B addCondition(@NotNull String columnName, @Nullable Object queryValue) { + Objects.requireNonNull(columnName, "columnName could not be null"); + if (queryValue == null) { + return addCondition(withBackQuote(columnName) + " IS NULL"); + } else { + return addCondition(columnName, "=", queryValue); + } + } + + @Override + public B addCondition( + @NotNull String columnName, @NotNull String operator, @Nullable Object queryValue + ) { + Objects.requireNonNull(columnName, "columnName could not be null"); + Objects.requireNonNull(operator, "operator could not be null (e.g. > or = or <) "); + addCondition(withBackQuote(columnName) + " " + operator + " ?"); + this.conditionParams.add(queryValue); + return getThis(); + } + + @Override + public B addCondition( + @NotNull String[] columnNames, @Nullable Object[] queryValues + ) { + Objects.requireNonNull(columnNames, "columnName could not be null"); + if (queryValues == null || columnNames.length != queryValues.length) { + throw new RuntimeException("queryNames are not match with queryValues"); + } + for (int i = 0; i < columnNames.length; i++) { + addCondition(columnNames[i], queryValues[i]); + } + return getThis(); + } + + + @Override + public B addNotNullCondition(@NotNull String columnName) { + Objects.requireNonNull(columnName, "columnName could not be null"); + return addCondition(withBackQuote(columnName) + " IS NOT NULL"); + } + + + @Override + public B addTimeCondition( + @NotNull String columnName, @Nullable Date startDate, @Nullable Date endDate + ) { + Objects.requireNonNull(columnName, "columnName could not be null"); + if (startDate == null && endDate == null) return getThis(); // 都不限定时间,不用判断了 + if (startDate != null) { + addCondition(withBackQuote(columnName) + " BETWEEN ? AND ?"); + this.conditionParams.add(startDate); + if (endDate != null) { + this.conditionParams.add(endDate); + } else { + if (startDate instanceof java.sql.Date) { + this.conditionParams.add(new java.sql.Date(System.currentTimeMillis())); + } else if (startDate instanceof Time) { + this.conditionParams.add(new Time(System.currentTimeMillis())); + } else { + this.conditionParams.add(new Timestamp(System.currentTimeMillis())); + } + } + } else { + addCondition(columnName, "<=", endDate); + } + return getThis(); + } + + + @Override + public B setLimit(int limit) { + this.limit = limit; + return getThis(); + } + + protected String buildConditionSQL() { + + if (!conditionSQLs.isEmpty()) { + StringBuilder conditionBuilder = new StringBuilder(); + conditionBuilder.append("WHERE").append(" "); + Iterator iterator = conditionSQLs.iterator(); + while (iterator.hasNext()) { + conditionBuilder.append(iterator.next()); + if (iterator.hasNext()) conditionBuilder.append(" AND "); + } + return conditionBuilder.toString().trim(); + } else { + return null; + } + + } + + protected String buildLimitSQL() { + return limit > 0 ? "LIMIT " + limit : ""; + } + + protected ArrayList getConditionParams() { + return conditionParams; + } + + protected boolean hasConditions() { + return this.conditionSQLs != null && !this.conditionSQLs.isEmpty(); + } + + protected boolean hasConditionParams() { + return hasConditions() && getConditionParams() != null && !getConditionParams().isEmpty(); + } +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/sql/builder/impl/DeleteBuilderImpl.java b/src/main/java/com/io/yutian/elementoriginlib/sql/builder/impl/DeleteBuilderImpl.java new file mode 100644 index 0000000..c5951e5 --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/sql/builder/impl/DeleteBuilderImpl.java @@ -0,0 +1,53 @@ +package com.io.yutian.elementoriginlib.sql.builder.impl; + +import com.io.yutian.elementoriginlib.sql.action.PreparedSQLUpdateActionImpl; +import com.io.yutian.elementoriginlib.sql.api.SQLAction; +import com.io.yutian.elementoriginlib.sql.api.action.PreparedSQLUpdateAction; +import com.io.yutian.elementoriginlib.sql.api.builder.DeleteBuilder; +import com.io.yutian.elementoriginlib.sql.manager.SQLManagerImpl; +import org.jetbrains.annotations.NotNull; + +import java.util.Objects; + +import static com.io.yutian.elementoriginlib.sql.api.SQLBuilder.withBackQuote; + +public class DeleteBuilderImpl + extends AbstractConditionalBuilder> + implements DeleteBuilder { + + protected final String tableName; + + public DeleteBuilderImpl(@NotNull SQLManagerImpl manager, @NotNull String tableName) { + super(manager); + Objects.requireNonNull(tableName); + this.tableName = tableName; + } + + @Override + public PreparedSQLUpdateAction build() { + + StringBuilder sqlBuilder = new StringBuilder(); + + sqlBuilder.append("DELETE FROM ").append(withBackQuote(getTableName())); + + if (hasConditions()) sqlBuilder.append(" ").append(buildConditionSQL()); + if (limit > 0) sqlBuilder.append(" ").append(buildLimitSQL()); + + return new PreparedSQLUpdateActionImpl<>( + getManager(), Integer.class, sqlBuilder.toString(), + (hasConditionParams() ? getConditionParams() : null) + ); + } + + @Override + public String getTableName() { + return tableName; + } + + + @Override + protected DeleteBuilderImpl getThis() { + return this; + } + +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/sql/builder/impl/InsertBuilderImpl.java b/src/main/java/com/io/yutian/elementoriginlib/sql/builder/impl/InsertBuilderImpl.java new file mode 100644 index 0000000..8ff33d9 --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/sql/builder/impl/InsertBuilderImpl.java @@ -0,0 +1,69 @@ +package com.io.yutian.elementoriginlib.sql.builder.impl; + +import com.io.yutian.elementoriginlib.sql.api.SQLAction; +import com.io.yutian.elementoriginlib.sql.api.builder.InsertBuilder; +import com.io.yutian.elementoriginlib.sql.builder.AbstractSQLBuilder; +import com.io.yutian.elementoriginlib.sql.manager.SQLManagerImpl; +import org.jetbrains.annotations.NotNull; + +import java.util.Iterator; +import java.util.List; +import java.util.Objects; + +import static com.io.yutian.elementoriginlib.sql.api.SQLBuilder.withBackQuote; + +public abstract class InsertBuilderImpl> + extends AbstractSQLBuilder implements InsertBuilder { + + protected final String tableName; + protected boolean ignore; + + public InsertBuilderImpl(@NotNull SQLManagerImpl manager, String tableName) { + super(manager); + Objects.requireNonNull(tableName); + this.tableName = tableName; + } + + @Override + public boolean isIgnore() { + return ignore; + } + + @Override + public InsertBuilder setIgnore(boolean ignore) { + this.ignore = ignore; + return this; + } + + protected static String buildSQL(String tableName, boolean ignore, List columnNames) { + return buildSQL("INSERT "+(ignore ? "IGNORE " : "")+"INTO", tableName, columnNames); + } + + protected static String buildSQL(String sqlPrefix, String tableName, List columnNames) { + int valueLength = columnNames.size(); + StringBuilder sqlBuilder = new StringBuilder(); + + sqlBuilder.append(sqlPrefix).append(" ").append(withBackQuote(tableName)).append("("); + Iterator iterator = columnNames.iterator(); + while (iterator.hasNext()) { + sqlBuilder.append(withBackQuote(iterator.next())); + if (iterator.hasNext()) sqlBuilder.append(", "); + } + + sqlBuilder.append(") VALUES ("); + + for (int i = 0; i < valueLength; i++) { + sqlBuilder.append("?"); + if (i != valueLength - 1) { + sqlBuilder.append(", "); + } + } + sqlBuilder.append(")"); + return sqlBuilder.toString(); + } + + @Override + public String getTableName() { + return tableName; + } +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/sql/builder/impl/QueryBuilderImpl.java b/src/main/java/com/io/yutian/elementoriginlib/sql/builder/impl/QueryBuilderImpl.java new file mode 100644 index 0000000..008e203 --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/sql/builder/impl/QueryBuilderImpl.java @@ -0,0 +1,39 @@ +package com.io.yutian.elementoriginlib.sql.builder.impl; + +import com.io.yutian.elementoriginlib.sql.action.query.PreparedQueryActionImpl; +import com.io.yutian.elementoriginlib.sql.action.query.QueryActionImpl; +import com.io.yutian.elementoriginlib.sql.api.action.query.PreparedQueryAction; +import com.io.yutian.elementoriginlib.sql.api.action.query.QueryAction; +import com.io.yutian.elementoriginlib.sql.api.builder.QueryBuilder; +import com.io.yutian.elementoriginlib.sql.api.builder.TableQueryBuilder; +import com.io.yutian.elementoriginlib.sql.builder.AbstractSQLBuilder; +import com.io.yutian.elementoriginlib.sql.manager.SQLManagerImpl; +import org.jetbrains.annotations.NotNull; + +import java.util.Objects; + +public class QueryBuilderImpl extends AbstractSQLBuilder implements QueryBuilder { + public QueryBuilderImpl(@NotNull SQLManagerImpl manager) { + super(manager); + } + + @Override + @Deprecated + public QueryAction withSQL(@NotNull String sql) { + Objects.requireNonNull(sql, "sql could not be null"); + return new QueryActionImpl(getManager(), sql); + } + + @Override + public PreparedQueryAction withPreparedSQL(@NotNull String sql) { + Objects.requireNonNull(sql, "sql could not be null"); + return new PreparedQueryActionImpl(getManager(), sql); + } + + @Override + public TableQueryBuilder inTable(@NotNull String tableName) { + Objects.requireNonNull(tableName, "tableName could not be null"); + return new TableQueryBuilderImpl(getManager(), tableName); + } + +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/sql/builder/impl/ReplaceBuilderImpl.java b/src/main/java/com/io/yutian/elementoriginlib/sql/builder/impl/ReplaceBuilderImpl.java new file mode 100644 index 0000000..0cfaa62 --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/sql/builder/impl/ReplaceBuilderImpl.java @@ -0,0 +1,29 @@ +package com.io.yutian.elementoriginlib.sql.builder.impl; + +import com.io.yutian.elementoriginlib.sql.api.SQLAction; +import com.io.yutian.elementoriginlib.sql.api.builder.ReplaceBuilder; +import com.io.yutian.elementoriginlib.sql.builder.AbstractSQLBuilder; +import com.io.yutian.elementoriginlib.sql.manager.SQLManagerImpl; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +public abstract class ReplaceBuilderImpl> + extends AbstractSQLBuilder implements ReplaceBuilder { + + protected final @NotNull String tableName; + + public ReplaceBuilderImpl(@NotNull SQLManagerImpl manager, @NotNull String tableName) { + super(manager); + this.tableName = tableName; + } + + protected static String buildSQL(String tableName, List columnNames) { + return InsertBuilderImpl.buildSQL("REPLACE INTO", tableName, columnNames); + } + + @Override + public @NotNull String getTableName() { + return tableName; + } +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/sql/builder/impl/TableAlterBuilderImpl.java b/src/main/java/com/io/yutian/elementoriginlib/sql/builder/impl/TableAlterBuilderImpl.java new file mode 100644 index 0000000..6631a94 --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/sql/builder/impl/TableAlterBuilderImpl.java @@ -0,0 +1,147 @@ +package com.io.yutian.elementoriginlib.sql.builder.impl; + +import com.io.yutian.elementoriginlib.sql.action.SQLUpdateActionImpl; +import com.io.yutian.elementoriginlib.sql.api.SQLAction; +import com.io.yutian.elementoriginlib.sql.api.builder.TableAlterBuilder; +import com.io.yutian.elementoriginlib.sql.api.enums.IndexType; +import com.io.yutian.elementoriginlib.sql.builder.AbstractSQLBuilder; +import com.io.yutian.elementoriginlib.sql.manager.SQLManagerImpl; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Objects; + +import static com.io.yutian.elementoriginlib.sql.api.SQLBuilder.withBackQuote; +import static com.io.yutian.elementoriginlib.sql.api.SQLBuilder.withQuote; + +public class TableAlterBuilderImpl extends AbstractSQLBuilder implements TableAlterBuilder { + + protected final @NotNull String tableName; + + public TableAlterBuilderImpl(@NotNull SQLManagerImpl manager, @NotNull String tableName) { + super(manager); + this.tableName = tableName; + } + + public @NotNull String getTableName() { + return tableName; + } + + @Override + public SQLAction renameTo(@NotNull String newTableName) { + Objects.requireNonNull(newTableName, "table name could not be null"); + return createAction("ALTER TABLE " + withBackQuote(tableName) + " RENAME TO " + withBackQuote(newTableName)); + } + + @Override + public SQLAction changeComment(@NotNull String newTableComment) { + Objects.requireNonNull(newTableComment, "table comment could not be null"); + return createAction("ALTER TABLE " + withBackQuote(getTableName()) + " COMMENT " + withQuote(newTableComment)); + } + + @Override + public SQLAction setAutoIncrementIndex(int index) { + return createAction("ALTER TABLE " + withBackQuote(getTableName()) + " AUTO_INCREMENT=" + index); + } + + @Override + public SQLAction addIndex(@NotNull IndexType indexType, @Nullable String indexName, + @NotNull String columnName, @NotNull String... moreColumns) { + Objects.requireNonNull(indexType, "indexType could not be null"); + Objects.requireNonNull(columnName, "column names could not be null"); + Objects.requireNonNull(moreColumns, "column names could not be null"); + return createAction( + "ALTER TABLE " + withBackQuote(getTableName()) + " ADD " + + TableCreateBuilderImpl.buildIndexSettings(indexType, indexName, columnName, moreColumns) + ); + } + + @Override + public SQLAction dropIndex(@NotNull String indexName) { + Objects.requireNonNull(indexName, "indexName could not be null"); + return createAction( + "ALTER TABLE " + withBackQuote(getTableName()) + " DROP INDEX " + withBackQuote(indexName) + ); + } + + @Override + public SQLAction dropForeignKey(@NotNull String keySymbol) { + Objects.requireNonNull(keySymbol, "keySymbol could not be null"); + return createAction( + "ALTER TABLE " + withBackQuote(getTableName()) + " DROP FOREIGN KEY " + withBackQuote(keySymbol) + ); + } + + @Override + public SQLAction dropPrimaryKey() { + return createAction( + "ALTER TABLE " + withBackQuote(getTableName()) + " DROP PRIMARY KEY" + ); + } + + @Override + public SQLAction addColumn(@NotNull String columnName, @NotNull String settings, @Nullable String afterColumn) { + Objects.requireNonNull(columnName, "columnName could not be null"); + Objects.requireNonNull(settings, "settings could not be null"); + String orderSettings = null; + if (afterColumn != null) { + if (afterColumn.length() > 0) { + orderSettings = "AFTER " + withBackQuote(afterColumn); + } else { + orderSettings = "FIRST"; + } + } + + return createAction( + "ALTER TABLE " + withBackQuote(getTableName()) + " ADD " + withBackQuote(columnName) + " " + settings + + (orderSettings != null ? " " + orderSettings : "") + ); + } + + @Override + public SQLAction renameColumn(@NotNull String columnName, @NotNull String newName) { + Objects.requireNonNull(columnName, "columnName could not be null"); + Objects.requireNonNull(newName, "please specify new column name"); + return createAction( + "ALTER TABLE " + withBackQuote(getTableName()) + " RENAME COLUMN " + withBackQuote(columnName) + " TO " + withBackQuote(newName) + ); + } + + @Override + public SQLAction modifyColumn(@NotNull String columnName, @NotNull String settings) { + Objects.requireNonNull(columnName, "columnName could not be null"); + Objects.requireNonNull(settings, "please specify new column settings"); + return createAction( + "ALTER TABLE " + withBackQuote(getTableName()) + " MODIFY COLUMN " + withBackQuote(columnName) + " " + settings + ); + } + + @Override + public SQLAction removeColumn(@NotNull String columnName) { + Objects.requireNonNull(columnName, "columnName could not be null"); + return createAction( + "ALTER TABLE " + withBackQuote(getTableName()) + " DROP " + withBackQuote(columnName) + ); + } + + @Override + public SQLAction setColumnDefault(@NotNull String columnName, @NotNull String defaultValue) { + Objects.requireNonNull(columnName, "columnName could not be null"); + Objects.requireNonNull(defaultValue, "defaultValue could not be null, if you need to remove the default value, please use #removeColumnDefault()."); + return createAction( + "ALTER TABLE " + withBackQuote(getTableName()) + " ALTER " + withBackQuote(columnName) + " SET DEFAULT " + defaultValue + ); + } + + @Override + public SQLAction removeColumnDefault(@NotNull String columnName) { + Objects.requireNonNull(columnName, "columnName could not be null"); + return createAction( + "ALTER TABLE " + withBackQuote(getTableName()) + " ALTER " + withBackQuote(columnName) + " DROP DEFAULT" + ); + } + + private SQLUpdateActionImpl createAction(@NotNull String sql) { + return new SQLUpdateActionImpl<>(getManager(), Integer.class, sql); + } +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/sql/builder/impl/TableCreateBuilderImpl.java b/src/main/java/com/io/yutian/elementoriginlib/sql/builder/impl/TableCreateBuilderImpl.java new file mode 100644 index 0000000..a6f52ae --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/sql/builder/impl/TableCreateBuilderImpl.java @@ -0,0 +1,172 @@ +package com.io.yutian.elementoriginlib.sql.builder.impl; + +import com.io.yutian.elementoriginlib.sql.action.SQLUpdateActionImpl; +import com.io.yutian.elementoriginlib.sql.api.action.SQLUpdateAction; +import com.io.yutian.elementoriginlib.sql.api.builder.TableCreateBuilder; +import com.io.yutian.elementoriginlib.sql.api.enums.ForeignKeyRule; +import com.io.yutian.elementoriginlib.sql.api.enums.IndexType; +import com.io.yutian.elementoriginlib.sql.api.enums.NumberType; +import com.io.yutian.elementoriginlib.sql.builder.AbstractSQLBuilder; +import com.io.yutian.elementoriginlib.sql.manager.SQLManagerImpl; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +import static com.io.yutian.elementoriginlib.sql.api.SQLBuilder.withBackQuote; +import static com.io.yutian.elementoriginlib.sql.api.SQLBuilder.withQuote; + +public class TableCreateBuilderImpl extends AbstractSQLBuilder implements TableCreateBuilder { + + protected final @NotNull String tableName; + @NotNull + final List indexes = new ArrayList<>(); + @NotNull + final List foreignKeys = new ArrayList<>(); + @NotNull List columns = new ArrayList<>(); + @NotNull String tableSettings = defaultTablesSettings(); + @Nullable String tableComment; + + public TableCreateBuilderImpl(SQLManagerImpl manager, @NotNull String tableName) { + super(manager); + this.tableName = tableName; + } + + protected static String buildIndexSettings(@NotNull IndexType indexType, @Nullable String indexName, + @NotNull String columnName, @NotNull String... moreColumns) { + Objects.requireNonNull(indexType, "indexType could not be null"); + Objects.requireNonNull(columnName, "column names could not be null"); + Objects.requireNonNull(moreColumns, "column names could not be null"); + StringBuilder indexBuilder = new StringBuilder(); + + indexBuilder.append(indexType.getName()).append(" "); + if (indexName != null) { + indexBuilder.append(withBackQuote(indexName)); + } + indexBuilder.append("("); + indexBuilder.append(withBackQuote(columnName)); + + if (moreColumns.length > 0) { + indexBuilder.append(", "); + + for (int i = 0; i < moreColumns.length; i++) { + indexBuilder.append(withBackQuote(moreColumns[i])); + if (i != moreColumns.length - 1) indexBuilder.append(", "); + } + + } + + indexBuilder.append(")"); + + return indexBuilder.toString(); + } + + @Override + public @NotNull String getTableName() { + return this.tableName; + } + + @Override + public @NotNull String getTableSettings() { + return this.tableSettings; + } + + @Override + public SQLUpdateAction build() { + StringBuilder createSQL = new StringBuilder(); + createSQL.append("CREATE TABLE IF NOT EXISTS ").append(withBackQuote(tableName)); + createSQL.append("("); + createSQL.append(String.join(", ", columns)); + if (indexes.size() > 0) { + createSQL.append(", "); + createSQL.append(String.join(", ", indexes)); + } + if (foreignKeys.size() > 0) { + createSQL.append(", "); + createSQL.append(String.join(", ", foreignKeys)); + } + createSQL.append(") ").append(getTableSettings()); + + if (tableComment != null) { + createSQL.append(" COMMENT ").append(withQuote(tableComment)); + } + + return new SQLUpdateActionImpl<>(getManager(), Integer.class, createSQL.toString()); + } + + @Override + public TableCreateBuilder addColumn(@NotNull String column) { + Objects.requireNonNull(column, "column could not be null"); + this.columns.add(column); + return this; + } + + @Override + public TableCreateBuilder addAutoIncrementColumn(@NotNull String columnName, @Nullable NumberType numberType, + boolean asPrimaryKey, boolean unsigned) { + return addColumn(columnName, + (numberType == null ? NumberType.INT : numberType).name() + + (unsigned ? " UNSIGNED " : " ") + + "NOT NULL AUTO_INCREMENT " + (asPrimaryKey ? "PRIMARY KEY" : "UNIQUE KEY") + ); + } + + @Override + public TableCreateBuilder setIndex(@NotNull IndexType type, @Nullable String indexName, + @NotNull String columnName, @NotNull String... moreColumns) { + Objects.requireNonNull(columnName, "columnName could not be null"); + this.indexes.add(buildIndexSettings(type, indexName, columnName, moreColumns)); + return this; + } + + @Override + public TableCreateBuilder addForeignKey(@NotNull String tableColumn, @Nullable String constraintName, + @NotNull String foreignTable, @NotNull String foreignColumn, + @Nullable ForeignKeyRule updateRule, @Nullable ForeignKeyRule deleteRule) { + Objects.requireNonNull(tableColumn, "tableColumn could not be null"); + Objects.requireNonNull(foreignTable, "foreignTable could not be null"); + Objects.requireNonNull(foreignColumn, "foreignColumn could not be null"); + + StringBuilder keyBuilder = new StringBuilder(); + + keyBuilder.append("CONSTRAINT "); + if (constraintName == null) { + keyBuilder.append(withBackQuote("fk_" + tableColumn + "_" + foreignTable)); + } else { + keyBuilder.append(withBackQuote(constraintName)); + } + keyBuilder.append(" "); + keyBuilder.append("FOREIGN KEY (").append(withBackQuote(tableColumn)).append(") "); + keyBuilder.append("REFERENCES ").append(withBackQuote(foreignTable)).append("(").append(withBackQuote(foreignColumn)).append(")"); + + if (updateRule != null) keyBuilder.append(" ON UPDATE ").append(updateRule.getRuleName()); + if (deleteRule != null) keyBuilder.append(" ON DELETE ").append(deleteRule.getRuleName()); + + this.foreignKeys.add(keyBuilder.toString()); + return this; + } + + @Override + public TableCreateBuilder setColumns(@NotNull String... columns) { + Objects.requireNonNull(columns, "columns could not be null"); + this.columns = Arrays.asList(columns); + return this; + } + + @Override + public TableCreateBuilder setTableSettings(@NotNull String settings) { + Objects.requireNonNull(settings, "settings could not be null"); + this.tableSettings = settings; + return this; + } + + @Override + public TableCreateBuilder setTableComment(@Nullable String comment) { + this.tableComment = comment; + return this; + } + +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/sql/builder/impl/TableMetadataBuilderImpl.java b/src/main/java/com/io/yutian/elementoriginlib/sql/builder/impl/TableMetadataBuilderImpl.java new file mode 100644 index 0000000..849eac5 --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/sql/builder/impl/TableMetadataBuilderImpl.java @@ -0,0 +1,74 @@ +package com.io.yutian.elementoriginlib.sql.builder.impl; + +import com.io.yutian.elementoriginlib.sql.api.builder.TableMetadataBuilder; +import com.io.yutian.elementoriginlib.sql.api.function.SQLBiFunction; +import com.io.yutian.elementoriginlib.sql.api.function.SQLFunction; +import com.io.yutian.elementoriginlib.sql.builder.AbstractSQLBuilder; +import com.io.yutian.elementoriginlib.sql.manager.SQLManagerImpl; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.ResultSet; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.CompletableFuture; + +public class TableMetadataBuilderImpl + extends AbstractSQLBuilder + implements TableMetadataBuilder { + + protected final @NotNull String tablePattern; + + public TableMetadataBuilderImpl(@NotNull SQLManagerImpl manager, @NotNull String tablePattern) { + super(manager); + this.tablePattern = tablePattern; + } + + @Override + public CompletableFuture validateExist() { + return validate((meta, conn) -> meta.getTables(conn.getCatalog(), conn.getSchema(), tablePattern.toUpperCase(), new String[]{"TABLE"})); + } + + @Override + public CompletableFuture fetchColumns(@Nullable String columnPattern, + @NotNull SQLFunction reader) { + return getManager().fetchMetadata((meta, conn) -> meta.getColumns( + conn.getCatalog(), conn.getSchema(), tablePattern.toUpperCase(), + Optional.ofNullable(columnPattern).map(String::toUpperCase).orElse("%") + ), reader); + } + + @Override + public CompletableFuture isColumnExists(@NotNull String columnPattern) { + return validate((meta, conn) -> meta.getColumns( + conn.getCatalog(), conn.getSchema(), + tablePattern.toUpperCase(), columnPattern.toUpperCase() + )); + } + + @Override + public CompletableFuture> listColumns(@Nullable String columnPattern) { + return fetchColumns(columnPattern, (rs) -> { + Set data = new LinkedHashSet<>(); + while (rs.next()) { + data.add(rs.getString("COLUMN_NAME")); + } + return Collections.unmodifiableSet(data); + }); + } + + /** + * fast validate EXISTS. + * + * @param supplier supplier to get result set + * @return result future + */ + private CompletableFuture validate(SQLBiFunction supplier) { + return getManager().fetchMetadata(supplier, ResultSet::next); + } + +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/sql/builder/impl/TableQueryBuilderImpl.java b/src/main/java/com/io/yutian/elementoriginlib/sql/builder/impl/TableQueryBuilderImpl.java new file mode 100644 index 0000000..d2a3e07 --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/sql/builder/impl/TableQueryBuilderImpl.java @@ -0,0 +1,94 @@ +package com.io.yutian.elementoriginlib.sql.builder.impl; + +import com.io.yutian.elementoriginlib.sql.action.query.PreparedQueryActionImpl; +import com.io.yutian.elementoriginlib.sql.api.action.query.PreparedQueryAction; +import com.io.yutian.elementoriginlib.sql.api.builder.TableQueryBuilder; +import com.io.yutian.elementoriginlib.sql.manager.SQLManagerImpl; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Objects; + +import static com.io.yutian.elementoriginlib.sql.api.SQLBuilder.withBackQuote; + +public class TableQueryBuilderImpl + extends AbstractConditionalBuilder + implements TableQueryBuilder { + + + protected final @NotNull String tableName; + + String[] columns; + + @Nullable String orderBy; + + int[] pageLimit; + + public TableQueryBuilderImpl(@NotNull SQLManagerImpl manager, @NotNull String tableName) { + super(manager); + this.tableName = tableName; + } + + @Override + public PreparedQueryActionImpl build() { + StringBuilder sqlBuilder = new StringBuilder(); + sqlBuilder.append("SELECT").append(" "); + if (columns == null || columns.length < 1) { + sqlBuilder.append("*"); + } else { + for (int i = 0; i < columns.length; i++) { + String name = columns[i]; + sqlBuilder.append(withBackQuote(name)); + if (i != columns.length - 1) { + sqlBuilder.append(","); + } + } + } + + sqlBuilder.append(" ").append("FROM").append(" ").append(withBackQuote(tableName)); + + if (hasConditions()) sqlBuilder.append(" ").append(buildConditionSQL()); + + if (orderBy != null) sqlBuilder.append(" ").append(orderBy); + + if (pageLimit != null && pageLimit.length == 2) { + sqlBuilder.append(" LIMIT ").append(pageLimit[0]).append(",").append(pageLimit[1]); + } else if (limit > 0) { + sqlBuilder.append(" ").append(buildLimitSQL()); + } + + + return new PreparedQueryActionImpl(getManager(), sqlBuilder.toString()) + .setParams(hasConditionParams() ? getConditionParams() : null); + } + + @Override + public @NotNull String getTableName() { + return tableName; + } + + @Override + public TableQueryBuilderImpl selectColumns(@NotNull String... columnNames) { + Objects.requireNonNull(columnNames, "columnNames could not be null"); + this.columns = columnNames; + return this; + } + + @Override + public TableQueryBuilder orderBy(@NotNull String columnName, boolean asc) { + Objects.requireNonNull(columnName, "columnName could not be null"); + this.orderBy = "ORDER BY " + withBackQuote(columnName) + " " + (asc ? "ASC" : "DESC"); + return this; + } + + @Override + public TableQueryBuilder setPageLimit(int start, int end) { + this.pageLimit = new int[]{start, end}; + return this; + } + + @Override + protected TableQueryBuilderImpl getThis() { + return this; + } +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/sql/builder/impl/UpdateBuilderImpl.java b/src/main/java/com/io/yutian/elementoriginlib/sql/builder/impl/UpdateBuilderImpl.java new file mode 100644 index 0000000..fba6bb2 --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/sql/builder/impl/UpdateBuilderImpl.java @@ -0,0 +1,89 @@ +package com.io.yutian.elementoriginlib.sql.builder.impl; + +import com.io.yutian.elementoriginlib.sql.action.PreparedSQLUpdateActionImpl; +import com.io.yutian.elementoriginlib.sql.api.SQLAction; +import com.io.yutian.elementoriginlib.sql.api.action.PreparedSQLUpdateAction; +import com.io.yutian.elementoriginlib.sql.api.builder.UpdateBuilder; +import com.io.yutian.elementoriginlib.sql.manager.SQLManagerImpl; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.*; + +import static com.io.yutian.elementoriginlib.sql.api.SQLBuilder.withBackQuote; + +public class UpdateBuilderImpl + extends AbstractConditionalBuilder> + implements UpdateBuilder { + + protected final @NotNull String tableName; + + @NotNull LinkedHashMap columnData; + + public UpdateBuilderImpl(@NotNull SQLManagerImpl manager, @NotNull String tableName) { + super(manager); + this.tableName = tableName; + this.columnData = new LinkedHashMap<>(); + } + + @Override + public PreparedSQLUpdateAction build() { + + StringBuilder sqlBuilder = new StringBuilder(); + + sqlBuilder.append("UPDATE ").append(withBackQuote(getTableName())).append(" SET "); + + Iterator iterator = this.columnData.keySet().iterator(); + while (iterator.hasNext()) { + sqlBuilder.append(withBackQuote(iterator.next())).append(" = ?"); + if (iterator.hasNext()) sqlBuilder.append(" , "); + } + List allParams = new ArrayList<>(this.columnData.values()); + + if (hasConditions()) { + sqlBuilder.append(" ").append(buildConditionSQL()); + allParams.addAll(getConditionParams()); + } + + if (limit > 0) sqlBuilder.append(" ").append(buildLimitSQL()); + + return new PreparedSQLUpdateActionImpl<>(getManager(), Integer.class, sqlBuilder.toString(), allParams); + } + + @Override + public @NotNull String getTableName() { + return tableName; + } + + @Override + public UpdateBuilder addColumnValue(@NotNull String columnName, Object columnValue) { + Objects.requireNonNull(columnName, "columnName could not be null"); + this.columnData.put(columnName, columnValue); + return this; + } + + @Override + public UpdateBuilder setColumnValues(LinkedHashMap columnData) { + this.columnData = columnData; + return this; + } + + @Override + public UpdateBuilder setColumnValues(@NotNull String[] columnNames, @Nullable Object[] columnValues) { + Objects.requireNonNull(columnNames, "columnName could not be null"); + if (columnNames.length != columnValues.length) { + throw new RuntimeException("columnNames are not match with columnValues"); + } + LinkedHashMap columnData = new LinkedHashMap<>(); + for (int i = 0; i < columnNames.length; i++) { + columnData.put(columnNames[i], columnValues[i]); + } + return setColumnValues(columnData); + } + + + @Override + protected UpdateBuilder getThis() { + return this; + } +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/sql/manager/SQLManagerImpl.java b/src/main/java/com/io/yutian/elementoriginlib/sql/manager/SQLManagerImpl.java new file mode 100644 index 0000000..9e7d9dd --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/sql/manager/SQLManagerImpl.java @@ -0,0 +1,254 @@ +package com.io.yutian.elementoriginlib.sql.manager; + +import com.io.yutian.elementoriginlib.sql.action.PreparedSQLBatchUpdateActionImpl; +import com.io.yutian.elementoriginlib.sql.action.PreparedSQLUpdateActionImpl; +import com.io.yutian.elementoriginlib.sql.action.SQLUpdateActionImpl; +import com.io.yutian.elementoriginlib.sql.action.SQLUpdateBatchActionImpl; +import com.io.yutian.elementoriginlib.sql.api.SQLManager; +import com.io.yutian.elementoriginlib.sql.api.SQLQuery; +import com.io.yutian.elementoriginlib.sql.api.action.PreparedSQLUpdateAction; +import com.io.yutian.elementoriginlib.sql.api.action.PreparedSQLUpdateBatchAction; +import com.io.yutian.elementoriginlib.sql.api.action.SQLUpdateBatchAction; +import com.io.yutian.elementoriginlib.sql.api.builder.*; +import com.io.yutian.elementoriginlib.sql.api.function.SQLBiFunction; +import com.io.yutian.elementoriginlib.sql.api.function.SQLDebugHandler; +import com.io.yutian.elementoriginlib.sql.api.function.SQLExceptionHandler; +import com.io.yutian.elementoriginlib.sql.api.function.SQLFunction; +import com.io.yutian.elementoriginlib.sql.builder.impl.*; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.function.Supplier; + +public class SQLManagerImpl implements SQLManager { + + private final Logger LOGGER; + private final DataSource dataSource; + private final ConcurrentHashMap activeQuery = new ConcurrentHashMap<>(); + protected ExecutorService executorPool; + @NotNull Supplier debugMode = () -> Boolean.FALSE; + + @NotNull SQLExceptionHandler exceptionHandler; + @NotNull SQLDebugHandler debugHandler; + + public SQLManagerImpl(@NotNull DataSource dataSource) { + this(dataSource, null); + } + + public SQLManagerImpl(@NotNull DataSource dataSource, @Nullable String name) { + this(dataSource, LoggerFactory.getLogger(SQLManagerImpl.class), name); + } + + public SQLManagerImpl(@NotNull DataSource dataSource, @NotNull Logger logger, @Nullable String name) { + String managerName = "SQLManager" + (name != null ? "#" + name : ""); + this.LOGGER = logger; + this.dataSource = dataSource; + this.executorPool = SQLManager.defaultExecutorPool(managerName); + this.exceptionHandler = SQLExceptionHandler.detailed(getLogger()); + this.debugHandler = SQLDebugHandler.defaultHandler(getLogger()); + } + + @Override + public boolean isDebugMode() { + return this.debugMode.get(); + } + + @Override + public void setDebugMode(@NotNull Supplier<@NotNull Boolean> debugMode) { + this.debugMode = debugMode; + } + + @Override + public @NotNull SQLDebugHandler getDebugHandler() { + return this.debugHandler; + } + + @Override + public void setDebugHandler(@NotNull SQLDebugHandler debugHandler) { + this.debugHandler = debugHandler; + } + + @Override + public Logger getLogger() { + return LOGGER; + } + + public @NotNull ExecutorService getExecutorPool() { + return executorPool; + } + + public void setExecutorPool(@NotNull ExecutorService executorPool) { + this.executorPool = executorPool; + } + + @Override + public @NotNull DataSource getDataSource() { + return this.dataSource; + } + + @Override + public @NotNull Connection getConnection() throws SQLException { + return getDataSource().getConnection(); + } + + @Override + public @NotNull Map getActiveQuery() { + return this.activeQuery; + } + + @Override + public @NotNull SQLExceptionHandler getExceptionHandler() { + return this.exceptionHandler; + } + + @Override + public void setExceptionHandler(@Nullable SQLExceptionHandler handler) { + if (handler == null) this.exceptionHandler = SQLExceptionHandler.detailed(getLogger()); + else this.exceptionHandler = handler; + } + + @Override + public Integer executeSQL(String sql) { + return new SQLUpdateActionImpl<>(this, Integer.class, sql).execute(null); + } + + @Override + public Integer executeSQL(String sql, Object[] params) { + return new PreparedSQLUpdateActionImpl<>(this, Integer.class, sql, params).execute(null); + } + + @Override + public List executeSQLBatch(String sql, Iterable paramsBatch) { + return new PreparedSQLBatchUpdateActionImpl<>(this, Integer.class, sql).setAllParams(paramsBatch).execute(null); + } + + @Override + public List executeSQLBatch(@NotNull String sql, String... moreSQL) { + SQLUpdateBatchAction action = new SQLUpdateBatchActionImpl(this, sql); + if (moreSQL != null && moreSQL.length > 0) { + Arrays.stream(moreSQL).forEach(action::addBatch); + } + return action.execute(null); + } + + @Override + public @Nullable List executeSQLBatch(@NotNull Iterable sqlBatch) { + Iterator iterator = sqlBatch.iterator(); + if (!iterator.hasNext()) return null; // PLEASE GIVE IT SOMETHING + + SQLUpdateBatchAction action = new SQLUpdateBatchActionImpl(this, iterator.next()); + while (iterator.hasNext()) { + action.addBatch(iterator.next()); + } + + return action.execute(null); + } + + @Override + public CompletableFuture fetchMetadata(@NotNull SQLBiFunction reader) { + return CompletableFuture.supplyAsync(() -> { + try (Connection conn = getConnection()) { + return reader.apply(conn.getMetaData(), conn); + } catch (SQLException ex) { + throw new RuntimeException(ex); + } + }, this.executorPool); + } + + @Override + public CompletableFuture fetchMetadata(@NotNull SQLBiFunction supplier, + @NotNull SQLFunction<@NotNull ResultSet, R> reader) { + return fetchMetadata((meta, conn) -> { + try (ResultSet rs = supplier.apply(conn.getMetaData(), conn)) { + if (rs == null) throw new NullPointerException("Metadata返回的ResultSet为null。"); + else return reader.apply(rs); + } catch (SQLException ex) { + throw new RuntimeException(ex); + } + }); + } + + @Override + public TableCreateBuilder createTable(@NotNull String tableName) { + return new TableCreateBuilderImpl(this, tableName); + } + + @Override + public TableAlterBuilder alterTable(@NotNull String tableName) { + return new TableAlterBuilderImpl(this, tableName); + } + + @Override + public TableMetadataBuilder fetchTableMetadata(@NotNull String tablePattern) { + return new TableMetadataBuilderImpl(this, tablePattern); + } + + @Override + public QueryBuilder createQuery() { + return new QueryBuilderImpl(this); + } + + @Override + public InsertBuilder> createInsertBatch(@NotNull String tableName) { + return new InsertBuilderImpl>(this, tableName) { + @Override + public PreparedSQLUpdateBatchAction setColumnNames(List columnNames) { + return new PreparedSQLBatchUpdateActionImpl<>(getManager(), Integer.class, buildSQL(getTableName(), isIgnore(), columnNames)); + } + }; + } + + @Override + public InsertBuilder> createInsert(@NotNull String tableName) { + return new InsertBuilderImpl>(this, tableName) { + @Override + public PreparedSQLUpdateAction setColumnNames(List columnNames) { + return new PreparedSQLUpdateActionImpl<>(getManager(), Integer.class, buildSQL(getTableName(), isIgnore(), columnNames)); + } + }; + } + + @Override + public ReplaceBuilder> createReplaceBatch(@NotNull String tableName) { + return new ReplaceBuilderImpl>(this, tableName) { + @Override + public PreparedSQLUpdateBatchAction setColumnNames(List columnNames) { + return new PreparedSQLBatchUpdateActionImpl<>(getManager(), Integer.class, buildSQL(getTableName(), columnNames)); + } + }; + } + + @Override + public ReplaceBuilder> createReplace(@NotNull String tableName) { + return new ReplaceBuilderImpl>(this, tableName) { + @Override + public PreparedSQLUpdateAction setColumnNames(List columnNames) { + return new PreparedSQLUpdateActionImpl<>(getManager(), Integer.class, buildSQL(getTableName(), columnNames)); + } + }; + } + + @Override + public UpdateBuilder createUpdate(@NotNull String tableName) { + return new UpdateBuilderImpl(this, tableName); + } + + @Override + public DeleteBuilder createDelete(@NotNull String tableName) { + return new DeleteBuilderImpl(this, tableName); + } + + +} + diff --git a/src/main/java/com/io/yutian/elementoriginlib/sql/util/DatabaseDriverType.java b/src/main/java/com/io/yutian/elementoriginlib/sql/util/DatabaseDriverType.java new file mode 100644 index 0000000..3ba91e6 --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/sql/util/DatabaseDriverType.java @@ -0,0 +1,20 @@ +package com.io.yutian.elementoriginlib.sql.util; + +public enum DatabaseDriverType { + + MYSQL("com.mysql.jdbc.Driver"), + MYSQL_CJ("com.mysql.cj.jdbc.Driver"), + SQLITE("org.sqlite.JDBC"), + H2("org.h2.Driver"); + + private String driverClassName; + + DatabaseDriverType(String driverClassName) { + this.driverClassName = driverClassName; + } + + public String getDriverClassName() { + return driverClassName; + } + +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/sql/util/StatementUtil.java b/src/main/java/com/io/yutian/elementoriginlib/sql/util/StatementUtil.java new file mode 100644 index 0000000..a9c703a --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/sql/util/StatementUtil.java @@ -0,0 +1,210 @@ +package com.io.yutian.elementoriginlib.sql.util; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.sql.*; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +public class StatementUtil { + + /** + * 创建一个 {@link PreparedStatement} 。 + * + * @param connection 数据库连接 + * @param sql SQL语句,使用"?"做为占位符 + * @param params "?"所代表的对应参数列表 + * @return 完成参数填充的 {@link PreparedStatement} + */ + public static PreparedStatement createPrepareStatement( + Connection connection, String sql, Object[] params + ) throws SQLException { + return createPrepareStatement(connection, sql, params, false); + } + + /** + * 创建一个 {@link PreparedStatement} 。 + * + * @param connection 数据库连接 + * @param sql SQL语句,使用"?"做为占位符 + * @param params "?"所代表的对应参数列表 + * @param returnGeneratedKey 是否会返回自增主键 + * @return 完成参数填充的 {@link PreparedStatement} + */ + public static PreparedStatement createPrepareStatement( + Connection connection, String sql, Object[] params, boolean returnGeneratedKey + ) throws SQLException { + sql = sql.trim(); + PreparedStatement statement = connection.prepareStatement(sql, returnGeneratedKey ? Statement.RETURN_GENERATED_KEYS : Statement.NO_GENERATED_KEYS); + Map nullTypeMap = new HashMap<>(); + if (params != null) fillParams(statement, Arrays.asList(params), nullTypeMap); + statement.addBatch(); + return statement; + } + + /** + * 创建批量操作的一个 {@link PreparedStatement} 。 + * + * @param connection 数据库连接 + * @param sql SQL语句,使用"?"做为占位符 + * @param paramsBatch "?"所代表的对应参数列表 + * @return 完成参数填充的 {@link PreparedStatement} + */ + public static PreparedStatement createPrepareStatementBatch( + Connection connection, String sql, Iterable paramsBatch + ) throws SQLException { + return createPrepareStatementBatch(connection, sql, paramsBatch, false); + } + + /** + * 创建批量操作的一个 {@link PreparedStatement} 。 + * + * @param connection 数据库连接 + * @param sql SQL语句,使用"?"做为占位符 + * @param paramsBatch "?"所代表的对应参数列表 + * @param returnGeneratedKey 是否会返回自增主键 + * @return 完成参数填充的 {@link PreparedStatement} + */ + public static PreparedStatement createPrepareStatementBatch( + Connection connection, String sql, Iterable paramsBatch, boolean returnGeneratedKey + ) throws SQLException { + + sql = sql.trim(); + PreparedStatement statement = connection.prepareStatement(sql, returnGeneratedKey ? Statement.RETURN_GENERATED_KEYS : Statement.NO_GENERATED_KEYS); + Map nullTypeMap = new HashMap<>(); + for (Object[] params : paramsBatch) { + fillParams(statement, Arrays.asList(params), nullTypeMap); + statement.addBatch(); + } + + return statement; + } + + + /** + * 填充PreparedStatement的参数。 + * + * @param statement PreparedStatement + * @param params SQL参数 + * @return {@link PreparedStatement} 填充参数后的PreparedStatement + * @throws SQLException SQL执行异常 + */ + public static PreparedStatement fillParams( + PreparedStatement statement, Iterable params + ) throws SQLException { + return fillParams(statement, params, null); + } + + /** + * 填充PreparedStatement的参数。 + * + * @param statement PreparedStatement + * @param params SQL参数 + * @param nullCache null参数的类型缓存,避免循环中重复获取类型 + * @return 完成参数填充的 {@link PreparedStatement} + */ + public static PreparedStatement fillParams( + PreparedStatement statement, Iterable params, Map nullCache + ) throws SQLException { + if (null == params) { + return statement;// 无参数 + } + + int paramIndex = 1;//第一个参数从1计数 + for (Object param : params) { + setParam(statement, paramIndex++, param, nullCache); + } + return statement; + } + + /** + * 获取null字段对应位置的数据类型 + * 如果类型获取失败将使用默认的 {@link Types#VARCHAR} + * + * @param statement {@link PreparedStatement} + * @param paramIndex 参数序列,第一位从1开始 + * @return 数据类型,默认为 {@link Types#VARCHAR} + */ + public static int getNullType(PreparedStatement statement, int paramIndex) { + try { + ParameterMetaData pmd = statement.getParameterMetaData(); + return pmd.getParameterType(paramIndex); + } catch (SQLException ignore) { + return Types.VARCHAR; + } + } + + /** + * 为 {@link PreparedStatement} 设置单个参数 + * + * @param preparedStatement {@link PreparedStatement} + * @param paramIndex 参数序列,从1开始 + * @param param 参数,不能为{@code null} + * @param nullCache 用于缓存参数为null位置的类型,避免重复获取 + */ + private static void setParam( + PreparedStatement preparedStatement, int paramIndex, Object param, + Map nullCache + ) throws SQLException { + + if (param == null) { + Integer type = (null == nullCache) ? null : nullCache.get(paramIndex); + if (null == type) { + type = getNullType(preparedStatement, paramIndex); + if (null != nullCache) { + nullCache.put(paramIndex, type); + } + } + preparedStatement.setNull(paramIndex, type); + } + + // 针对UUID特殊处理,避免元数据直接传入 + if (param instanceof UUID) { + preparedStatement.setString(paramIndex, param.toString()); + return; + } + + // 针对StringBuilder或StringBuffer进行处理,避免元数据传入 + if (param instanceof StringBuilder || param instanceof StringBuffer) { + preparedStatement.setString(paramIndex, param.toString()); + return; + } + + // 日期特殊处理,默认按照时间戳传入,避免精度丢失 + if (param instanceof java.util.Date) { + if (param instanceof Date) { + preparedStatement.setDate(paramIndex, (Date) param); + } else if (param instanceof Time) { + preparedStatement.setTime(paramIndex, (Time) param); + } else { + preparedStatement.setTimestamp(paramIndex, new Timestamp(((java.util.Date) param).getTime())); + } + return; + } + + // 针对大数字类型的特殊处理 + if (param instanceof Number) { + if (param instanceof BigDecimal) { + // BigDecimal的转换交给JDBC驱动处理 + preparedStatement.setBigDecimal(paramIndex, (BigDecimal) param); + return; + } else if (param instanceof BigInteger) { + preparedStatement.setBigDecimal(paramIndex, new BigDecimal((BigInteger) param)); + return; + } + // 其它数字类型按照默认类型传入 + } + + if (param instanceof Enum) { + //枚举类采用 name() + preparedStatement.setString(paramIndex, ((Enum) param).name()); + return; + } + + // 其它参数类型直接传入 + preparedStatement.setObject(paramIndex, param); + } + +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/util/AESUtil.java b/src/main/java/com/io/yutian/elementoriginlib/util/AESUtil.java new file mode 100644 index 0000000..d842c98 --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/util/AESUtil.java @@ -0,0 +1,65 @@ +package com.io.yutian.elementoriginlib.util; + +import javax.crypto.Cipher; +import javax.crypto.KeyGenerator; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.Base64; + +public class AESUtil { + + private static final String KEY_ALGORITHM = "AES"; + private static final String DEFAULT_CIPHER_ALGORITHM = "AES/ECB/PKCS5Padding"; + + public static String getAESRandomKey() { + SecureRandom random = new SecureRandom(); + long randomKey = random.nextLong(); + return String.valueOf(randomKey); + } + + public static String encrypt(String content, String key) { + try { + Cipher cipher = Cipher.getInstance(DEFAULT_CIPHER_ALGORITHM); + byte[] byteContent = content.getBytes("utf-8"); + cipher.init(Cipher.ENCRYPT_MODE, getSecretKey(key)); + byte[] result = cipher.doFinal(byteContent); + return byte2Base64(result); + } catch (Exception ex) { + } + return null; + } + + public static String decrypt(String content, String key) { + try { + Cipher cipher = Cipher.getInstance(DEFAULT_CIPHER_ALGORITHM); + cipher.init(Cipher.DECRYPT_MODE, getSecretKey(key)); + byte[] result = cipher.doFinal(base642Byte(content)); + return new String(result, "utf-8"); + } catch (Exception ex) { + } + return null; + } + + private static SecretKeySpec getSecretKey(final String key) { + try { + KeyGenerator kg = KeyGenerator.getInstance(KEY_ALGORITHM); + SecureRandom random = SecureRandom.getInstance("SHA1PRNG"); + random.setSeed(key.getBytes()); + kg.init(128, random); + SecretKey secretKey = kg.generateKey(); + return new SecretKeySpec(secretKey.getEncoded(), KEY_ALGORITHM); + } catch (NoSuchAlgorithmException ex) { + } + return null; + } + + private static String byte2Base64(byte[] bytes) { + return Base64.getEncoder().encodeToString(bytes); + } + + private static byte[] base642Byte(String base64Key) { + return Base64.getDecoder().decode(base64Key); + } +} \ No newline at end of file diff --git a/src/main/java/com/io/yutian/elementoriginlib/util/ClassUtil.java b/src/main/java/com/io/yutian/elementoriginlib/util/ClassUtil.java new file mode 100644 index 0000000..401fa6e --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/util/ClassUtil.java @@ -0,0 +1,54 @@ +package com.io.yutian.elementoriginlib.util; + +import org.bukkit.plugin.Plugin; + +import java.util.HashMap; +import java.util.Map; + +public class ClassUtil { + + public static boolean isAssignableFrom(Object object, Class clazz) { + if (object == null) { + return false; + } + Class c = PRIMITIVE_TO_WRAPPER.getOrDefault(clazz, clazz); + if (c.isAssignableFrom(object.getClass())) { + return true; + } + if (object.getClass().equals(Integer.class)) { + if (c.equals(Double.class) || c.equals(Float.class) || c.equals(Long.class)) { + return true; + } + } + if (object.getClass().equals(String.class)) { + String s = (String) object; + if (c.equals(Double.class)) { + return StringUtil.isDouble(s); + } else if (c.equals(Float.class)) { + return StringUtil.isFloat(s); + } else if (c.equals(Long.class)) { + return StringUtil.isLong(s); + } + } + return false; + } + + public static String getJar(Plugin plugin) { + String p = plugin.getClass().getProtectionDomain().getCodeSource().getLocation().getPath(); + return p.substring(1); + } + + public static final Map, Class> PRIMITIVE_TO_WRAPPER = new HashMap<>(); + + static { + PRIMITIVE_TO_WRAPPER.put(boolean.class, Boolean.class); + PRIMITIVE_TO_WRAPPER.put(byte.class, Byte.class); + PRIMITIVE_TO_WRAPPER.put(short.class, Short.class); + PRIMITIVE_TO_WRAPPER.put(char.class, Character.class); + PRIMITIVE_TO_WRAPPER.put(int.class, Integer.class); + PRIMITIVE_TO_WRAPPER.put(long.class, Long.class); + PRIMITIVE_TO_WRAPPER.put(float.class, Float.class); + PRIMITIVE_TO_WRAPPER.put(double.class, Double.class); + } + +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/util/ComponentBuilder.java b/src/main/java/com/io/yutian/elementoriginlib/util/ComponentBuilder.java new file mode 100644 index 0000000..9cf15b4 --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/util/ComponentBuilder.java @@ -0,0 +1,47 @@ +package com.io.yutian.elementoriginlib.util; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.event.ClickEvent; +import net.kyori.adventure.text.event.HoverEvent; + +public class ComponentBuilder { + + private Component component; + + public ComponentBuilder() { + component = Component.text(""); + } + + public ComponentBuilder add(Component component) { + component.append(component); + return this; + } + + public ComponentBuilder add(String text) { + return add(Component.text(text)); + } + + public ComponentBuilder add(String text, ClickEvent clickEvent) { + Component component1 = Component.text(text); + component1.clickEvent(clickEvent); + return add(component1); + } + + public ComponentBuilder add(String text, ClickEvent clickEvent, HoverEvent hoverEvent) { + Component component1 = Component.text(text); + component.clickEvent(clickEvent); + component.hoverEvent(hoverEvent); + return add(component1); + } + + public ComponentBuilder add(String text, HoverEvent hoverEvent) { + Component component1 = Component.text(text); + component.hoverEvent(hoverEvent); + return add(component1); + } + + public Component build() { + return component; + } + +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/util/CompressUtils.java b/src/main/java/com/io/yutian/elementoriginlib/util/CompressUtils.java new file mode 100644 index 0000000..9bbea25 --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/util/CompressUtils.java @@ -0,0 +1,106 @@ +package com.io.yutian.elementoriginlib.util; + +import com.github.luben.zstd.Zstd; +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.compress.archivers.ArchiveEntry; +import org.apache.commons.compress.archivers.ArchiveInputStream; +import org.apache.commons.compress.archivers.ArchiveOutputStream; +import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; +import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream; +import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream; +import org.apache.commons.compress.utils.IOUtils; + +import java.io.*; +import java.nio.file.*; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.Objects; + +public class CompressUtils { + + public static String zstdString(String unzip) { + byte[] compress = Zstd.compress(unzip.getBytes()); + return Base64.encodeBase64String(compress); + } + public static String unZstdString(String zip) { + byte[] decode = Base64.decodeBase64(zip); + Long size = Zstd.decompressedSize(decode); + byte[] decompress = Zstd.decompress(decode, size.intValue()); + return new String(decompress); + } + + public static void zip(File file, File file1) { + try { + OutputStream outputStream = new FileOutputStream(file1); + zip(file, outputStream); + } catch (Exception e) { + e.printStackTrace(); + } + } + + public static void zip(File file, OutputStream outputStream) throws IOException { + try (BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(outputStream); + ArchiveOutputStream out = new ZipArchiveOutputStream(bufferedOutputStream)) { + Path start = Paths.get(file.toURI()); + Files.walkFileTree(start, new SimpleFileVisitor<>() { + + @Override + public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { + ArchiveEntry entry = new ZipArchiveEntry(dir.toFile(), start.relativize(dir).toString()); + out.putArchiveEntry(entry); + out.closeArchiveEntry(); + return super.preVisitDirectory(dir, attrs); + } + + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + try (InputStream input = new FileInputStream(file.toFile())) { + ArchiveEntry entry = new ZipArchiveEntry(file.toFile(), start.relativize(file).toString()); + out.putArchiveEntry(entry); + IOUtils.copy(input, out); + out.closeArchiveEntry(); + } + return super.visitFile(file, attrs); + } + + }); + + } + } + + public static void unzip(File file, String destDir) { + try { + try (InputStream inputStream = new FileInputStream(file);) { + unzip(inputStream, destDir); + } + } catch (Exception e) { + e.printStackTrace(); + } + + } + + public static void unzip(InputStream inputStream, String destDir) { + try { + try (BufferedInputStream bufferedInputStream = new BufferedInputStream(inputStream); ArchiveInputStream in = new ZipArchiveInputStream(bufferedInputStream)) { + ArchiveEntry entry; + while (Objects.nonNull(entry = in.getNextEntry())) { + File file = Paths.get(destDir, entry.getName()).toFile(); + if (in.canReadEntryData(entry)) { + if (entry.isDirectory()) { + if (!file.exists()) { + file.mkdirs(); + } + } else { + try (OutputStream out = new FileOutputStream(file)) { + IOUtils.copy(in, out); + } + } + } + } + } + } catch (Exception e) { + e.printStackTrace(); + } + + } + +} \ No newline at end of file diff --git a/src/main/java/com/io/yutian/elementoriginlib/util/EntityUtil.java b/src/main/java/com/io/yutian/elementoriginlib/util/EntityUtil.java new file mode 100644 index 0000000..8de1426 --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/util/EntityUtil.java @@ -0,0 +1,40 @@ +package com.io.yutian.elementoriginlib.util; + +import com.google.common.base.Charsets; +import com.mojang.authlib.GameProfile; +import org.bukkit.Bukkit; +import org.bukkit.World; +import org.bukkit.attribute.Attribute; +import org.bukkit.entity.Entity; +import org.bukkit.entity.LivingEntity; +import org.bukkit.entity.Player; + +import java.util.UUID; + +public class EntityUtil { + + public static Entity getEntity(World world, UUID uuid) { + for (Entity entity : world.getEntities()) { + if (entity.getUniqueId().equals(uuid)) { + return entity; + } + } + return null; + } + + private static UUID getPlayerUniqueId(String name) { + Player player = Bukkit.getPlayerExact(name); + if (player != null) { + return player.getUniqueId(); + } + GameProfile profile = new GameProfile(UUID.nameUUIDFromBytes(("OfflinePlayer:" + name).getBytes(Charsets.UTF_8)), name); + return profile != null ? profile.getId() : null; + } + + public static void recoverHealth(LivingEntity livingEntity, double value) { + if (value > 0.0) { + livingEntity.setHealth(Math.min(livingEntity.getAttribute(Attribute.GENERIC_MAX_HEALTH).getValue(), livingEntity.getHealth() + value)); + } + } + +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/util/FileUtil.java b/src/main/java/com/io/yutian/elementoriginlib/util/FileUtil.java new file mode 100644 index 0000000..400ca7b --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/util/FileUtil.java @@ -0,0 +1,102 @@ +package com.io.yutian.elementoriginlib.util; + +import org.bukkit.plugin.Plugin; + +import java.io.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class FileUtil { + + public static List getFilesForFolder(File folder) { + List files = new ArrayList<>(); + if (!folder.isDirectory()) { + throw new IllegalArgumentException("File \"" + folder.getName() + "\" is not a directory"); + } + File[] listFiles = folder.listFiles(); + for (File file : listFiles) { + if (file.isDirectory()) { + files.addAll(getFilesForFolder(file)); + } else { + files.add(file); + } + } + return files; + } + + public static List getSubFolders(File folder) { + List subFolders = new ArrayList<>(); + if (!folder.isDirectory()) { + throw new IllegalArgumentException("File \"" + folder.getName() + "\" is not a directory"); + } + File[] listFiles = folder.listFiles(); + for (File file : listFiles) { + if (file.isDirectory()) { + subFolders.add(file); + } + } + return subFolders; + } + + public static void copy(File inFile, File outFile) { + if (inFile.isDirectory()) { + copyDir(inFile, outFile, new String[0]); + } else { + copyFile(inFile, outFile); + } + } + + public static boolean copyFile(File inFile, File outFile) { + return org.bukkit.util.FileUtil.copy(inFile, outFile); + } + + public static void copyDir(File inDir, File outDir, String... exclude) { + if (!outDir.exists()) { + outDir.mkdir(); + } + String[] list = inDir.list(); + for (String p : list) { + if (!Arrays.asList(exclude).contains(p)) { + copy(new File(inDir, p), new File(outDir, p)); + } + } + } + + public static boolean removeDir(File dir) { + if (dir.isDirectory()) { + for (File f : dir.listFiles()) { + if (!removeDir(f)) { + return false; + } + } + } + return dir.delete(); + } + + public static FileOutputStream getOutputStream(File file) { + try { + return new FileOutputStream(file); + } catch (Exception e) { + return null; + } + } + + public static FileInputStream getInputStream(File file) { + try { + return new FileInputStream(file); + } catch (Exception e) { + return null; + } + } + + public static File getFile(Plugin plugin, String path, String fileName) { + File p = (path == null || path.equals("")) ? plugin.getDataFolder() : new File(plugin.getDataFolder() + File.separator + path); + if (!p.exists()) { + p.mkdirs(); + } + File file = new File(p + File.separator + fileName); + return file; + } + +} \ No newline at end of file diff --git a/src/main/java/com/io/yutian/elementoriginlib/util/InventoryUtil.java b/src/main/java/com/io/yutian/elementoriginlib/util/InventoryUtil.java new file mode 100644 index 0000000..24e0fa0 --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/util/InventoryUtil.java @@ -0,0 +1,28 @@ +package com.io.yutian.elementoriginlib.util; + +import org.bukkit.Material; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; + +public class InventoryUtil { + + public static void takeItemStack(Inventory inventory, ItemStack itemStack, int amount) { + int index = -1; + int nowAmount = amount; + while (index < inventory.getSize() && nowAmount > 0) { + ++index; + ItemStack i = inventory.getItem(index); + if (i != null && ItemStackUtil.matches(itemStack, i)) { + int k = i.getAmount(); + if (k < nowAmount) { + i.setType(Material.AIR); + } else { + int l = k - nowAmount; + i.setAmount(l); + } + nowAmount -= k; + } + } + } + +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/util/ItemStackBuilder.java b/src/main/java/com/io/yutian/elementoriginlib/util/ItemStackBuilder.java new file mode 100644 index 0000000..e52b4ac --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/util/ItemStackBuilder.java @@ -0,0 +1,387 @@ +package com.io.yutian.elementoriginlib.util; + +import com.mojang.authlib.GameProfile; +import com.mojang.authlib.properties.Property; +import org.bukkit.*; +import org.bukkit.attribute.Attribute; +import org.bukkit.attribute.AttributeModifier; +import org.bukkit.block.banner.Pattern; +import org.bukkit.enchantments.Enchantment; +import org.bukkit.inventory.EquipmentSlot; +import org.bukkit.inventory.ItemFlag; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.EnchantmentStorageMeta; +import org.bukkit.inventory.meta.FireworkMeta; +import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.inventory.meta.SkullMeta; +import org.bukkit.potion.PotionEffect; + +import java.lang.reflect.Field; +import java.util.*; + +public class ItemStackBuilder { + + private ItemStack itemStack; + + private Material type; + + private int amount; + + private short data; + + private ItemMeta meta; + + private String displayName; + + private int customModelData; + + private List lore; + + private Map enchants; + + private Set itemFlags; + + private List potionEffects; + private List patterns; + private List fireworkEffects; + private String skullTextures; + private UUID skullOwner; + + public ItemStackBuilder(Material material) { + this.itemStack = new ItemStack(material); + this.type = material; + this.amount = 1; + this.data = 0; + this.meta = this.itemStack.getItemMeta(); + this.displayName = this.meta.getDisplayName(); + this.customModelData = this.meta.hasCustomModelData() ? this.meta.getCustomModelData() : -1; + this.lore = new ArrayList<>(); + this.enchants = new HashMap<>(); + this.itemFlags = new HashSet<>(); + this.potionEffects = new ArrayList<>(); + this.patterns = new ArrayList<>(); + this.fireworkEffects = new ArrayList<>(); + } + + public ItemStackBuilder(ItemStack item) { + this.itemStack = item.clone(); + this.type = item.getType(); + this.amount = item.getAmount(); + this.data = item.getDurability(); + this.meta = item.getItemMeta(); + this.displayName = this.meta.getDisplayName(); + this.customModelData = this.meta.hasCustomModelData() ? this.meta.getCustomModelData() : -1; + this.lore = this.meta.getLore() == null ? new ArrayList<>() : meta.getLore(); + this.enchants = this.meta.getEnchants(); + this.itemFlags = this.meta.getItemFlags(); + this.potionEffects = new ArrayList<>(); + this.patterns = new ArrayList<>(); + this.fireworkEffects = new ArrayList<>(); + } + + public ItemStack build() { + ItemStack item = new ItemStack(type, amount); + item.setType(type); + item.setAmount(amount); + ItemMeta itemMeta = item.getItemMeta(); + if (displayName != null) { + itemMeta.setDisplayName(ChatColor.translateAlternateColorCodes('&', displayName)); + } + if (enchants != null) { + item.addUnsafeEnchantments(enchants); + } + if (lore != null) { + List l = new ArrayList<>(); + lore.forEach((s) -> { + l.add(ChatColor.translateAlternateColorCodes('&', s)); + }); + itemMeta.setLore(l); + } + if (this.customModelData != -1) { + itemMeta.setCustomModelData(customModelData); + } + if (itemFlags != null) { + for (ItemFlag flag : itemFlags) { + itemMeta.addItemFlags(flag); + } + } + if (fireworkEffects != null && fireworkEffects.size() > 0 && (type.equals(Material.FIREWORK_STAR) || type.equals(Material.FIREWORK_ROCKET))) { + FireworkMeta fireworkMeta = (FireworkMeta) itemMeta; + for (FireworkEffect effect : fireworkEffects) { + fireworkMeta.addEffect(effect); + } + } + if (skullTextures != null && type.equals(Material.PLAYER_HEAD)) { + SkullMeta skullMeta = (SkullMeta) itemMeta; + String uuid = UUID.randomUUID().toString(); + + GameProfile profile = new GameProfile(UUID.fromString(uuid), null); + profile.getProperties().put("textures", new Property("textures", skullTextures)); + Field profileField; + try { + profileField = skullMeta.getClass().getDeclaredField("profile"); + profileField.setAccessible(true); + profileField.set(skullMeta, profile); + } catch (NullPointerException | NoSuchFieldException | IllegalArgumentException | IllegalAccessException e1) { + } + } else if (skullOwner != null && type.equals(Material.PLAYER_HEAD)) { + SkullMeta skullMeta = (SkullMeta) itemMeta; + OfflinePlayer offlinePlayer = Bukkit.getOfflinePlayer(skullOwner); + if (offlinePlayer != null) { + skullMeta.setOwningPlayer(Bukkit.getOfflinePlayer(skullOwner)); + } + } + item.setItemMeta(itemMeta); + return item; + } + + public int getAmount() { + return amount; + } + + public Material getType() { + return type; + } + + public List getLore() { + return lore; + } + + public int getCustomModelData() { + return customModelData; + } + + public Map getEnchants() { + return enchants; + } + + public Set getItemFlags() { + return itemFlags; + } + + public List getPotionEffects() { + return potionEffects; + } + + public List getFireworkEffects() { + return fireworkEffects; + } + + public List getPatterns() { + return patterns; + } + + public String getSkullTextures() { + return skullTextures; + } + + public String getDisplayName() { + return displayName; + } + + public short getData() { + return data; + } + + public UUID getSkullOwner() { + return skullOwner; + } + + public ItemStackBuilder setItemStack(ItemStack item) { + this.itemStack = item; + return this; + } + + public ItemStackBuilder setType(Material type) { + this.type = type; + return this; + } + + public ItemStackBuilder setCustomModelData(int customModelData) { + this.customModelData = customModelData; + return this; + } + + public ItemStackBuilder setAmount(int amount) { + this.amount = amount; + return this; + } + + public ItemStackBuilder setData(int data) { + if (data > 255) { + data = 255; + } + this.data = (short) data; + return this; + } + + public ItemStackBuilder setData(short data) { + this.data = data; + return this; + } + + public ItemStackBuilder setItemMeta(ItemMeta meta) { + this.meta = meta; + return this; + } + + public ItemStackBuilder setDisplayName(String name) { + this.displayName = name; + return this; + } + + public ItemStackBuilder setLore(List loreList) { + this.lore = loreList; + return this; + } + + public ItemStackBuilder setLore(String... lores) { + this.lore = new ArrayList<>(Arrays.asList(lores)); + return this; + } + + public ItemStackBuilder addLore(String lore) { + this.lore.add(lore); + return this; + } + + public ItemStackBuilder addLore(String... lores) { + this.lore.addAll(Arrays.asList(lores)); + return this; + } + + public ItemStackBuilder addLore(List lores) { + for (String lore : lores) { + this.lore.add(lore); + } + return this; + } + + public ItemStackBuilder addAttributeModifier(Attribute attribute, AttributeModifier attributeModifier) { + this.meta.addAttributeModifier(attribute, attributeModifier); + return this; + } + + public ItemStackBuilder removeAttributeModifier(Attribute attribute) { + this.meta.removeAttributeModifier(attribute); + return this; + } + + public ItemStackBuilder removeAttributeModifier(EquipmentSlot equipmentSlot) { + this.meta.removeAttributeModifier(equipmentSlot); + return this; + } + + public ItemStackBuilder removeAttributeModifier(Attribute attribute, AttributeModifier attributeModifier) { + this.meta.removeAttributeModifier(attribute, attributeModifier); + return this; + } + + public ItemStackBuilder addEnchant(Enchantment ench, int level) { + this.enchants.put(ench, level); + return this; + } + + public ItemStackBuilder removeEnchant(Enchantment ench) { + this.enchants.remove(ench); + return this; + } + + public ItemStackBuilder setEnchants(Map enchants) { + this.enchants = enchants; + return this; + } + + public ItemStackBuilder setEnchants(List enchants) { + this.enchants = getEnchantsFromList(enchants); + return this; + } + + public ItemStackBuilder setItemFlags(Set itemFlags) { + this.itemFlags = itemFlags; + return this; + } + + public ItemStackBuilder addItemFlags(ItemFlag[] itemFlags) { + this.itemFlags.addAll(Arrays.asList(itemFlags)); + return this; + } + + public ItemStackBuilder addItemFlags(ItemFlag itemFlag) { + this.itemFlags.add(itemFlag); + return this; + } + + public ItemStackBuilder addPotionEffect(PotionEffect potionEffect) { + this.potionEffects.add(potionEffect); + return this; + } + + public ItemStackBuilder addPattern(Pattern pattern) { + this.patterns.add(pattern); + return this; + } + + public ItemStackBuilder addFireworkEffect(FireworkEffect fireworkEffect) { + this.fireworkEffects.add(fireworkEffect); + return this; + } + + public ItemStackBuilder setSkullTextures(String skullTextures) { + this.skullTextures = skullTextures; + return this; + } + + public ItemStackBuilder setSkullOwner(UUID skullOwner) { + this.skullOwner = skullOwner; + return this; + } + + protected Map getEnchantsFromList(List enchants) { + Map map = new HashMap<>(); + if (enchants == null) { + return map; + } + for (String s : enchants) { + if (s.contains(":")) { + String[] part = s.split(":"); + Enchantment en = Enchantment.getByName(part[0]); + if (en == null) { + continue; + } + map.put(en, Integer.parseInt(part[1])); + } + } + return map; + } + + protected static ItemStack setEnchants(ItemStack stack, List enchants) { + if (enchants == null) { + return stack; + } + for (String s : enchants) { + if (s.contains(":")) { + String[] part = s.split(":"); + Enchantment en = Enchantment.getByName(part[0]); + if (en == null) { + continue; + } + if (stack.getType() != Material.ENCHANTED_BOOK) { + stack.addUnsafeEnchantment(en, Integer.parseInt(part[1])); + } else { + EnchantmentStorageMeta esm = (EnchantmentStorageMeta) stack.getItemMeta(); + esm.addStoredEnchant(en, Integer.parseInt(part[1]), true); + stack.setItemMeta((ItemMeta) esm); + } + } + } + return stack; + } + + private void setField(Object instance, String name, Object value) throws ReflectiveOperationException { + Field field = instance.getClass().getDeclaredField(name); + field.setAccessible(true); + field.set(instance, value); + } + +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/util/ItemStackUtil.java b/src/main/java/com/io/yutian/elementoriginlib/util/ItemStackUtil.java new file mode 100644 index 0000000..cdcb578 --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/util/ItemStackUtil.java @@ -0,0 +1,99 @@ +package com.io.yutian.elementoriginlib.util; + +import com.io.yutian.elementoriginlib.nbt.NBTCompound; +import com.io.yutian.elementoriginlib.nbt.NBTItem; +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; + +public class ItemStackUtil { + + public static List mergeItems(ItemStack item, int amount) { + int maxStackSize = item.getMaxStackSize(); + int totalAmount = amount; + int remainder = totalAmount % maxStackSize; + int quotient = totalAmount / maxStackSize; + List items = new ArrayList<>(); + for (int i = 0; i < quotient; i++) { + ItemStack itemStack = item.clone(); + itemStack.setAmount(maxStackSize); + items.add(itemStack); + } + ItemStack itemStack = item.clone(); + itemStack.setAmount(remainder); + items.add(itemStack); + return items; + } + + public static boolean matches(@Nullable ItemStack original, @Nullable ItemStack tester) { + if (original == null || tester == null) { + return false; + } + original = original.clone(); + original.setAmount(1); + tester = tester.clone(); + tester.setAmount(1); + Material comparisonType = original.getType().isLegacy() ? Bukkit.getUnsafe().fromLegacy(original.getData(), true) : original.getType(); + boolean flag = comparisonType == tester.getType() && original.getDurability() == tester.getDurability() && original.hasItemMeta() == tester.hasItemMeta() && (!original.hasItemMeta() || Bukkit.getItemFactory().equals(original.getItemMeta(), tester.getItemMeta())); + if (!flag) { + return false; + } + NBTItem nbtOriginal = new NBTItem(original); + NBTItem nbtTester = new NBTItem(tester); + NBTCompound nbtCompound0 = nbtOriginal.getTag(); + NBTCompound nbtCompound1 = nbtTester.getTag(); + return nbtCompound0.equals(nbtCompound1); + } + + public static int countItems(Inventory inventory, @NotNull ItemStack item) { + if (inventory == null) { + return 0; + } + int count = 0; + for (ItemStack itemStack : inventory) { + if (itemStack == null || itemStack.getType() == Material.AIR) { + continue; + } + if (matches(item, itemStack)) { + count+= itemStack.getAmount(); + } + } + return count; + } + + public static int countSpace(Inventory inventory, @NotNull ItemStack item) { + if (inventory == null) { + return 0; + } + int count = 0; + int itemMaxStackSize = item.getMaxStackSize(); + for (ItemStack itemStack : inventory) { + if (itemStack == null || itemStack.getType() == Material.AIR) { + count+=itemMaxStackSize; + } else if (matches(item, itemStack)) { + count+=itemStack.getAmount() >= itemMaxStackSize ? 0 : itemMaxStackSize - itemStack.getAmount(); + } + } + return count; + } + + public static String getItemName(ItemStack itemStack) { + ItemMeta meta = itemStack.getItemMeta(); + if (meta != null && meta.hasDisplayName()) { + return meta.getDisplayName(); + } + Material material = itemStack.getType(); + String id = material.name().toLowerCase(); + boolean isBlock = material.isBlock(); + String prefix = isBlock ? "block" : "item"; + return LangUtil.getLang(prefix+".minecraft."+id); + } + +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/util/LangUtil.java b/src/main/java/com/io/yutian/elementoriginlib/util/LangUtil.java new file mode 100644 index 0000000..d7624c4 --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/util/LangUtil.java @@ -0,0 +1,41 @@ +package com.io.yutian.elementoriginlib.util; + +import com.io.yutian.elementoriginlib.ElementOriginLib; +import com.io.yutian.elementoriginlib.expiringmap.ExpirationPolicy; +import com.io.yutian.elementoriginlib.expiringmap.ExpiringMap; +import org.json.JSONObject; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.concurrent.TimeUnit; + +public class LangUtil { + + private static ExpiringMap expiringMap = ExpiringMap.builder().expiration(3, TimeUnit.MINUTES).expirationPolicy(ExpirationPolicy.CREATED).build(); + private static JSONObject jsonObject = new JSONObject(); + + public static String getLang(String key) { + if (expiringMap.containsKey(key)) { + return expiringMap.get(key); + } + String value = jsonObject.getString(key); + expiringMap.put(key, value); + return value; + } + + public static void init() { + File file = FileUtil.getFile(ElementOriginLib.inst(), "", "zh_cn.json"); + if (!file.exists()) { + ElementOriginLib.inst().saveResource("zh_cn.json", false); + } + try { + String s = new String(Files.readAllBytes(Paths.get(file.toURI()))); + jsonObject = new JSONObject(s); + } catch (IOException e) { + e.printStackTrace(); + } + } + +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/util/MathUtil.java b/src/main/java/com/io/yutian/elementoriginlib/util/MathUtil.java new file mode 100644 index 0000000..e4eb3ca --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/util/MathUtil.java @@ -0,0 +1,29 @@ +package com.io.yutian.elementoriginlib.util; + +import java.math.BigDecimal; +import java.math.RoundingMode; + +public class MathUtil { + + public static double round(double d) { + return new BigDecimal(Double.toString(d)).setScale(2, RoundingMode.HALF_UP).doubleValue(); + } + + public static double round(double d, int scale) { + return new BigDecimal(Double.toString(d)).setScale(scale, RoundingMode.HALF_UP).doubleValue(); + } + + public static int clamp(int value, int min, int max) { + return Math.max(min, Math.min(max, value)); + } + + public static double clamp(double value, double min, double max) { + return Math.max(min, Math.min(max, value)); + } + + public static float clamp(float value, float min, float max) { + return Math.max(min, Math.min(max, value)); + } + + +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/util/MojongAPI.java b/src/main/java/com/io/yutian/elementoriginlib/util/MojongAPI.java new file mode 100644 index 0000000..cd02ad2 --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/util/MojongAPI.java @@ -0,0 +1,76 @@ +package com.io.yutian.elementoriginlib.util; + +import org.json.JSONObject; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.UUID; + +public class MojongAPI { + + public static final String uuidURL = "https://sessionserver.mojang.com/session/minecraft/profile/"; + public static final String nameURL = "https://api.mojang.com/users/profiles/minecraft/"; + + public static JSONObject getPlayerJSON(UUID uuid, String name) throws IOException { + URL url; + + if (name != null) { + url = new URL(nameURL + name); + } else { + url = new URL(uuidURL + uuid); + } + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setUseCaches(false); + connection.setInstanceFollowRedirects(true); + connection.setDoOutput(true); + connection.setDoInput(true); + connection.setRequestMethod("GET"); + + BufferedReader bufferedReader = new BufferedReader( + new InputStreamReader(connection.getInputStream())); + String line; + StringBuilder stringBuilder = new StringBuilder(); + while ((line = bufferedReader.readLine()) != null) { + stringBuilder.append(line); + } + bufferedReader.close(); + if (!stringBuilder.toString().startsWith("{")) { + return null; + } + return new JSONObject(stringBuilder.toString()); + } + + public static UUID getUUID(String name) { + try { + String uuidTemp = getPlayerJSON(null, name).getString("id"); + String uuid = ""; + for (int i = 0; i <= 31; i++) { + uuid = uuid + uuidTemp.charAt(i); + if (i == 7 || i == 11 || i == 15 || i == 19) { + uuid = uuid + "-"; + } + } + if (getPlayerJSON(null, name) != null) { + return UUID.fromString(uuid); + } + } catch (Exception e) { + return null; + } + return null; + } + + public static String getName(UUID uuid) { + try { + if (getPlayerJSON(uuid, null) != null) { + return getPlayerJSON(uuid, null).getString("name"); + } + return null; + } catch (Exception e) { + return null; + } + } + +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/util/Pair.java b/src/main/java/com/io/yutian/elementoriginlib/util/Pair.java new file mode 100644 index 0000000..92e3070 --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/util/Pair.java @@ -0,0 +1,42 @@ +package com.io.yutian.elementoriginlib.util; + +public class Pair { + + protected final L left; + protected final R right; + + public Pair(L left, R right) { + this.left = left; + this.right = right; + } + + public static Pair of(K left, V right) { + return new Pair(left, right); + } + + public L left() { + return this.left; + } + + public R right() { + return this.right; + } + + public L first() { + return this.left(); + } + + public R second() { + return this.right(); + } + + public L key() { + return this.left(); + } + + public R value() { + return this.right(); + } + + +} \ No newline at end of file diff --git a/src/main/java/com/io/yutian/elementoriginlib/util/PlayerInventoryUtil.java b/src/main/java/com/io/yutian/elementoriginlib/util/PlayerInventoryUtil.java new file mode 100644 index 0000000..0ee8092 --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/util/PlayerInventoryUtil.java @@ -0,0 +1,93 @@ +package com.io.yutian.elementoriginlib.util; + +import org.bukkit.Material; +import org.bukkit.entity.Item; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; + +import java.util.function.Predicate; + +public class PlayerInventoryUtil { + + public static void giveItemStack(Player player, ItemStack itemStack) { + if (PlayerInventoryUtil.hasFreeSlot(player)) { + player.getInventory().addItem(itemStack); + } else { + Item item = player.getLocation().getWorld().dropItem(player.getLocation(), itemStack); + item.setOwner(player.getUniqueId()); + item.setThrower(player.getUniqueId()); + item.setPickupDelay(20); + } + } + + public static boolean hasItemStack(Player player, Predicate predicate) { + return hasItemStack(player, predicate, 1); + } + + public static boolean hasItemStack(Player player, Predicate predicate, int amount) { + int index = -1; + int a = 0; + while (index < 35) { + index++; + ItemStack i = player.getInventory().getItem(index); + if (i == null) { + continue; + } + if (predicate.test(i)) { + a += i.getAmount(); + } + } + return a >= amount; + } + + public static void takeItemStack(Player player, Predicate predicate) { + takeItemStack(player, predicate, 1); + } + + public static void takeItemStack(Player player, Predicate predicate, int amount) { + int index = -1; + int a = amount; + while (index < 35) { + if (a <= 0) { + break; + } + index++; + ItemStack i = player.getInventory().getItem(index); + if (i == null) { + continue; + } + if (predicate.test(i)) { + int k = i.getAmount(); + if (k < a) { + i.setType(Material.AIR); + } else { + int l = k - a; + i.setAmount(l); + } + a -= k; + } + } + } + + public static boolean hasFreeSlot(Player player) { + return getFreeSize(player) > 0; + } + + public static int getFreeSize(Player player) { + int index = -1; + int freesize = 0; + while (index < 35) { + index++; + ItemStack i = player.getInventory().getItem(index); + if (i == null) { + freesize++; + continue; + } + if (i.getType().equals(Material.AIR)) { + freesize++; + } + } + return freesize; + } + +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/util/RandomBuilder.java b/src/main/java/com/io/yutian/elementoriginlib/util/RandomBuilder.java new file mode 100644 index 0000000..3d061bd --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/util/RandomBuilder.java @@ -0,0 +1,83 @@ +package com.io.yutian.elementoriginlib.util; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Random; + +public class RandomBuilder { + + private Map valueMap; + + private List elementList = new ArrayList<>(); + private double maxElement = 0; + + public RandomBuilder(Map map) { + valueMap = map; + + double minElement = 0; + + for (Map.Entry entry : map.entrySet()) { + minElement = maxElement; + maxElement += entry.getValue(); + elementList.add(new Element(entry.getKey(), minElement, maxElement)); + } + } + + public T random() { + return random(new Random()); + } + + public T random(Random random) { + double d = random.nextDouble() * maxElement; + if (d == 0) { + d = 0.1 * maxElement; + } + int size = valueMap.size(); + for (int i = 0; i < elementList.size(); i++) { + Element element = elementList.get(i); + if (element.isContainKey(d)) { + return element.getValue(); + } + } + return null; + } + + public double getMaxElement() { + return maxElement; + } + + public List getElementList() { + return elementList; + } + + public Map getValueMap() { + return valueMap; + } + + private class Element { + + private T value; + + private final double minElement; + private final double maxElement; + + public Element(T value, double element0, double element1) { + this.value = value; + this.minElement = Math.min(element0, element1); + this.maxElement = Math.max(element0, element1); + } + + public T getValue() { + return value; + } + + public boolean isContainKey(double element) { + return element > minElement && element <= maxElement; + } + + } + + + +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/util/RandomUtil.java b/src/main/java/com/io/yutian/elementoriginlib/util/RandomUtil.java new file mode 100644 index 0000000..93505a5 --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/util/RandomUtil.java @@ -0,0 +1,59 @@ +package com.io.yutian.elementoriginlib.util; + +import org.bukkit.Color; +import org.bukkit.Location; +import org.bukkit.World; + +import java.util.List; +import java.util.Random; + +public class RandomUtil { + + public static boolean random(double d) { + return d >= new Random().nextFloat(); + } + + public static Color getRandomColor() { + Random random = new Random(); + int r = random.nextInt(256); + int g = random.nextInt(256); + int b = random.nextInt(256); + return Color.fromRGB(r, g, b); + } + + public static Location getRandomLocation(World world, double minX, double maxX, double minZ, double maxZ) { + double rx = getRandomDouble(minX, maxX, 3); + double rz = getRandomDouble(minZ, maxZ, 3); + int y = world.getHighestBlockAt((int)rx, (int)rz).getY()+1; + Location location = new Location(world, rx, y, rz); + return location; + } + + public static E getRandomElement(List list) { + if (list.size() == 0) { + return null; + } + return list.get(getRandomInt(0, list.size()-1)); + } + + public static String getRandomString(String[] array) { + Random r = new Random(); + return array[getRandomInt(0, array.length)]; + } + + public static int getRandomInt(int min, int max) { + if (min == max) { + return max; + } + Random r = new Random(); + int i = min < max ? min : max; + int a = min < max ? max : min; + return r.nextInt(a - i + 1) + i; + } + + public static double getRandomDouble(double min, double max, int scl) { + int pow = (int) Math.pow(10, scl); + return Math.floor((Math.random() * (max - min) + min) * pow) / pow; + } + +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/util/ReflectionUtil.java b/src/main/java/com/io/yutian/elementoriginlib/util/ReflectionUtil.java new file mode 100644 index 0000000..4dca45a --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/util/ReflectionUtil.java @@ -0,0 +1,181 @@ +package com.io.yutian.elementoriginlib.util; + +import org.bukkit.Bukkit; +import sun.misc.Unsafe; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.security.AccessController; +import java.security.PrivilegedAction; + +public class ReflectionUtil { + + private static String nmsVersion; + + public static String getNMSVersion() { + if (nmsVersion == null) { + String name = Bukkit.getServer().getClass().getPackage().getName(); + nmsVersion = name.substring(name.lastIndexOf('.') + 1); + } + return nmsVersion; + } + + public static Field getDeclaredField(Class clazz, String fieldName) { + try { + return clazz.getDeclaredField(fieldName); + } catch (Exception e) { + return null; + } + } + + public static Field getDeclaredField(Object object, String fieldName) { + Field field = null; + Class clazz = object.getClass(); + for (;clazz != Object.class; clazz = clazz.getSuperclass()) { + try { + field = clazz.getDeclaredField(fieldName); + return field; + } catch (NoSuchFieldException e) { + } + } + return null; + } + + public static Object getFieldValue(Class clazz, Object object, String fieldName) { + try { + Field field = clazz.getDeclaredField(fieldName); + field.setAccessible(true); + return field.get(object); + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } + + public static Object getFieldValue(Object object, String fieldName) { + try { + Class clazz = object.getClass(); + Field field = clazz.getDeclaredField(fieldName); + field.setAccessible(true); + return field.get(object); + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } + + public static void setField(Class clazz, Object object, String fieldName, Object value) { + try { + Field field = clazz.getDeclaredField(fieldName); + setField(object, value, field); + } catch (Exception e) { + e.printStackTrace(); + } + } + + public static void setField(Object object, String fieldName, Object value) { + try { + Class clazz = object.getClass(); + Field field = clazz.getDeclaredField(fieldName); + setField(object, value, field); + } catch (Exception e) { + e.printStackTrace(); + } + } + + public static void setField(Object object, Object value, Field foundField) { + boolean isStatic = (foundField.getModifiers() & Modifier.STATIC) == Modifier.STATIC; + if (isStatic) { + setStaticFieldUsingUnsafe(foundField, value); + } else { + setFieldUsingUnsafe(foundField, object, value); + } + } + + public static void setStaticFieldUsingUnsafe(final Field field, final Object newValue) { + try { + field.setAccessible(true); + int fieldModifiersMask = field.getModifiers(); + boolean isFinalModifierPresent = (fieldModifiersMask & Modifier.FINAL) == Modifier.FINAL; + if (isFinalModifierPresent) { + AccessController.doPrivileged((PrivilegedAction) () -> { + try { + Unsafe unsafe = getUnsafe(); + long offset = unsafe.staticFieldOffset(field); + Object base = unsafe.staticFieldBase(field); + setFieldUsingUnsafe(base, field.getType(), offset, newValue, unsafe); + return null; + } catch (Throwable t) { + throw new RuntimeException(t); + } + }); + } else { + field.set(null, newValue); + } + } catch (SecurityException | IllegalAccessException | IllegalArgumentException ex) { + throw new RuntimeException(ex); + } + } + + public static void setFieldUsingUnsafe(final Field field, final Object object, final Object newValue) { + try { + field.setAccessible(true); + int fieldModifiersMask = field.getModifiers(); + boolean isFinalModifierPresent = (fieldModifiersMask & Modifier.FINAL) == Modifier.FINAL; + if (isFinalModifierPresent) { + AccessController.doPrivileged((PrivilegedAction) () -> { + try { + Unsafe unsafe = getUnsafe(); + long offset = unsafe.objectFieldOffset(field); + setFieldUsingUnsafe(object, field.getType(), offset, newValue, unsafe); + return null; + } catch (Throwable t) { + throw new RuntimeException(t); + } + }); + } else { + try { + field.set(object, newValue); + } catch (IllegalAccessException ex) { + throw new RuntimeException(ex); + } + } + } catch (SecurityException ex) { + throw new RuntimeException(ex); + } + } + + public static Unsafe getUnsafe() { + try { + Field field = Unsafe.class.getDeclaredField("theUnsafe"); + field.setAccessible(true); + return (Unsafe) field.get(null); + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } + + private static void setFieldUsingUnsafe(Object base, Class type, long offset, Object newValue, Unsafe unsafe) { + if (type == Integer.TYPE) { + unsafe.putInt(base, offset, ((Integer) newValue)); + } else if (type == Short.TYPE) { + unsafe.putShort(base, offset, ((Short) newValue)); + } else if (type == Long.TYPE) { + unsafe.putLong(base, offset, ((Long) newValue)); + } else if (type == Byte.TYPE) { + unsafe.putByte(base, offset, ((Byte) newValue)); + } else if (type == Boolean.TYPE) { + unsafe.putBoolean(base, offset, ((Boolean) newValue)); + } else if (type == Float.TYPE) { + unsafe.putFloat(base, offset, ((Float) newValue)); + } else if (type == Double.TYPE) { + unsafe.putDouble(base, offset, ((Double) newValue)); + } else if (type == Character.TYPE) { + unsafe.putChar(base, offset, ((Character) newValue)); + } else { + unsafe.putObject(base, offset, newValue); + } + } + +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/util/StringUtil.java b/src/main/java/com/io/yutian/elementoriginlib/util/StringUtil.java new file mode 100644 index 0000000..0dd1a51 --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/util/StringUtil.java @@ -0,0 +1,267 @@ +package com.io.yutian.elementoriginlib.util; + +import java.math.BigInteger; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class StringUtil { + + public static byte[] parseByteArray(String string) { + String[] array = string.split(";"); + byte[] bytes = new byte[array.length]; + for (int i = 0; i < array.length; i++) { + String s = array[i]; + if (isByte(s)) { + bytes[i] = Byte.parseByte(s); + } else { + return null; + } + } + return bytes; + } + + public static int[] parseIntArray(String string) { + String[] array = string.split(";"); + int[] ints = new int[array.length]; + for (int i = 0; i < array.length; i++) { + String s = array[i]; + if (isInt(s)) { + ints[i] = Integer.parseInt(s); + } else { + return null; + } + } + return ints; + } + + public static List getStringContainData(String str, String start, String end) { + List result = new ArrayList<>(); + String regex = start + "(.*?)" + end; + Pattern pattern = Pattern.compile(regex); + Matcher matcher = pattern.matcher(str); + while(matcher.find()){ + String key = matcher.group(matcher.groupCount()); + if(!key.contains(start) && !key.contains(end)){ + result.add(key); + } + } + return result; + } + + public static boolean isByte(String arg0) { + try { + Byte.valueOf(arg0); + return true; + } catch (Exception e) { + return false; + } + } + + public static boolean isBoolean(String arg0) { + try { + if (arg0.equalsIgnoreCase("true") || arg0.equalsIgnoreCase("false")) { + return true; + } else { + return false; + } + } catch (Exception e) { + return false; + } + } + + public static boolean isDouble(String arg0) { + try { + Double.valueOf(arg0); + return true; + } catch (Exception e) { + return false; + } + } + + public static boolean isFloat(String arg0) { + try { + Float.valueOf(arg0); + return true; + } catch (Exception e) { + return false; + } + } + + public static boolean isInt(String arg0) { + try { + Integer.valueOf(arg0); + return true; + } catch (Exception e) { + return false; + } + } + + public static boolean isLong(String arg0) { + try { + Long.valueOf(arg0); + return true; + } catch (Exception e) { + return false; + } + } + + public static boolean isEnum(String arg0, Class enumType) { + try { + Enum.valueOf(enumType, arg0); + return true; + } catch (Exception e) { + return false; + } + } + + public static boolean isUUID(String arg0) { + try { + UUID.fromString(arg0); + return true; + } catch (Exception e) { + return false; + } + } + + public static String[] splitSpace(String arg0) { + return arg0.split(" "); + } + + public static Object getRegexValue(String key, String lore) { + if (key == null || lore == null) { + return null; + } + String s = lore.contains(":") ? ":" : ":"; + if (lore.contains(s) && lore.contains(key)) { + Pattern p = Pattern.compile("[^"+key+s+"]+"); + Matcher m = p.matcher(lore); + try { + m.find(); + if (m.group() != null) { + return (Object) m.group(); + } + } catch (Exception e) { + return null; + } + } + return null; + } + + public static UUID getUUIDFromString(String s) { + String md5 = getMD5(s); + String uuid = md5.substring(0, 8) + "-" + md5.substring(8, 12) + "-" + md5.substring(12, 16) + "-" + md5.substring(16, 20) + "-" + md5.substring(20); + + return UUID.fromString(uuid); + } + + public static String getMD5(String input) { + try { + MessageDigest md = MessageDigest.getInstance("MD5"); + byte[] messageDigest = md.digest(input.getBytes()); + BigInteger number = new BigInteger(1, messageDigest); + String hashtext; + for (hashtext = number.toString(16); hashtext.length() < 32; hashtext = "0" + hashtext) {} + return hashtext; + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + } + + public static String convertRGBToHex(int r, int g, int b) { + String rFString, rSString, gFString, gSString, + bFString, bSString, result; + int red, green, blue; + int rred, rgreen, rblue; + red = r / 16; + rred = r % 16; + if (red == 10) rFString = "A"; + else if (red == 11) rFString = "B"; + else if (red == 12) rFString = "C"; + else if (red == 13) rFString = "D"; + else if (red == 14) rFString = "E"; + else if (red == 15) rFString = "F"; + else rFString = String.valueOf(red); + + if (rred == 10) rSString = "A"; + else if (rred == 11) rSString = "B"; + else if (rred == 12) rSString = "C"; + else if (rred == 13) rSString = "D"; + else if (rred == 14) rSString = "E"; + else if (rred == 15) rSString = "F"; + else rSString = String.valueOf(rred); + + rFString = rFString + rSString; + + green = g / 16; + rgreen = g % 16; + + if (green == 10) gFString = "A"; + else if (green == 11) gFString = "B"; + else if (green == 12) gFString = "C"; + else if (green == 13) gFString = "D"; + else if (green == 14) gFString = "E"; + else if (green == 15) gFString = "F"; + else gFString = String.valueOf(green); + + if (rgreen == 10) gSString = "A"; + else if (rgreen == 11) gSString = "B"; + else if (rgreen == 12) gSString = "C"; + else if (rgreen == 13) gSString = "D"; + else if (rgreen == 14) gSString = "E"; + else if (rgreen == 15) gSString = "F"; + else gSString = String.valueOf(rgreen); + + gFString = gFString + gSString; + + blue = b / 16; + rblue = b % 16; + + if (blue == 10) bFString = "A"; + else if (blue == 11) bFString = "B"; + else if (blue == 12) bFString = "C"; + else if (blue == 13) bFString = "D"; + else if (blue == 14) bFString = "E"; + else if (blue == 15) bFString = "F"; + else bFString = String.valueOf(blue); + + if (rblue == 10) bSString = "A"; + else if (rblue == 11) bSString = "B"; + else if (rblue == 12) bSString = "C"; + else if (rblue == 13) bSString = "D"; + else if (rblue == 14) bSString = "E"; + else if (rblue == 15) bSString = "F"; + else bSString = String.valueOf(rblue); + bFString = bFString + bSString; + result = "#" + rFString + gFString + bFString; + return result; + + } + + public static int[] getRGB(String rgb) { + int[] ret = new int[3]; + for (int i = 0; i < 3; i++) { + ret[i] = Integer.parseInt(rgb.substring(i * 2, i * 2 + 2), 16); + } + return ret; + } + + private static String[] roman_thousands = {"", "M", "MM", "MMM"}; + private static String[] roman_hundreds = {"", "C", "CC", "CCC", "CD", "D", "DC", "DCC", "DCCC", "CM"}; + private static String[] roman_tens = {"", "X", "XX", "XXX", "XL", "L", "LX", "LXX", "LXXX", "XC"}; + private static String[] roman_ones = {"", "I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX"}; + + public static String intToRoman(int num) { + StringBuffer roman = new StringBuffer(); + roman.append(roman_thousands[num / 1000]); + roman.append(roman_hundreds[num % 1000 / 100]); + roman.append(roman_tens[num % 100 / 10]); + roman.append(roman_ones[num % 10]); + return roman.toString(); + } + +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/util/TextComponentBuilder.java b/src/main/java/com/io/yutian/elementoriginlib/util/TextComponentBuilder.java new file mode 100644 index 0000000..09e12f6 --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/util/TextComponentBuilder.java @@ -0,0 +1,45 @@ +package com.io.yutian.elementoriginlib.util; + +import net.md_5.bungee.api.chat.ClickEvent; +import net.md_5.bungee.api.chat.HoverEvent; +import net.md_5.bungee.api.chat.TextComponent; + +public class TextComponentBuilder { + + private TextComponent textComponent; + + public TextComponentBuilder() { + this.textComponent = new TextComponent(); + } + + public TextComponentBuilder addText(String text) { + textComponent.addExtra(text); + return this; + } + + public TextComponentBuilder addText(String text, ClickEvent clickEvent) { + TextComponent component = new TextComponent(text); + component.setClickEvent(clickEvent); + this.textComponent.addExtra(component); + return this; + } + + public TextComponentBuilder addText(String text, ClickEvent clickEvent, HoverEvent hoverEvent) { + TextComponent component = new TextComponent(text); + component.setClickEvent(clickEvent); + component.setHoverEvent(hoverEvent); + this.textComponent.addExtra(component); + return this; + } + + public TextComponentBuilder addText(String text, HoverEvent hoverEvent) { + TextComponent component = new TextComponent(text); + component.setHoverEvent(hoverEvent); + this.textComponent.addExtra(component); + return this; + } + + public TextComponent build() { + return textComponent; + } +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/util/TimeUtil.java b/src/main/java/com/io/yutian/elementoriginlib/util/TimeUtil.java new file mode 100644 index 0000000..0e7e86a --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/util/TimeUtil.java @@ -0,0 +1,97 @@ +package com.io.yutian.elementoriginlib.util; + +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; + +public class TimeUtil { + + private static final int HOUR_SECOND = 60 * 60; + private static final int MINUTE_SECOND = 60; + + public static boolean inOreDay(long time1, long time2) { + Date date = new Date(time1); + Date date2 = new Date(time2); + Calendar calendar = Calendar.getInstance(); + Calendar calendar2 = Calendar.getInstance(); + calendar.setTime(date); + int y1 = calendar.get(Calendar.YEAR); + int d1 = calendar.get(Calendar.DAY_OF_YEAR); + calendar2.setTime(date2); + + int y2 = calendar2.get(Calendar.YEAR); + int d2 = calendar2.get(Calendar.DAY_OF_YEAR); + return y1 == y2 && d1 == d2; + } + + public static boolean inOreWeek(long time1, long time2) { + Date date = new Date(time1); + Date date2 = new Date(time2); + Calendar calendar = Calendar.getInstance(); + Calendar calendar2 = Calendar.getInstance(); + calendar.setTime(date); + int y1 = calendar.get(Calendar.YEAR); + int d1 = calendar.get(Calendar.WEEK_OF_YEAR); + calendar2.setTime(date2); + int y2 = calendar2.get(Calendar.YEAR); + int d2 = calendar2.get(Calendar.WEEK_OF_YEAR); + return y1 == y2 && d1 == d2; + } + + public static boolean inOreMonth(long time1, long time2) { + Date date = new Date(time1); + Date date2 = new Date(time2); + Calendar calendar = Calendar.getInstance(); + Calendar calendar2 = Calendar.getInstance(); + calendar.setTime(date); + int y1 = calendar.get(Calendar.YEAR); + int d1 = calendar.get(Calendar.MONTH); + calendar2.setTime(date2); + int y2 = calendar2.get(Calendar.YEAR); + int d2 = calendar2.get(Calendar.MONTH); + return y1 == y2 && d1 == d2; + } + + public static String timeToString(long time) { + SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy/MM/dd"); + return simpleDateFormat.format(new Date(time)); + } + + public static String getTimeStringBySecondFormat(long second) { + if (second <= 0) { + return "0小时0分钟0秒"; + } + StringBuilder sb = new StringBuilder(); + long hours = second / HOUR_SECOND; + if (hours > 0) { + second -= hours * HOUR_SECOND; + } + long minutes = second / MINUTE_SECOND; + if (minutes > 0) { + second -= minutes * MINUTE_SECOND; + } + String hoursString = String.valueOf(hours); + String minutesString = (minutes >= 10 ? (minutes + "") : ("0" + minutes)); + String secondString = (second >= 10 ? (second + "") : ("0" + second)); + return hoursString+"小时"+minutesString+"分钟"+secondString+"秒"; + } + + public static String getTimeStringBySecond(long second) { + if (second <= 0) { + return "00:00:00"; + } + StringBuilder sb = new StringBuilder(); + long hours = second / HOUR_SECOND; + if (hours > 0) { + second -= hours * HOUR_SECOND; + } + long minutes = second / MINUTE_SECOND; + if (minutes > 0) { + second -= minutes * MINUTE_SECOND; + } + return (hours >= 10 ? (hours + "") + : ("0" + hours) + ":" + (minutes >= 10 ? (minutes + "") : ("0" + minutes)) + ":" + + (second >= 10 ? (second + "") : ("0" + second))); + } + +} \ No newline at end of file diff --git a/src/main/java/org/json/JSONArray.java b/src/main/java/org/json/JSONArray.java new file mode 100644 index 0000000..1ae70b1 --- /dev/null +++ b/src/main/java/org/json/JSONArray.java @@ -0,0 +1,1517 @@ +package org.json; + +/* + Copyright (c) 2002 JSON.org + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + The Software shall be used for Good, not Evil. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + */ + +import java.io.IOException; +import java.io.StringWriter; +import java.io.Writer; +import java.lang.reflect.Array; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.*; + + +/** + * A JSONArray is an ordered sequence of values. Its external text form is a + * string wrapped in square brackets with commas separating the values. The + * internal form is an object having get and opt + * methods for accessing the values by index, and put methods for + * adding or replacing values. The values can be any of these types: + * Boolean, JSONArray, JSONObject, + * Number, String, or the + * JSONObject.NULL object. + *

+ * The constructor can convert a JSON text into a Java object. The + * toString method converts to JSON text. + *

+ * A get method returns a value if one can be found, and throws an + * exception if one cannot be found. An opt method returns a + * default value instead of throwing an exception, and so is useful for + * obtaining optional values. + *

+ * The generic get() and opt() methods return an + * object which you can cast or query for type. There are also typed + * get and opt methods that do type checking and type + * coercion for you. + *

+ * The texts produced by the toString methods strictly conform to + * JSON syntax rules. The constructors are more forgiving in the texts they will + * accept: + *

    + *
  • An extra , (comma) may appear just + * before the closing bracket.
  • + *
  • The null value will be inserted when there is , + *  (comma) elision.
  • + *
  • Strings may be quoted with ' (single + * quote).
  • + *
  • Strings do not need to be quoted at all if they do not begin with a quote + * or single quote, and if they do not contain leading or trailing spaces, and + * if they do not contain any of these characters: + * { } [ ] / \ : , # and if they do not look like numbers and + * if they are not the reserved words true, false, or + * null.
  • + *
+ * + * @author JSON.org + * @version 2016-08/15 + */ +public class JSONArray implements Iterable { + + /** + * The arrayList where the JSONArray's properties are kept. + */ + private final ArrayList myArrayList; + + /** + * Construct an empty JSONArray. + */ + public JSONArray() { + this.myArrayList = new ArrayList(); + } + + /** + * Construct a JSONArray from a JSONTokener. + * + * @param x + * A JSONTokener + * @throws JSONException + * If there is a syntax error. + */ + public JSONArray(JSONTokener x) throws JSONException { + this(); + if (x.nextClean() != '[') { + throw x.syntaxError("A JSONArray text must start with '['"); + } + + char nextChar = x.nextClean(); + if (nextChar == 0) { + // array is unclosed. No ']' found, instead EOF + throw x.syntaxError("Expected a ',' or ']'"); + } + if (nextChar != ']') { + x.back(); + for (;;) { + if (x.nextClean() == ',') { + x.back(); + this.myArrayList.add(JSONObject.NULL); + } else { + x.back(); + this.myArrayList.add(x.nextValue()); + } + switch (x.nextClean()) { + case 0: + // array is unclosed. No ']' found, instead EOF + throw x.syntaxError("Expected a ',' or ']'"); + case ',': + nextChar = x.nextClean(); + if (nextChar == 0) { + // array is unclosed. No ']' found, instead EOF + throw x.syntaxError("Expected a ',' or ']'"); + } + if (nextChar == ']') { + return; + } + x.back(); + break; + case ']': + return; + default: + throw x.syntaxError("Expected a ',' or ']'"); + } + } + } + } + + /** + * Construct a JSONArray from a source JSON text. + * + * @param source + * A string that begins with [ (left + * bracket) and ends with ] + *  (right bracket). + * @throws JSONException + * If there is a syntax error. + */ + public JSONArray(String source) throws JSONException { + this(new JSONTokener(source)); + } + + /** + * Construct a JSONArray from a Collection. + * + * @param collection + * A Collection. + */ + public JSONArray(Collection collection) { + if (collection == null) { + this.myArrayList = new ArrayList(); + } else { + this.myArrayList = new ArrayList(collection.size()); + for (Object o: collection){ + this.myArrayList.add(JSONObject.wrap(o)); + } + } + } + + /** + * Construct a JSONArray from an array. + * + * @param array + * Array. If the parameter passed is null, or not an array, an + * exception will be thrown. + * + * @throws JSONException + * If not an array or if an array value is non-finite number. + * @throws NullPointerException + * Thrown if the array parameter is null. + */ + public JSONArray(Object array) throws JSONException { + this(); + if (array.getClass().isArray()) { + int length = Array.getLength(array); + this.myArrayList.ensureCapacity(length); + for (int i = 0; i < length; i += 1) { + this.put(JSONObject.wrap(Array.get(array, i))); + } + } else { + throw new JSONException( + "JSONArray initial value should be a string or collection or array."); + } + } + + @Override + public Iterator iterator() { + return this.myArrayList.iterator(); + } + + /** + * Get the object value associated with an index. + * + * @param index + * The index must be between 0 and length() - 1. + * @return An object value. + * @throws JSONException + * If there is no value for the index. + */ + public Object get(int index) throws JSONException { + Object object = this.opt(index); + if (object == null) { + throw new JSONException("JSONArray[" + index + "] not found."); + } + return object; + } + + /** + * Get the boolean value associated with an index. The string values "true" + * and "false" are converted to boolean. + * + * @param index + * The index must be between 0 and length() - 1. + * @return The truth. + * @throws JSONException + * If there is no value for the index or if the value is not + * convertible to boolean. + */ + public boolean getBoolean(int index) throws JSONException { + Object object = this.get(index); + if (object.equals(Boolean.FALSE) + || (object instanceof String && ((String) object) + .equalsIgnoreCase("false"))) { + return false; + } else if (object.equals(Boolean.TRUE) + || (object instanceof String && ((String) object) + .equalsIgnoreCase("true"))) { + return true; + } + throw wrongValueFormatException(index, "boolean", null); + } + + /** + * Get the double value associated with an index. + * + * @param index + * The index must be between 0 and length() - 1. + * @return The value. + * @throws JSONException + * If the key is not found or if the value cannot be converted + * to a number. + */ + public double getDouble(int index) throws JSONException { + final Object object = this.get(index); + if(object instanceof Number) { + return ((Number)object).doubleValue(); + } + try { + return Double.parseDouble(object.toString()); + } catch (Exception e) { + throw wrongValueFormatException(index, "double", e); + } + } + + /** + * Get the float value associated with a key. + * + * @param index + * The index must be between 0 and length() - 1. + * @return The numeric value. + * @throws JSONException + * if the key is not found or if the value is not a Number + * object and cannot be converted to a number. + */ + public float getFloat(int index) throws JSONException { + final Object object = this.get(index); + if(object instanceof Number) { + return ((Float)object).floatValue(); + } + try { + return Float.parseFloat(object.toString()); + } catch (Exception e) { + throw wrongValueFormatException(index, "float", e); + } + } + + /** + * Get the Number value associated with a key. + * + * @param index + * The index must be between 0 and length() - 1. + * @return The numeric value. + * @throws JSONException + * if the key is not found or if the value is not a Number + * object and cannot be converted to a number. + */ + public Number getNumber(int index) throws JSONException { + Object object = this.get(index); + try { + if (object instanceof Number) { + return (Number)object; + } + return JSONObject.stringToNumber(object.toString()); + } catch (Exception e) { + throw wrongValueFormatException(index, "number", e); + } + } + + /** + * Get the enum value associated with an index. + * + * @param + * Enum Type + * @param clazz + * The type of enum to retrieve. + * @param index + * The index must be between 0 and length() - 1. + * @return The enum value at the index location + * @throws JSONException + * if the key is not found or if the value cannot be converted + * to an enum. + */ + public > E getEnum(Class clazz, int index) throws JSONException { + E val = optEnum(clazz, index); + if(val==null) { + // JSONException should really take a throwable argument. + // If it did, I would re-implement this with the Enum.valueOf + // method and place any thrown exception in the JSONException + throw wrongValueFormatException(index, "enum of type " + + JSONObject.quote(clazz.getSimpleName()), null); + } + return val; + } + + /** + * Get the BigDecimal value associated with an index. If the value is float + * or double, the the {@link BigDecimal#BigDecimal(double)} constructor + * will be used. See notes on the constructor for conversion issues that + * may arise. + * + * @param index + * The index must be between 0 and length() - 1. + * @return The value. + * @throws JSONException + * If the key is not found or if the value cannot be converted + * to a BigDecimal. + */ + public BigDecimal getBigDecimal (int index) throws JSONException { + Object object = this.get(index); + BigDecimal val = JSONObject.objectToBigDecimal(object, null); + if(val == null) { + throw wrongValueFormatException(index, "BigDecimal", object, null); + } + return val; + } + + /** + * Get the BigInteger value associated with an index. + * + * @param index + * The index must be between 0 and length() - 1. + * @return The value. + * @throws JSONException + * If the key is not found or if the value cannot be converted + * to a BigInteger. + */ + public BigInteger getBigInteger (int index) throws JSONException { + Object object = this.get(index); + BigInteger val = JSONObject.objectToBigInteger(object, null); + if(val == null) { + throw wrongValueFormatException(index, "BigInteger", object, null); + } + return val; + } + + /** + * Get the int value associated with an index. + * + * @param index + * The index must be between 0 and length() - 1. + * @return The value. + * @throws JSONException + * If the key is not found or if the value is not a number. + */ + public int getInt(int index) throws JSONException { + final Object object = this.get(index); + if(object instanceof Number) { + return ((Number)object).intValue(); + } + try { + return Integer.parseInt(object.toString()); + } catch (Exception e) { + throw wrongValueFormatException(index, "int", e); + } + } + + /** + * Get the JSONArray associated with an index. + * + * @param index + * The index must be between 0 and length() - 1. + * @return A JSONArray value. + * @throws JSONException + * If there is no value for the index. or if the value is not a + * JSONArray + */ + public JSONArray getJSONArray(int index) throws JSONException { + Object object = this.get(index); + if (object instanceof JSONArray) { + return (JSONArray) object; + } + throw wrongValueFormatException(index, "JSONArray", null); + } + + /** + * Get the JSONObject associated with an index. + * + * @param index + * subscript + * @return A JSONObject value. + * @throws JSONException + * If there is no value for the index or if the value is not a + * JSONObject + */ + public JSONObject getJSONObject(int index) throws JSONException { + Object object = this.get(index); + if (object instanceof JSONObject) { + return (JSONObject) object; + } + throw wrongValueFormatException(index, "JSONObject", null); + } + + /** + * Get the long value associated with an index. + * + * @param index + * The index must be between 0 and length() - 1. + * @return The value. + * @throws JSONException + * If the key is not found or if the value cannot be converted + * to a number. + */ + public long getLong(int index) throws JSONException { + final Object object = this.get(index); + if(object instanceof Number) { + return ((Number)object).longValue(); + } + try { + return Long.parseLong(object.toString()); + } catch (Exception e) { + throw wrongValueFormatException(index, "long", e); + } + } + + /** + * Get the string associated with an index. + * + * @param index + * The index must be between 0 and length() - 1. + * @return A string value. + * @throws JSONException + * If there is no string value for the index. + */ + public String getString(int index) throws JSONException { + Object object = this.get(index); + if (object instanceof String) { + return (String) object; + } + throw wrongValueFormatException(index, "String", null); + } + + /** + * Determine if the value is null. + * + * @param index + * The index must be between 0 and length() - 1. + * @return true if the value at the index is null, or if there is no value. + */ + public boolean isNull(int index) { + return JSONObject.NULL.equals(this.opt(index)); + } + + /** + * Make a string from the contents of this JSONArray. The + * separator string is inserted between each element. Warning: + * This method assumes that the data structure is acyclical. + * + * @param separator + * A string that will be inserted between the elements. + * @return a string. + * @throws JSONException + * If the array contains an invalid number. + */ + public String join(String separator) throws JSONException { + int len = this.length(); + if (len == 0) { + return ""; + } + + StringBuilder sb = new StringBuilder( + JSONObject.valueToString(this.myArrayList.get(0))); + + for (int i = 1; i < len; i++) { + sb.append(separator) + .append(JSONObject.valueToString(this.myArrayList.get(i))); + } + return sb.toString(); + } + + /** + * Get the number of elements in the JSONArray, included nulls. + * + * @return The length (or size). + */ + public int length() { + return this.myArrayList.size(); + } + + /** + * Get the optional object value associated with an index. + * + * @param index + * The index must be between 0 and length() - 1. If not, null is returned. + * @return An object value, or null if there is no object at that index. + */ + public Object opt(int index) { + return (index < 0 || index >= this.length()) ? null : this.myArrayList + .get(index); + } + + /** + * Get the optional boolean value associated with an index. It returns false + * if there is no value at that index, or if the value is not Boolean.TRUE + * or the String "true". + * + * @param index + * The index must be between 0 and length() - 1. + * @return The truth. + */ + public boolean optBoolean(int index) { + return this.optBoolean(index, false); + } + + /** + * Get the optional boolean value associated with an index. It returns the + * defaultValue if there is no value at that index or if it is not a Boolean + * or the String "true" or "false" (case insensitive). + * + * @param index + * The index must be between 0 and length() - 1. + * @param defaultValue + * A boolean default. + * @return The truth. + */ + public boolean optBoolean(int index, boolean defaultValue) { + try { + return this.getBoolean(index); + } catch (Exception e) { + return defaultValue; + } + } + + /** + * Get the optional double value associated with an index. NaN is returned + * if there is no value for the index, or if the value is not a number and + * cannot be converted to a number. + * + * @param index + * The index must be between 0 and length() - 1. + * @return The value. + */ + public double optDouble(int index) { + return this.optDouble(index, Double.NaN); + } + + /** + * Get the optional double value associated with an index. The defaultValue + * is returned if there is no value for the index, or if the value is not a + * number and cannot be converted to a number. + * + * @param index + * subscript + * @param defaultValue + * The default value. + * @return The value. + */ + public double optDouble(int index, double defaultValue) { + final Number val = this.optNumber(index, null); + if (val == null) { + return defaultValue; + } + final double doubleValue = val.doubleValue(); + // if (Double.isNaN(doubleValue) || Double.isInfinite(doubleValue)) { + // return defaultValue; + // } + return doubleValue; + } + + /** + * Get the optional float value associated with an index. NaN is returned + * if there is no value for the index, or if the value is not a number and + * cannot be converted to a number. + * + * @param index + * The index must be between 0 and length() - 1. + * @return The value. + */ + public float optFloat(int index) { + return this.optFloat(index, Float.NaN); + } + + /** + * Get the optional float value associated with an index. The defaultValue + * is returned if there is no value for the index, or if the value is not a + * number and cannot be converted to a number. + * + * @param index + * subscript + * @param defaultValue + * The default value. + * @return The value. + */ + public float optFloat(int index, float defaultValue) { + final Number val = this.optNumber(index, null); + if (val == null) { + return defaultValue; + } + final float floatValue = val.floatValue(); + // if (Float.isNaN(floatValue) || Float.isInfinite(floatValue)) { + // return floatValue; + // } + return floatValue; + } + + /** + * Get the optional int value associated with an index. Zero is returned if + * there is no value for the index, or if the value is not a number and + * cannot be converted to a number. + * + * @param index + * The index must be between 0 and length() - 1. + * @return The value. + */ + public int optInt(int index) { + return this.optInt(index, 0); + } + + /** + * Get the optional int value associated with an index. The defaultValue is + * returned if there is no value for the index, or if the value is not a + * number and cannot be converted to a number. + * + * @param index + * The index must be between 0 and length() - 1. + * @param defaultValue + * The default value. + * @return The value. + */ + public int optInt(int index, int defaultValue) { + final Number val = this.optNumber(index, null); + if (val == null) { + return defaultValue; + } + return val.intValue(); + } + + /** + * Get the enum value associated with a key. + * + * @param + * Enum Type + * @param clazz + * The type of enum to retrieve. + * @param index + * The index must be between 0 and length() - 1. + * @return The enum value at the index location or null if not found + */ + public > E optEnum(Class clazz, int index) { + return this.optEnum(clazz, index, null); + } + + /** + * Get the enum value associated with a key. + * + * @param + * Enum Type + * @param clazz + * The type of enum to retrieve. + * @param index + * The index must be between 0 and length() - 1. + * @param defaultValue + * The default in case the value is not found + * @return The enum value at the index location or defaultValue if + * the value is not found or cannot be assigned to clazz + */ + public > E optEnum(Class clazz, int index, E defaultValue) { + try { + Object val = this.opt(index); + if (JSONObject.NULL.equals(val)) { + return defaultValue; + } + if (clazz.isAssignableFrom(val.getClass())) { + // we just checked it! + @SuppressWarnings("unchecked") + E myE = (E) val; + return myE; + } + return Enum.valueOf(clazz, val.toString()); + } catch (IllegalArgumentException e) { + return defaultValue; + } catch (NullPointerException e) { + return defaultValue; + } + } + + /** + * Get the optional BigInteger value associated with an index. The + * defaultValue is returned if there is no value for the index, or if the + * value is not a number and cannot be converted to a number. + * + * @param index + * The index must be between 0 and length() - 1. + * @param defaultValue + * The default value. + * @return The value. + */ + public BigInteger optBigInteger(int index, BigInteger defaultValue) { + Object val = this.opt(index); + return JSONObject.objectToBigInteger(val, defaultValue); + } + + /** + * Get the optional BigDecimal value associated with an index. The + * defaultValue is returned if there is no value for the index, or if the + * value is not a number and cannot be converted to a number. If the value + * is float or double, the the {@link BigDecimal#BigDecimal(double)} + * constructor will be used. See notes on the constructor for conversion + * issues that may arise. + * + * @param index + * The index must be between 0 and length() - 1. + * @param defaultValue + * The default value. + * @return The value. + */ + public BigDecimal optBigDecimal(int index, BigDecimal defaultValue) { + Object val = this.opt(index); + return JSONObject.objectToBigDecimal(val, defaultValue); + } + + /** + * Get the optional JSONArray associated with an index. + * + * @param index + * subscript + * @return A JSONArray value, or null if the index has no value, or if the + * value is not a JSONArray. + */ + public JSONArray optJSONArray(int index) { + Object o = this.opt(index); + return o instanceof JSONArray ? (JSONArray) o : null; + } + + /** + * Get the optional JSONObject associated with an index. Null is returned if + * the key is not found, or null if the index has no value, or if the value + * is not a JSONObject. + * + * @param index + * The index must be between 0 and length() - 1. + * @return A JSONObject value. + */ + public JSONObject optJSONObject(int index) { + Object o = this.opt(index); + return o instanceof JSONObject ? (JSONObject) o : null; + } + + /** + * Get the optional long value associated with an index. Zero is returned if + * there is no value for the index, or if the value is not a number and + * cannot be converted to a number. + * + * @param index + * The index must be between 0 and length() - 1. + * @return The value. + */ + public long optLong(int index) { + return this.optLong(index, 0); + } + + /** + * Get the optional long value associated with an index. The defaultValue is + * returned if there is no value for the index, or if the value is not a + * number and cannot be converted to a number. + * + * @param index + * The index must be between 0 and length() - 1. + * @param defaultValue + * The default value. + * @return The value. + */ + public long optLong(int index, long defaultValue) { + final Number val = this.optNumber(index, null); + if (val == null) { + return defaultValue; + } + return val.longValue(); + } + + /** + * Get an optional {@link Number} value associated with a key, or null + * if there is no such key or if the value is not a number. If the value is a string, + * an attempt will be made to evaluate it as a number ({@link BigDecimal}). This method + * would be used in cases where type coercion of the number value is unwanted. + * + * @param index + * The index must be between 0 and length() - 1. + * @return An object which is the value. + */ + public Number optNumber(int index) { + return this.optNumber(index, null); + } + + /** + * Get an optional {@link Number} value associated with a key, or the default if there + * is no such key or if the value is not a number. If the value is a string, + * an attempt will be made to evaluate it as a number ({@link BigDecimal}). This method + * would be used in cases where type coercion of the number value is unwanted. + * + * @param index + * The index must be between 0 and length() - 1. + * @param defaultValue + * The default. + * @return An object which is the value. + */ + public Number optNumber(int index, Number defaultValue) { + Object val = this.opt(index); + if (JSONObject.NULL.equals(val)) { + return defaultValue; + } + if (val instanceof Number){ + return (Number) val; + } + + if (val instanceof String) { + try { + return JSONObject.stringToNumber((String) val); + } catch (Exception e) { + return defaultValue; + } + } + return defaultValue; + } + + /** + * Get the optional string value associated with an index. It returns an + * empty string if there is no value at that index. If the value is not a + * string and is not null, then it is converted to a string. + * + * @param index + * The index must be between 0 and length() - 1. + * @return A String value. + */ + public String optString(int index) { + return this.optString(index, ""); + } + + /** + * Get the optional string associated with an index. The defaultValue is + * returned if the key is not found. + * + * @param index + * The index must be between 0 and length() - 1. + * @param defaultValue + * The default value. + * @return A String value. + */ + public String optString(int index, String defaultValue) { + Object object = this.opt(index); + return JSONObject.NULL.equals(object) ? defaultValue : object + .toString(); + } + + /** + * Append a boolean value. This increases the array's length by one. + * + * @param value + * A boolean value. + * @return this. + */ + public JSONArray put(boolean value) { + return this.put(value ? Boolean.TRUE : Boolean.FALSE); + } + + /** + * Put a value in the JSONArray, where the value will be a JSONArray which + * is produced from a Collection. + * + * @param value + * A Collection value. + * @return this. + * @throws JSONException + * If the value is non-finite number. + */ + public JSONArray put(Collection value) { + return this.put(new JSONArray(value)); + } + + /** + * Append a double value. This increases the array's length by one. + * + * @param value + * A double value. + * @return this. + * @throws JSONException + * if the value is not finite. + */ + public JSONArray put(double value) throws JSONException { + return this.put(Double.valueOf(value)); + } + + /** + * Append a float value. This increases the array's length by one. + * + * @param value + * A float value. + * @return this. + * @throws JSONException + * if the value is not finite. + */ + public JSONArray put(float value) throws JSONException { + return this.put(Float.valueOf(value)); + } + + /** + * Append an int value. This increases the array's length by one. + * + * @param value + * An int value. + * @return this. + */ + public JSONArray put(int value) { + return this.put(Integer.valueOf(value)); + } + + /** + * Append an long value. This increases the array's length by one. + * + * @param value + * A long value. + * @return this. + */ + public JSONArray put(long value) { + return this.put(Long.valueOf(value)); + } + + /** + * Put a value in the JSONArray, where the value will be a JSONObject which + * is produced from a Map. + * + * @param value + * A Map value. + * @return this. + * @throws JSONException + * If a value in the map is non-finite number. + * @throws NullPointerException + * If a key in the map is null + */ + public JSONArray put(Map value) { + return this.put(new JSONObject(value)); + } + + /** + * Append an object value. This increases the array's length by one. + * + * @param value + * An object value. The value should be a Boolean, Double, + * Integer, JSONArray, JSONObject, Long, or String, or the + * JSONObject.NULL object. + * @return this. + * @throws JSONException + * If the value is non-finite number. + */ + public JSONArray put(Object value) { + JSONObject.testValidity(value); + this.myArrayList.add(value); + return this; + } + + /** + * Put or replace a boolean value in the JSONArray. If the index is greater + * than the length of the JSONArray, then null elements will be added as + * necessary to pad it out. + * + * @param index + * The subscript. + * @param value + * A boolean value. + * @return this. + * @throws JSONException + * If the index is negative. + */ + public JSONArray put(int index, boolean value) throws JSONException { + return this.put(index, value ? Boolean.TRUE : Boolean.FALSE); + } + + /** + * Put a value in the JSONArray, where the value will be a JSONArray which + * is produced from a Collection. + * + * @param index + * The subscript. + * @param value + * A Collection value. + * @return this. + * @throws JSONException + * If the index is negative or if the value is non-finite. + */ + public JSONArray put(int index, Collection value) throws JSONException { + return this.put(index, new JSONArray(value)); + } + + /** + * Put or replace a double value. If the index is greater than the length of + * the JSONArray, then null elements will be added as necessary to pad it + * out. + * + * @param index + * The subscript. + * @param value + * A double value. + * @return this. + * @throws JSONException + * If the index is negative or if the value is non-finite. + */ + public JSONArray put(int index, double value) throws JSONException { + return this.put(index, Double.valueOf(value)); + } + + /** + * Put or replace a float value. If the index is greater than the length of + * the JSONArray, then null elements will be added as necessary to pad it + * out. + * + * @param index + * The subscript. + * @param value + * A float value. + * @return this. + * @throws JSONException + * If the index is negative or if the value is non-finite. + */ + public JSONArray put(int index, float value) throws JSONException { + return this.put(index, Float.valueOf(value)); + } + + /** + * Put or replace an int value. If the index is greater than the length of + * the JSONArray, then null elements will be added as necessary to pad it + * out. + * + * @param index + * The subscript. + * @param value + * An int value. + * @return this. + * @throws JSONException + * If the index is negative. + */ + public JSONArray put(int index, int value) throws JSONException { + return this.put(index, Integer.valueOf(value)); + } + + /** + * Put or replace a long value. If the index is greater than the length of + * the JSONArray, then null elements will be added as necessary to pad it + * out. + * + * @param index + * The subscript. + * @param value + * A long value. + * @return this. + * @throws JSONException + * If the index is negative. + */ + public JSONArray put(int index, long value) throws JSONException { + return this.put(index, Long.valueOf(value)); + } + + /** + * Put a value in the JSONArray, where the value will be a JSONObject that + * is produced from a Map. + * + * @param index + * The subscript. + * @param value + * The Map value. + * @return this. + * @throws JSONException + * If the index is negative or if the the value is an invalid + * number. + * @throws NullPointerException + * If a key in the map is null + */ + public JSONArray put(int index, Map value) throws JSONException { + this.put(index, new JSONObject(value)); + return this; + } + + /** + * Put or replace an object value in the JSONArray. If the index is greater + * than the length of the JSONArray, then null elements will be added as + * necessary to pad it out. + * + * @param index + * The subscript. + * @param value + * The value to put into the array. The value should be a + * Boolean, Double, Integer, JSONArray, JSONObject, Long, or + * String, or the JSONObject.NULL object. + * @return this. + * @throws JSONException + * If the index is negative or if the the value is an invalid + * number. + */ + public JSONArray put(int index, Object value) throws JSONException { + if (index < 0) { + throw new JSONException("JSONArray[" + index + "] not found."); + } + if (index < this.length()) { + JSONObject.testValidity(value); + this.myArrayList.set(index, value); + return this; + } + if(index == this.length()){ + // simple append + return this.put(value); + } + // if we are inserting past the length, we want to grow the array all at once + // instead of incrementally. + this.myArrayList.ensureCapacity(index + 1); + while (index != this.length()) { + // we don't need to test validity of NULL objects + this.myArrayList.add(JSONObject.NULL); + } + return this.put(value); + } + + /** + * Creates a JSONPointer using an initialization string and tries to + * match it to an item within this JSONArray. For example, given a + * JSONArray initialized with this document: + *
+     * [
+     *     {"b":"c"}
+     * ]
+     * 
+ * and this JSONPointer string: + *
+     * "/0/b"
+     * 
+ * Then this method will return the String "c" + * A JSONPointerException may be thrown from code called by this method. + * + * @param jsonPointer string that can be used to create a JSONPointer + * @return the item matched by the JSONPointer, otherwise null + */ + public Object query(String jsonPointer) { + return query(new JSONPointer(jsonPointer)); + } + + /** + * Uses a user initialized JSONPointer and tries to + * match it to an item within this JSONArray. For example, given a + * JSONArray initialized with this document: + *
+     * [
+     *     {"b":"c"}
+     * ]
+     * 
+ * and this JSONPointer: + *
+     * "/0/b"
+     * 
+ * Then this method will return the String "c" + * A JSONPointerException may be thrown from code called by this method. + * + * @param jsonPointer string that can be used to create a JSONPointer + * @return the item matched by the JSONPointer, otherwise null + */ + public Object query(JSONPointer jsonPointer) { + return jsonPointer.queryFrom(this); + } + + /** + * Queries and returns a value from this object using {@code jsonPointer}, or + * returns null if the query fails due to a missing key. + * + * @param jsonPointer the string representation of the JSON pointer + * @return the queried value or {@code null} + * @throws IllegalArgumentException if {@code jsonPointer} has invalid syntax + */ + public Object optQuery(String jsonPointer) { + return optQuery(new JSONPointer(jsonPointer)); + } + + /** + * Queries and returns a value from this object using {@code jsonPointer}, or + * returns null if the query fails due to a missing key. + * + * @param jsonPointer The JSON pointer + * @return the queried value or {@code null} + * @throws IllegalArgumentException if {@code jsonPointer} has invalid syntax + */ + public Object optQuery(JSONPointer jsonPointer) { + try { + return jsonPointer.queryFrom(this); + } catch (JSONPointerException e) { + return null; + } + } + + /** + * Remove an index and close the hole. + * + * @param index + * The index of the element to be removed. + * @return The value that was associated with the index, or null if there + * was no value. + */ + public Object remove(int index) { + return index >= 0 && index < this.length() + ? this.myArrayList.remove(index) + : null; + } + + /** + * Determine if two JSONArrays are similar. + * They must contain similar sequences. + * + * @param other The other JSONArray + * @return true if they are equal + */ + public boolean similar(Object other) { + if (!(other instanceof JSONArray)) { + return false; + } + int len = this.length(); + if (len != ((JSONArray)other).length()) { + return false; + } + for (int i = 0; i < len; i += 1) { + Object valueThis = this.myArrayList.get(i); + Object valueOther = ((JSONArray)other).myArrayList.get(i); + if(valueThis == valueOther) { + continue; + } + if(valueThis == null) { + return false; + } + if (valueThis instanceof JSONObject) { + if (!((JSONObject)valueThis).similar(valueOther)) { + return false; + } + } else if (valueThis instanceof JSONArray) { + if (!((JSONArray)valueThis).similar(valueOther)) { + return false; + } + } else if (!valueThis.equals(valueOther)) { + return false; + } + } + return true; + } + + /** + * Produce a JSONObject by combining a JSONArray of names with the values of + * this JSONArray. + * + * @param names + * A JSONArray containing a list of key strings. These will be + * paired with the values. + * @return A JSONObject, or null if there are no names or if this JSONArray + * has no values. + * @throws JSONException + * If any of the names are null. + */ + public JSONObject toJSONObject(JSONArray names) throws JSONException { + if (names == null || names.isEmpty() || this.isEmpty()) { + return null; + } + JSONObject jo = new JSONObject(names.length()); + for (int i = 0; i < names.length(); i += 1) { + jo.put(names.getString(i), this.opt(i)); + } + return jo; + } + + /** + * Make a JSON text of this JSONArray. For compactness, no unnecessary + * whitespace is added. If it is not possible to produce a syntactically + * correct JSON text then null will be returned instead. This could occur if + * the array contains an invalid number. + *

+ * Warning: This method assumes that the data structure is acyclical. + * + * + * @return a printable, displayable, transmittable representation of the + * array. + */ + @Override + public String toString() { + try { + return this.toString(0); + } catch (Exception e) { + return null; + } + } + + /** + * Make a pretty-printed JSON text of this JSONArray. + * + *

If indentFactor > 0 and the {@link JSONArray} has only + * one element, then the array will be output on a single line: + *

{@code [1]}
+ * + *

If an array has 2 or more elements, then it will be output across + * multiple lines:

{@code
+     * [
+     * 1,
+     * "value 2",
+     * 3
+     * ]
+     * }
+ *

+ * Warning: This method assumes that the data structure is acyclical. + * + * + * @param indentFactor + * The number of spaces to add to each level of indentation. + * @return a printable, displayable, transmittable representation of the + * object, beginning with [ (left + * bracket) and ending with ] + *  (right bracket). + * @throws JSONException + */ + public String toString(int indentFactor) throws JSONException { + StringWriter sw = new StringWriter(); + synchronized (sw.getBuffer()) { + return this.write(sw, indentFactor, 0).toString(); + } + } + + /** + * Write the contents of the JSONArray as JSON text to a writer. For + * compactness, no whitespace is added. + *

+ * Warning: This method assumes that the data structure is acyclical. + * + * + * @return The writer. + * @throws JSONException + */ + public Writer write(Writer writer) throws JSONException { + return this.write(writer, 0, 0); + } + + /** + * Write the contents of the JSONArray as JSON text to a writer. + * + *

If indentFactor > 0 and the {@link JSONArray} has only + * one element, then the array will be output on a single line: + *

{@code [1]}
+ * + *

If an array has 2 or more elements, then it will be output across + * multiple lines:

{@code
+     * [
+     * 1,
+     * "value 2",
+     * 3
+     * ]
+     * }
+ *

+ * Warning: This method assumes that the data structure is acyclical. + * + * + * @param writer + * Writes the serialized JSON + * @param indentFactor + * The number of spaces to add to each level of indentation. + * @param indent + * The indentation of the top level. + * @return The writer. + * @throws JSONException + */ + public Writer write(Writer writer, int indentFactor, int indent) + throws JSONException { + try { + boolean needsComma = false; + int length = this.length(); + writer.write('['); + + if (length == 1) { + try { + JSONObject.writeValue(writer, this.myArrayList.get(0), + indentFactor, indent); + } catch (Exception e) { + throw new JSONException("Unable to write JSONArray value at index: 0", e); + } + } else if (length != 0) { + final int newIndent = indent + indentFactor; + + for (int i = 0; i < length; i += 1) { + if (needsComma) { + writer.write(','); + } + if (indentFactor > 0) { + writer.write('\n'); + } + JSONObject.indent(writer, newIndent); + try { + JSONObject.writeValue(writer, this.myArrayList.get(i), + indentFactor, newIndent); + } catch (Exception e) { + throw new JSONException("Unable to write JSONArray value at index: " + i, e); + } + needsComma = true; + } + if (indentFactor > 0) { + writer.write('\n'); + } + JSONObject.indent(writer, indent); + } + writer.write(']'); + return writer; + } catch (IOException e) { + throw new JSONException(e); + } + } + + /** + * Returns a java.util.List containing all of the elements in this array. + * If an element in the array is a JSONArray or JSONObject it will also + * be converted to a List and a Map respectively. + *

+ * Warning: This method assumes that the data structure is acyclical. + * + * @return a java.util.List containing the elements of this array + */ + public List toList() { + List results = new ArrayList(this.myArrayList.size()); + for (Object element : this.myArrayList) { + if (element == null || JSONObject.NULL.equals(element)) { + results.add(null); + } else if (element instanceof JSONArray) { + results.add(((JSONArray) element).toList()); + } else if (element instanceof JSONObject) { + results.add(((JSONObject) element).toMap()); + } else { + results.add(element); + } + } + return results; + } + + /** + * Check if JSONArray is empty. + * + * @return true if JSONArray is empty, otherwise false. + */ + public boolean isEmpty() { + return this.myArrayList.isEmpty(); + } + + /** + * Create a new JSONException in a common format for incorrect conversions. + * @param idx index of the item + * @param valueType the type of value being coerced to + * @param cause optional cause of the coercion failure + * @return JSONException that can be thrown. + */ + private static JSONException wrongValueFormatException( + int idx, + String valueType, + Throwable cause) { + return new JSONException( + "JSONArray[" + idx + "] is not a " + valueType + "." + , cause); + } + + /** + * Create a new JSONException in a common format for incorrect conversions. + * @param idx index of the item + * @param valueType the type of value being coerced to + * @param cause optional cause of the coercion failure + * @return JSONException that can be thrown. + */ + private static JSONException wrongValueFormatException( + int idx, + String valueType, + Object value, + Throwable cause) { + return new JSONException( + "JSONArray[" + idx + "] is not a " + valueType + " (" + value + ")." + , cause); + } + +} diff --git a/src/main/java/org/json/JSONException.java b/src/main/java/org/json/JSONException.java new file mode 100644 index 0000000..72542df --- /dev/null +++ b/src/main/java/org/json/JSONException.java @@ -0,0 +1,45 @@ +package org.json; + +/** + * The JSONException is thrown by the JSON.org classes when things are amiss. + * + * @author JSON.org + * @version 2015-12-09 + */ +public class JSONException extends RuntimeException { + /** Serialization ID */ + private static final long serialVersionUID = 0; + + /** + * Constructs a JSONException with an explanatory message. + * + * @param message + * Detail about the reason for the exception. + */ + public JSONException(final String message) { + super(message); + } + + /** + * Constructs a JSONException with an explanatory message and cause. + * + * @param message + * Detail about the reason for the exception. + * @param cause + * The cause. + */ + public JSONException(final String message, final Throwable cause) { + super(message, cause); + } + + /** + * Constructs a new JSONException with the specified cause. + * + * @param cause + * The cause. + */ + public JSONException(final Throwable cause) { + super(cause.getMessage(), cause); + } + +} diff --git a/src/main/java/org/json/JSONObject.java b/src/main/java/org/json/JSONObject.java new file mode 100644 index 0000000..cb8e21e --- /dev/null +++ b/src/main/java/org/json/JSONObject.java @@ -0,0 +1,2647 @@ +package org.json; + +import java.io.Closeable; +import java.io.IOException; +import java.io.StringWriter; +import java.io.Writer; +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.*; +import java.util.Map.Entry; +import java.util.regex.Pattern; + +/* + Copyright (c) 2002 JSON.org + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + The Software shall be used for Good, not Evil. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + */ + +/** + * A JSONObject is an unordered collection of name/value pairs. Its external + * form is a string wrapped in curly braces with colons between the names and + * values, and commas between the values and names. The internal form is an + * object having get and opt methods for accessing + * the values by name, and put methods for adding or replacing + * values by name. The values can be any of these types: Boolean, + * JSONArray, JSONObject, Number, + * String, or the JSONObject.NULL object. A + * JSONObject constructor can be used to convert an external form JSON text + * into an internal form whose values can be retrieved with the + * get and opt methods, or to convert values into a + * JSON text using the put and toString methods. A + * get method returns a value if one can be found, and throws an + * exception if one cannot be found. An opt method returns a + * default value instead of throwing an exception, and so is useful for + * obtaining optional values. + *

+ * The generic get() and opt() methods return an + * object, which you can cast or query for type. There are also typed + * get and opt methods that do type checking and type + * coercion for you. The opt methods differ from the get methods in that they + * do not throw. Instead, they return a specified value, such as null. + *

+ * The put methods add or replace values in an object. For + * example, + * + *

+ * myString = new JSONObject()
+ *         .put("JSON", "Hello, World!").toString();
+ * 
+ * + * produces the string {"JSON": "Hello, World"}. + *

+ * The texts produced by the toString methods strictly conform to + * the JSON syntax rules. The constructors are more forgiving in the texts they + * will accept: + *

    + *
  • An extra , (comma) may appear just + * before the closing brace.
  • + *
  • Strings may be quoted with ' (single + * quote).
  • + *
  • Strings do not need to be quoted at all if they do not begin with a + * quote or single quote, and if they do not contain leading or trailing + * spaces, and if they do not contain any of these characters: + * { } [ ] / \ : , # and if they do not look like numbers and + * if they are not the reserved words true, false, + * or null.
  • + *
+ * + * @author JSON.org + * @version 2016-08-15 + */ +public class JSONObject { + /** + * JSONObject.NULL is equivalent to the value that JavaScript calls null, + * whilst Java's null is equivalent to the value that JavaScript calls + * undefined. + */ + private static final class Null { + + /** + * There is only intended to be a single instance of the NULL object, + * so the clone method returns itself. + * + * @return NULL. + */ + @Override + protected final Object clone() { + return this; + } + + /** + * A Null object is equal to the null value and to itself. + * + * @param object + * An object to test for nullness. + * @return true if the object parameter is the JSONObject.NULL object or + * null. + */ + @Override + public boolean equals(Object object) { + return object == null || object == this; + } + /** + * A Null object is equal to the null value and to itself. + * + * @return always returns 0. + */ + @Override + public int hashCode() { + return 0; + } + + /** + * Get the "null" string value. + * + * @return The string "null". + */ + @Override + public String toString() { + return "null"; + } + } + + /** + * Regular Expression Pattern that matches JSON Numbers. This is primarily used for + * output to guarantee that we are always writing valid JSON. + */ + static final Pattern NUMBER_PATTERN = Pattern.compile("-?(?:0|[1-9]\\d*)(?:\\.\\d+)?(?:[eE][+-]?\\d+)?"); + + /** + * The map where the JSONObject's properties are kept. + */ + private final Map map; + + /** + * It is sometimes more convenient and less ambiguous to have a + * NULL object than to use Java's null value. + * JSONObject.NULL.equals(null) returns true. + * JSONObject.NULL.toString() returns "null". + */ + public static final Object NULL = new Null(); + + /** + * Construct an empty JSONObject. + */ + public JSONObject() { + // HashMap is used on purpose to ensure that elements are unordered by + // the specification. + // JSON tends to be a portable transfer format to allows the container + // implementations to rearrange their items for a faster element + // retrieval based on associative access. + // Therefore, an implementation mustn't rely on the order of the item. + this.map = new HashMap(); + } + + /** + * Construct a JSONObject from a subset of another JSONObject. An array of + * strings is used to identify the keys that should be copied. Missing keys + * are ignored. + * + * @param jo + * A JSONObject. + * @param names + * An array of strings. + */ + public JSONObject(JSONObject jo, String ... names) { + this(names.length); + for (int i = 0; i < names.length; i += 1) { + try { + this.putOnce(names[i], jo.opt(names[i])); + } catch (Exception ignore) { + } + } + } + + /** + * Construct a JSONObject from a JSONTokener. + * + * @param x + * A JSONTokener object containing the source string. + * @throws JSONException + * If there is a syntax error in the source string or a + * duplicated key. + */ + public JSONObject(JSONTokener x) throws JSONException { + this(); + char c; + String key; + + if (x.nextClean() != '{') { + throw x.syntaxError("A JSONObject text must begin with '{'"); + } + for (;;) { + c = x.nextClean(); + switch (c) { + case 0: + throw x.syntaxError("A JSONObject text must end with '}'"); + case '}': + return; + default: + x.back(); + key = x.nextValue().toString(); + } + + // The key is followed by ':'. + + c = x.nextClean(); + if (c != ':') { + throw x.syntaxError("Expected a ':' after a key"); + } + + // Use syntaxError(..) to include error location + + if (key != null) { + // Check if key exists + if (this.opt(key) != null) { + // key already exists + throw x.syntaxError("Duplicate key \"" + key + "\""); + } + // Only add value if non-null + Object value = x.nextValue(); + if (value!=null) { + this.put(key, value); + } + } + + // Pairs are separated by ','. + + switch (x.nextClean()) { + case ';': + case ',': + if (x.nextClean() == '}') { + return; + } + x.back(); + break; + case '}': + return; + default: + throw x.syntaxError("Expected a ',' or '}'"); + } + } + } + + /** + * Construct a JSONObject from a Map. + * + * @param m + * A map object that can be used to initialize the contents of + * the JSONObject. + * @throws JSONException + * If a value in the map is non-finite number. + * @throws NullPointerException + * If a key in the map is null + */ + public JSONObject(Map m) { + if (m == null) { + this.map = new HashMap(); + } else { + this.map = new HashMap(m.size()); + for (final Entry e : m.entrySet()) { + if(e.getKey() == null) { + throw new NullPointerException("Null key."); + } + final Object value = e.getValue(); + if (value != null) { + this.map.put(String.valueOf(e.getKey()), wrap(value)); + } + } + } + } + + /** + * Construct a JSONObject from an Object using bean getters. It reflects on + * all of the public methods of the object. For each of the methods with no + * parameters and a name starting with "get" or + * "is" followed by an uppercase letter, the method is invoked, + * and a key and the value returned from the getter method are put into the + * new JSONObject. + *

+ * The key is formed by removing the "get" or "is" + * prefix. If the second remaining character is not upper case, then the + * first character is converted to lower case. + *

+ * Methods that are static, return void, + * have parameters, or are "bridge" methods, are ignored. + *

+ * For example, if an object has a method named "getName", and + * if the result of calling object.getName() is + * "Larry Fine", then the JSONObject will contain + * "name": "Larry Fine". + *

+ * The {@link JSONPropertyName} annotation can be used on a bean getter to + * override key name used in the JSONObject. For example, using the object + * above with the getName method, if we annotated it with: + *

+     * @JSONPropertyName("FullName")
+     * public String getName() { return this.name; }
+     * 
+ * The resulting JSON object would contain "FullName": "Larry Fine" + *

+ * Similarly, the {@link JSONPropertyName} annotation can be used on non- + * get and is methods. We can also override key + * name used in the JSONObject as seen below even though the field would normally + * be ignored: + *

+     * @JSONPropertyName("FullName")
+     * public String fullName() { return this.name; }
+     * 
+ * The resulting JSON object would contain "FullName": "Larry Fine" + *

+ * The {@link JSONPropertyIgnore} annotation can be used to force the bean property + * to not be serialized into JSON. If both {@link JSONPropertyIgnore} and + * {@link JSONPropertyName} are defined on the same method, a depth comparison is + * performed and the one closest to the concrete class being serialized is used. + * If both annotations are at the same level, then the {@link JSONPropertyIgnore} + * annotation takes precedent and the field is not serialized. + * For example, the following declaration would prevent the getName + * method from being serialized: + *

+     * @JSONPropertyName("FullName")
+     * @JSONPropertyIgnore 
+     * public String getName() { return this.name; }
+     * 
+ *

+ * + * @param bean + * An object that has getter methods that should be used to make + * a JSONObject. + */ + public JSONObject(Object bean) { + this(); + this.populateMap(bean); + } + + /** + * Construct a JSONObject from an Object, using reflection to find the + * public members. The resulting JSONObject's keys will be the strings from + * the names array, and the values will be the field values associated with + * those keys in the object. If a key is not found or not visible, then it + * will not be copied into the new JSONObject. + * + * @param object + * An object that has fields that should be used to make a + * JSONObject. + * @param names + * An array of strings, the names of the fields to be obtained + * from the object. + */ + public JSONObject(Object object, String ... names) { + this(names.length); + Class c = object.getClass(); + for (int i = 0; i < names.length; i += 1) { + String name = names[i]; + try { + this.putOpt(name, c.getField(name).get(object)); + } catch (Exception ignore) { + } + } + } + + /** + * Construct a JSONObject from a source JSON text string. This is the most + * commonly used JSONObject constructor. + * + * @param source + * A string beginning with { (left + * brace) and ending with } + *  (right brace). + * @exception JSONException + * If there is a syntax error in the source string or a + * duplicated key. + */ + public JSONObject(String source) throws JSONException { + this(new JSONTokener(source)); + } + + /** + * Construct a JSONObject from a ResourceBundle. + * + * @param baseName + * The ResourceBundle base name. + * @param locale + * The Locale to load the ResourceBundle for. + * @throws JSONException + * If any JSONExceptions are detected. + */ + public JSONObject(String baseName, Locale locale) throws JSONException { + this(); + ResourceBundle bundle = ResourceBundle.getBundle(baseName, locale, + Thread.currentThread().getContextClassLoader()); + +// Iterate through the keys in the bundle. + + Enumeration keys = bundle.getKeys(); + while (keys.hasMoreElements()) { + Object key = keys.nextElement(); + if (key != null) { + +// Go through the path, ensuring that there is a nested JSONObject for each +// segment except the last. Add the value using the last segment's name into +// the deepest nested JSONObject. + + String[] path = ((String) key).split("\\."); + int last = path.length - 1; + JSONObject target = this; + for (int i = 0; i < last; i += 1) { + String segment = path[i]; + JSONObject nextTarget = target.optJSONObject(segment); + if (nextTarget == null) { + nextTarget = new JSONObject(); + target.put(segment, nextTarget); + } + target = nextTarget; + } + target.put(path[last], bundle.getString((String) key)); + } + } + } + + /** + * Constructor to specify an initial capacity of the internal map. Useful for library + * internal calls where we know, or at least can best guess, how big this JSONObject + * will be. + * + * @param initialCapacity initial capacity of the internal map. + */ + protected JSONObject(int initialCapacity){ + this.map = new HashMap(initialCapacity); + } + + /** + * Accumulate values under a key. It is similar to the put method except + * that if there is already an object stored under the key then a JSONArray + * is stored under the key to hold all of the accumulated values. If there + * is already a JSONArray, then the new value is appended to it. In + * contrast, the put method replaces the previous value. + * + * If only one value is accumulated that is not a JSONArray, then the result + * will be the same as using put. But if multiple values are accumulated, + * then the result will be like append. + * + * @param key + * A key string. + * @param value + * An object to be accumulated under the key. + * @return this. + * @throws JSONException + * If the value is non-finite number. + * @throws NullPointerException + * If the key is null. + */ + public JSONObject accumulate(String key, Object value) throws JSONException { + testValidity(value); + Object object = this.opt(key); + if (object == null) { + this.put(key, + value instanceof JSONArray ? new JSONArray().put(value) + : value); + } else if (object instanceof JSONArray) { + ((JSONArray) object).put(value); + } else { + this.put(key, new JSONArray().put(object).put(value)); + } + return this; + } + + /** + * Append values to the array under a key. If the key does not exist in the + * JSONObject, then the key is put in the JSONObject with its value being a + * JSONArray containing the value parameter. If the key was already + * associated with a JSONArray, then the value parameter is appended to it. + * + * @param key + * A key string. + * @param value + * An object to be accumulated under the key. + * @return this. + * @throws JSONException + * If the value is non-finite number or if the current value associated with + * the key is not a JSONArray. + * @throws NullPointerException + * If the key is null. + */ + public JSONObject append(String key, Object value) throws JSONException { + testValidity(value); + Object object = this.opt(key); + if (object == null) { + this.put(key, new JSONArray().put(value)); + } else if (object instanceof JSONArray) { + this.put(key, ((JSONArray) object).put(value)); + } else { + throw wrongValueFormatException(key, "JSONArray", null, null); + } + return this; + } + + /** + * Produce a string from a double. The string "null" will be returned if the + * number is not finite. + * + * @param d + * A double. + * @return A String. + */ + public static String doubleToString(double d) { + if (Double.isInfinite(d) || Double.isNaN(d)) { + return "null"; + } + +// Shave off trailing zeros and decimal point, if possible. + + String string = Double.toString(d); + if (string.indexOf('.') > 0 && string.indexOf('e') < 0 + && string.indexOf('E') < 0) { + while (string.endsWith("0")) { + string = string.substring(0, string.length() - 1); + } + if (string.endsWith(".")) { + string = string.substring(0, string.length() - 1); + } + } + return string; + } + + /** + * Get the value object associated with a key. + * + * @param key + * A key string. + * @return The object associated with the key. + * @throws JSONException + * if the key is not found. + */ + public Object get(String key) throws JSONException { + if (key == null) { + throw new JSONException("Null key."); + } + Object object = this.opt(key); + if (object == null) { + throw new JSONException("JSONObject[" + quote(key) + "] not found."); + } + return object; + } + + /** + * Get the enum value associated with a key. + * + * @param + * Enum Type + * @param clazz + * The type of enum to retrieve. + * @param key + * A key string. + * @return The enum value associated with the key + * @throws JSONException + * if the key is not found or if the value cannot be converted + * to an enum. + */ + public > E getEnum(Class clazz, String key) throws JSONException { + E val = optEnum(clazz, key); + if(val==null) { + // JSONException should really take a throwable argument. + // If it did, I would re-implement this with the Enum.valueOf + // method and place any thrown exception in the JSONException + throw wrongValueFormatException(key, "enum of type " + quote(clazz.getSimpleName()), null); + } + return val; + } + + /** + * Get the boolean value associated with a key. + * + * @param key + * A key string. + * @return The truth. + * @throws JSONException + * if the value is not a Boolean or the String "true" or + * "false". + */ + public boolean getBoolean(String key) throws JSONException { + Object object = this.get(key); + if (object.equals(Boolean.FALSE) + || (object instanceof String && ((String) object) + .equalsIgnoreCase("false"))) { + return false; + } else if (object.equals(Boolean.TRUE) + || (object instanceof String && ((String) object) + .equalsIgnoreCase("true"))) { + return true; + } + throw wrongValueFormatException(key, "Boolean", null); + } + + /** + * Get the BigInteger value associated with a key. + * + * @param key + * A key string. + * @return The numeric value. + * @throws JSONException + * if the key is not found or if the value cannot + * be converted to BigInteger. + */ + public BigInteger getBigInteger(String key) throws JSONException { + Object object = this.get(key); + BigInteger ret = objectToBigInteger(object, null); + if (ret != null) { + return ret; + } + throw wrongValueFormatException(key, "BigInteger", object, null); + } + + /** + * Get the BigDecimal value associated with a key. If the value is float or + * double, the the {@link BigDecimal#BigDecimal(double)} constructor will + * be used. See notes on the constructor for conversion issues that may + * arise. + * + * @param key + * A key string. + * @return The numeric value. + * @throws JSONException + * if the key is not found or if the value + * cannot be converted to BigDecimal. + */ + public BigDecimal getBigDecimal(String key) throws JSONException { + Object object = this.get(key); + BigDecimal ret = objectToBigDecimal(object, null); + if (ret != null) { + return ret; + } + throw wrongValueFormatException(key, "BigDecimal", object, null); + } + + /** + * Get the double value associated with a key. + * + * @param key + * A key string. + * @return The numeric value. + * @throws JSONException + * if the key is not found or if the value is not a Number + * object and cannot be converted to a number. + */ + public double getDouble(String key) throws JSONException { + final Object object = this.get(key); + if(object instanceof Number) { + return ((Number)object).doubleValue(); + } + try { + return Double.parseDouble(object.toString()); + } catch (Exception e) { + throw wrongValueFormatException(key, "double", e); + } + } + + /** + * Get the float value associated with a key. + * + * @param key + * A key string. + * @return The numeric value. + * @throws JSONException + * if the key is not found or if the value is not a Number + * object and cannot be converted to a number. + */ + public float getFloat(String key) throws JSONException { + final Object object = this.get(key); + if(object instanceof Number) { + return ((Number)object).floatValue(); + } + try { + return Float.parseFloat(object.toString()); + } catch (Exception e) { + throw wrongValueFormatException(key, "float", e); + } + } + + /** + * Get the Number value associated with a key. + * + * @param key + * A key string. + * @return The numeric value. + * @throws JSONException + * if the key is not found or if the value is not a Number + * object and cannot be converted to a number. + */ + public Number getNumber(String key) throws JSONException { + Object object = this.get(key); + try { + if (object instanceof Number) { + return (Number)object; + } + return stringToNumber(object.toString()); + } catch (Exception e) { + throw wrongValueFormatException(key, "number", e); + } + } + + /** + * Get the int value associated with a key. + * + * @param key + * A key string. + * @return The integer value. + * @throws JSONException + * if the key is not found or if the value cannot be converted + * to an integer. + */ + public int getInt(String key) throws JSONException { + final Object object = this.get(key); + if(object instanceof Number) { + return ((Number)object).intValue(); + } + try { + return Integer.parseInt(object.toString()); + } catch (Exception e) { + throw wrongValueFormatException(key, "int", e); + } + } + + /** + * Get the JSONArray value associated with a key. + * + * @param key + * A key string. + * @return A JSONArray which is the value. + * @throws JSONException + * if the key is not found or if the value is not a JSONArray. + */ + public JSONArray getJSONArray(String key) throws JSONException { + Object object = this.get(key); + if (object instanceof JSONArray) { + return (JSONArray) object; + } + throw wrongValueFormatException(key, "JSONArray", null); + } + + /** + * Get the JSONObject value associated with a key. + * + * @param key + * A key string. + * @return A JSONObject which is the value. + * @throws JSONException + * if the key is not found or if the value is not a JSONObject. + */ + public JSONObject getJSONObject(String key) throws JSONException { + Object object = this.get(key); + if (object instanceof JSONObject) { + return (JSONObject) object; + } + throw wrongValueFormatException(key, "JSONObject", null); + } + + /** + * Get the long value associated with a key. + * + * @param key + * A key string. + * @return The long value. + * @throws JSONException + * if the key is not found or if the value cannot be converted + * to a long. + */ + public long getLong(String key) throws JSONException { + final Object object = this.get(key); + if(object instanceof Number) { + return ((Number)object).longValue(); + } + try { + return Long.parseLong(object.toString()); + } catch (Exception e) { + throw wrongValueFormatException(key, "long", e); + } + } + + public UUID getUUID(String key) throws JSONException { + Object object = this.get(key); + if (object instanceof UUID) { + return (UUID) object; + } else if (object instanceof String) { + return UUID.fromString(String.valueOf(object)); + } + throw wrongValueFormatException(key, "UUID", null); + } + + /** + * Get an array of field names from a JSONObject. + * + * @param jo + * JSON object + * @return An array of field names, or null if there are no names. + */ + public static String[] getNames(JSONObject jo) { + if (jo.isEmpty()) { + return null; + } + return jo.keySet().toArray(new String[jo.length()]); + } + + /** + * Get an array of public field names from an Object. + * + * @param object + * object to read + * @return An array of field names, or null if there are no names. + */ + public static String[] getNames(Object object) { + if (object == null) { + return null; + } + Class klass = object.getClass(); + Field[] fields = klass.getFields(); + int length = fields.length; + if (length == 0) { + return null; + } + String[] names = new String[length]; + for (int i = 0; i < length; i += 1) { + names[i] = fields[i].getName(); + } + return names; + } + + /** + * Get the string associated with a key. + * + * @param key + * A key string. + * @return A string which is the value. + * @throws JSONException + * if there is no string value for the key. + */ + public String getString(String key) throws JSONException { + Object object = this.get(key); + if (object instanceof String) { + return (String) object; + } + throw wrongValueFormatException(key, "string", null); + } + + public byte getByte(String key) throws JSONException { + Object object = this.get(key); + if (object instanceof Byte) { + return (byte) object; + } + throw wrongValueFormatException(key, "byte", null); + } + + public byte[] getByteArray(String key) throws JSONException { + Object object = this.get(key); + if (object instanceof JSONArray) { + JSONArray jsonArray = (JSONArray) object; + byte[] array = new byte[jsonArray.length()]; + for (int k = 0; k < jsonArray.length(); k++) { + array[k] = Byte.valueOf(String.valueOf(jsonArray.get(k))); + } + return array; + } + if (object instanceof byte[]) { + return (byte[]) object; + } + throw wrongValueFormatException(key, "byteArray", null); + } + + /** + * Determine if the JSONObject contains a specific key. + * + * @param key + * A key string. + * @return true if the key exists in the JSONObject. + */ + public boolean has(String key) { + return this.map.containsKey(key); + } + + /** + * Increment a property of a JSONObject. If there is no such property, + * create one with a value of 1 (Integer). If there is such a property, and if it is + * an Integer, Long, Double, Float, BigInteger, or BigDecimal then add one to it. + * No overflow bounds checking is performed, so callers should initialize the key + * prior to this call with an appropriate type that can handle the maximum expected + * value. + * + * @param key + * A key string. + * @return this. + * @throws JSONException + * If there is already a property with this name that is not an + * Integer, Long, Double, or Float. + */ + public JSONObject increment(String key) throws JSONException { + Object value = this.opt(key); + if (value == null) { + this.put(key, 1); + } else if (value instanceof Integer) { + this.put(key, ((Integer) value).intValue() + 1); + } else if (value instanceof Long) { + this.put(key, ((Long) value).longValue() + 1L); + } else if (value instanceof BigInteger) { + this.put(key, ((BigInteger)value).add(BigInteger.ONE)); + } else if (value instanceof Float) { + this.put(key, ((Float) value).floatValue() + 1.0f); + } else if (value instanceof Double) { + this.put(key, ((Double) value).doubleValue() + 1.0d); + } else if (value instanceof BigDecimal) { + this.put(key, ((BigDecimal)value).add(BigDecimal.ONE)); + } else { + throw new JSONException("Unable to increment [" + quote(key) + "]."); + } + return this; + } + + /** + * Determine if the value associated with the key is null or if there is no + * value. + * + * @param key + * A key string. + * @return true if there is no value associated with the key or if the value + * is the JSONObject.NULL object. + */ + public boolean isNull(String key) { + return JSONObject.NULL.equals(this.opt(key)); + } + + /** + * Get an enumeration of the keys of the JSONObject. Modifying this key Set will also + * modify the JSONObject. Use with caution. + * + * @see Set#iterator() + * + * @return An iterator of the keys. + */ + public Iterator keys() { + return this.keySet().iterator(); + } + + /** + * Get a set of keys of the JSONObject. Modifying this key Set will also modify the + * JSONObject. Use with caution. + * + * @see Map#keySet() + * + * @return A keySet. + */ + public Set keySet() { + return this.map.keySet(); + } + + /** + * Get a set of entries of the JSONObject. These are raw values and may not + * match what is returned by the JSONObject get* and opt* functions. Modifying + * the returned EntrySet or the Entry objects contained therein will modify the + * backing JSONObject. This does not return a clone or a read-only view. + * + * Use with caution. + * + * @see Map#entrySet() + * + * @return An Entry Set + */ + protected Set> entrySet() { + return this.map.entrySet(); + } + + /** + * Get the number of keys stored in the JSONObject. + * + * @return The number of keys in the JSONObject. + */ + public int length() { + return this.map.size(); + } + + /** + * Check if JSONObject is empty. + * + * @return true if JSONObject is empty, otherwise false. + */ + public boolean isEmpty() { + return this.map.isEmpty(); + } + + /** + * Produce a JSONArray containing the names of the elements of this + * JSONObject. + * + * @return A JSONArray containing the key strings, or null if the JSONObject + * is empty. + */ + public JSONArray names() { + if(this.map.isEmpty()) { + return null; + } + return new JSONArray(this.map.keySet()); + } + + /** + * Produce a string from a Number. + * + * @param number + * A Number + * @return A String. + * @throws JSONException + * If n is a non-finite number. + */ + public static String numberToString(Number number) throws JSONException { + if (number == null) { + throw new JSONException("Null pointer"); + } + testValidity(number); + + // Shave off trailing zeros and decimal point, if possible. + + String string = number.toString(); + if (string.indexOf('.') > 0 && string.indexOf('e') < 0 + && string.indexOf('E') < 0) { + while (string.endsWith("0")) { + string = string.substring(0, string.length() - 1); + } + if (string.endsWith(".")) { + string = string.substring(0, string.length() - 1); + } + } + return string; + } + + /** + * Get an optional value associated with a key. + * + * @param key + * A key string. + * @return An object which is the value, or null if there is no value. + */ + public Object opt(String key) { + return key == null ? null : this.map.get(key); + } + + /** + * Get the enum value associated with a key. + * + * @param + * Enum Type + * @param clazz + * The type of enum to retrieve. + * @param key + * A key string. + * @return The enum value associated with the key or null if not found + */ + public > E optEnum(Class clazz, String key) { + return this.optEnum(clazz, key, null); + } + + /** + * Get the enum value associated with a key. + * + * @param + * Enum Type + * @param clazz + * The type of enum to retrieve. + * @param key + * A key string. + * @param defaultValue + * The default in case the value is not found + * @return The enum value associated with the key or defaultValue + * if the value is not found or cannot be assigned to clazz + */ + public > E optEnum(Class clazz, String key, E defaultValue) { + try { + Object val = this.opt(key); + if (NULL.equals(val)) { + return defaultValue; + } + if (clazz.isAssignableFrom(val.getClass())) { + // we just checked it! + @SuppressWarnings("unchecked") + E myE = (E) val; + return myE; + } + return Enum.valueOf(clazz, val.toString()); + } catch (IllegalArgumentException e) { + return defaultValue; + } catch (NullPointerException e) { + return defaultValue; + } + } + + /** + * Get an optional boolean associated with a key. It returns false if there + * is no such key, or if the value is not Boolean.TRUE or the String "true". + * + * @param key + * A key string. + * @return The truth. + */ + public boolean optBoolean(String key) { + return this.optBoolean(key, false); + } + + /** + * Get an optional boolean associated with a key. It returns the + * defaultValue if there is no such key, or if it is not a Boolean or the + * String "true" or "false" (case insensitive). + * + * @param key + * A key string. + * @param defaultValue + * The default. + * @return The truth. + */ + public boolean optBoolean(String key, boolean defaultValue) { + Object val = this.opt(key); + if (NULL.equals(val)) { + return defaultValue; + } + if (val instanceof Boolean){ + return ((Boolean) val).booleanValue(); + } + try { + // we'll use the get anyway because it does string conversion. + return this.getBoolean(key); + } catch (Exception e) { + return defaultValue; + } + } + + /** + * Get an optional BigDecimal associated with a key, or the defaultValue if + * there is no such key or if its value is not a number. If the value is a + * string, an attempt will be made to evaluate it as a number. If the value + * is float or double, then the {@link BigDecimal#BigDecimal(double)} + * constructor will be used. See notes on the constructor for conversion + * issues that may arise. + * + * @param key + * A key string. + * @param defaultValue + * The default. + * @return An object which is the value. + */ + public BigDecimal optBigDecimal(String key, BigDecimal defaultValue) { + Object val = this.opt(key); + return objectToBigDecimal(val, defaultValue); + } + + /** + * @param val value to convert + * @param defaultValue default value to return is the conversion doesn't work or is null. + * @return BigDecimal conversion of the original value, or the defaultValue if unable + * to convert. + */ + static BigDecimal objectToBigDecimal(Object val, BigDecimal defaultValue) { + if (NULL.equals(val)) { + return defaultValue; + } + if (val instanceof BigDecimal){ + return (BigDecimal) val; + } + if (val instanceof BigInteger){ + return new BigDecimal((BigInteger) val); + } + if (val instanceof Double || val instanceof Float){ + final double d = ((Number) val).doubleValue(); + if(Double.isNaN(d)) { + return defaultValue; + } + return new BigDecimal(((Number) val).doubleValue()); + } + if (val instanceof Long || val instanceof Integer + || val instanceof Short || val instanceof Byte){ + return new BigDecimal(((Number) val).longValue()); + } + // don't check if it's a string in case of unchecked Number subclasses + try { + return new BigDecimal(val.toString()); + } catch (Exception e) { + return defaultValue; + } + } + + /** + * Get an optional BigInteger associated with a key, or the defaultValue if + * there is no such key or if its value is not a number. If the value is a + * string, an attempt will be made to evaluate it as a number. + * + * @param key + * A key string. + * @param defaultValue + * The default. + * @return An object which is the value. + */ + public BigInteger optBigInteger(String key, BigInteger defaultValue) { + Object val = this.opt(key); + return objectToBigInteger(val, defaultValue); + } + + /** + * @param val value to convert + * @param defaultValue default value to return is the conversion doesn't work or is null. + * @return BigInteger conversion of the original value, or the defaultValue if unable + * to convert. + */ + static BigInteger objectToBigInteger(Object val, BigInteger defaultValue) { + if (NULL.equals(val)) { + return defaultValue; + } + if (val instanceof BigInteger){ + return (BigInteger) val; + } + if (val instanceof BigDecimal){ + return ((BigDecimal) val).toBigInteger(); + } + if (val instanceof Double || val instanceof Float){ + final double d = ((Number) val).doubleValue(); + if(Double.isNaN(d)) { + return defaultValue; + } + return new BigDecimal(d).toBigInteger(); + } + if (val instanceof Long || val instanceof Integer + || val instanceof Short || val instanceof Byte){ + return BigInteger.valueOf(((Number) val).longValue()); + } + // don't check if it's a string in case of unchecked Number subclasses + try { + // the other opt functions handle implicit conversions, i.e. + // jo.put("double",1.1d); + // jo.optInt("double"); -- will return 1, not an error + // this conversion to BigDecimal then to BigInteger is to maintain + // that type cast support that may truncate the decimal. + final String valStr = val.toString(); + if(isDecimalNotation(valStr)) { + return new BigDecimal(valStr).toBigInteger(); + } + return new BigInteger(valStr); + } catch (Exception e) { + return defaultValue; + } + } + + /** + * Get an optional double associated with a key, or NaN if there is no such + * key or if its value is not a number. If the value is a string, an attempt + * will be made to evaluate it as a number. + * + * @param key + * A string which is the key. + * @return An object which is the value. + */ + public double optDouble(String key) { + return this.optDouble(key, Double.NaN); + } + + /** + * Get an optional double associated with a key, or the defaultValue if + * there is no such key or if its value is not a number. If the value is a + * string, an attempt will be made to evaluate it as a number. + * + * @param key + * A key string. + * @param defaultValue + * The default. + * @return An object which is the value. + */ + public double optDouble(String key, double defaultValue) { + Number val = this.optNumber(key); + if (val == null) { + return defaultValue; + } + final double doubleValue = val.doubleValue(); + // if (Double.isNaN(doubleValue) || Double.isInfinite(doubleValue)) { + // return defaultValue; + // } + return doubleValue; + } + + /** + * Get the optional double value associated with an index. NaN is returned + * if there is no value for the index, or if the value is not a number and + * cannot be converted to a number. + * + * @param key + * A key string. + * @return The value. + */ + public float optFloat(String key) { + return this.optFloat(key, Float.NaN); + } + + /** + * Get the optional double value associated with an index. The defaultValue + * is returned if there is no value for the index, or if the value is not a + * number and cannot be converted to a number. + * + * @param key + * A key string. + * @param defaultValue + * The default value. + * @return The value. + */ + public float optFloat(String key, float defaultValue) { + Number val = this.optNumber(key); + if (val == null) { + return defaultValue; + } + final float floatValue = val.floatValue(); + // if (Float.isNaN(floatValue) || Float.isInfinite(floatValue)) { + // return defaultValue; + // } + return floatValue; + } + + /** + * Get an optional int value associated with a key, or zero if there is no + * such key or if the value is not a number. If the value is a string, an + * attempt will be made to evaluate it as a number. + * + * @param key + * A key string. + * @return An object which is the value. + */ + public int optInt(String key) { + return this.optInt(key, 0); + } + + /** + * Get an optional int value associated with a key, or the default if there + * is no such key or if the value is not a number. If the value is a string, + * an attempt will be made to evaluate it as a number. + * + * @param key + * A key string. + * @param defaultValue + * The default. + * @return An object which is the value. + */ + public int optInt(String key, int defaultValue) { + final Number val = this.optNumber(key, null); + if (val == null) { + return defaultValue; + } + return val.intValue(); + } + + /** + * Get an optional JSONArray associated with a key. It returns null if there + * is no such key, or if its value is not a JSONArray. + * + * @param key + * A key string. + * @return A JSONArray which is the value. + */ + public JSONArray optJSONArray(String key) { + Object o = this.opt(key); + return o instanceof JSONArray ? (JSONArray) o : null; + } + + /** + * Get an optional JSONObject associated with a key. It returns null if + * there is no such key, or if its value is not a JSONObject. + * + * @param key + * A key string. + * @return A JSONObject which is the value. + */ + public JSONObject optJSONObject(String key) { + Object object = this.opt(key); + return object instanceof JSONObject ? (JSONObject) object : null; + } + + /** + * Get an optional long value associated with a key, or zero if there is no + * such key or if the value is not a number. If the value is a string, an + * attempt will be made to evaluate it as a number. + * + * @param key + * A key string. + * @return An object which is the value. + */ + public long optLong(String key) { + return this.optLong(key, 0); + } + + /** + * Get an optional long value associated with a key, or the default if there + * is no such key or if the value is not a number. If the value is a string, + * an attempt will be made to evaluate it as a number. + * + * @param key + * A key string. + * @param defaultValue + * The default. + * @return An object which is the value. + */ + public long optLong(String key, long defaultValue) { + final Number val = this.optNumber(key, null); + if (val == null) { + return defaultValue; + } + + return val.longValue(); + } + + /** + * Get an optional {@link Number} value associated with a key, or null + * if there is no such key or if the value is not a number. If the value is a string, + * an attempt will be made to evaluate it as a number ({@link BigDecimal}). This method + * would be used in cases where type coercion of the number value is unwanted. + * + * @param key + * A key string. + * @return An object which is the value. + */ + public Number optNumber(String key) { + return this.optNumber(key, null); + } + + /** + * Get an optional {@link Number} value associated with a key, or the default if there + * is no such key or if the value is not a number. If the value is a string, + * an attempt will be made to evaluate it as a number. This method + * would be used in cases where type coercion of the number value is unwanted. + * + * @param key + * A key string. + * @param defaultValue + * The default. + * @return An object which is the value. + */ + public Number optNumber(String key, Number defaultValue) { + Object val = this.opt(key); + if (NULL.equals(val)) { + return defaultValue; + } + if (val instanceof Number){ + return (Number) val; + } + + try { + return stringToNumber(val.toString()); + } catch (Exception e) { + return defaultValue; + } + } + + /** + * Get an optional string associated with a key. It returns an empty string + * if there is no such key. If the value is not a string and is not null, + * then it is converted to a string. + * + * @param key + * A key string. + * @return A string which is the value. + */ + public String optString(String key) { + return this.optString(key, ""); + } + + /** + * Get an optional string associated with a key. It returns the defaultValue + * if there is no such key. + * + * @param key + * A key string. + * @param defaultValue + * The default. + * @return A string which is the value. + */ + public String optString(String key, String defaultValue) { + Object object = this.opt(key); + return NULL.equals(object) ? defaultValue : object.toString(); + } + + /** + * Populates the internal map of the JSONObject with the bean properties. The + * bean can not be recursive. + * + * @see JSONObject#JSONObject(Object) + * + * @param bean + * the bean + */ + private void populateMap(Object bean) { + Class klass = bean.getClass(); + + // If klass is a System class then set includeSuperClass to false. + + boolean includeSuperClass = klass.getClassLoader() != null; + + Method[] methods = includeSuperClass ? klass.getMethods() : klass.getDeclaredMethods(); + for (final Method method : methods) { + final int modifiers = method.getModifiers(); + if (Modifier.isPublic(modifiers) + && !Modifier.isStatic(modifiers) + && method.getParameterTypes().length == 0 + && !method.isBridge() + && method.getReturnType() != Void.TYPE + && isValidMethodName(method.getName())) { + final String key = getKeyNameFromMethod(method); + if (key != null && !key.isEmpty()) { + try { + final Object result = method.invoke(bean); + if (result != null) { + this.map.put(key, wrap(result)); + // we don't use the result anywhere outside of wrap + // if it's a resource we should be sure to close it + // after calling toString + if (result instanceof Closeable) { + try { + ((Closeable) result).close(); + } catch (IOException ignore) { + } + } + } + } catch (IllegalAccessException ignore) { + } catch (IllegalArgumentException ignore) { + } catch (InvocationTargetException ignore) { + } + } + } + } + } + + private static boolean isValidMethodName(String name) { + return !"getClass".equals(name) && !"getDeclaringClass".equals(name); + } + + private static String getKeyNameFromMethod(Method method) { + final int ignoreDepth = getAnnotationDepth(method, JSONPropertyIgnore.class); + if (ignoreDepth > 0) { + final int forcedNameDepth = getAnnotationDepth(method, JSONPropertyName.class); + if (forcedNameDepth < 0 || ignoreDepth <= forcedNameDepth) { + // the hierarchy asked to ignore, and the nearest name override + // was higher or non-existent + return null; + } + } + JSONPropertyName annotation = getAnnotation(method, JSONPropertyName.class); + if (annotation != null && annotation.value() != null && !annotation.value().isEmpty()) { + return annotation.value(); + } + String key; + final String name = method.getName(); + if (name.startsWith("get") && name.length() > 3) { + key = name.substring(3); + } else if (name.startsWith("is") && name.length() > 2) { + key = name.substring(2); + } else { + return null; + } + // if the first letter in the key is not uppercase, then skip. + // This is to maintain backwards compatibility before PR406 + // (https://github.com/stleary/JSON-java/pull/406/) + if (Character.isLowerCase(key.charAt(0))) { + return null; + } + if (key.length() == 1) { + key = key.toLowerCase(Locale.ROOT); + } else if (!Character.isUpperCase(key.charAt(1))) { + key = key.substring(0, 1).toLowerCase(Locale.ROOT) + key.substring(1); + } + return key; + } + + /** + * Searches the class hierarchy to see if the method or it's super + * implementations and interfaces has the annotation. + * + * @param + * type of the annotation + * + * @param m + * method to check + * @param annotationClass + * annotation to look for + * @return the {@link Annotation} if the annotation exists on the current method + * or one of it's super class definitions + */ + private static A getAnnotation(final Method m, final Class annotationClass) { + // if we have invalid data the result is null + if (m == null || annotationClass == null) { + return null; + } + + if (m.isAnnotationPresent(annotationClass)) { + return m.getAnnotation(annotationClass); + } + + // if we've already reached the Object class, return null; + Class c = m.getDeclaringClass(); + if (c.getSuperclass() == null) { + return null; + } + + // check directly implemented interfaces for the method being checked + for (Class i : c.getInterfaces()) { + try { + Method im = i.getMethod(m.getName(), m.getParameterTypes()); + return getAnnotation(im, annotationClass); + } catch (final SecurityException ex) { + continue; + } catch (final NoSuchMethodException ex) { + continue; + } + } + + try { + return getAnnotation( + c.getSuperclass().getMethod(m.getName(), m.getParameterTypes()), + annotationClass); + } catch (final SecurityException ex) { + return null; + } catch (final NoSuchMethodException ex) { + return null; + } + } + + /** + * Searches the class hierarchy to see if the method or it's super + * implementations and interfaces has the annotation. Returns the depth of the + * annotation in the hierarchy. + * + * type of the annotation + * + * @param m + * method to check + * @param annotationClass + * annotation to look for + * @return Depth of the annotation or -1 if the annotation is not on the method. + */ + private static int getAnnotationDepth(final Method m, final Class annotationClass) { + // if we have invalid data the result is -1 + if (m == null || annotationClass == null) { + return -1; + } + + if (m.isAnnotationPresent(annotationClass)) { + return 1; + } + + // if we've already reached the Object class, return -1; + Class c = m.getDeclaringClass(); + if (c.getSuperclass() == null) { + return -1; + } + + // check directly implemented interfaces for the method being checked + for (Class i : c.getInterfaces()) { + try { + Method im = i.getMethod(m.getName(), m.getParameterTypes()); + int d = getAnnotationDepth(im, annotationClass); + if (d > 0) { + // since the annotation was on the interface, add 1 + return d + 1; + } + } catch (final SecurityException ex) { + continue; + } catch (final NoSuchMethodException ex) { + continue; + } + } + + try { + int d = getAnnotationDepth( + c.getSuperclass().getMethod(m.getName(), m.getParameterTypes()), + annotationClass); + if (d > 0) { + // since the annotation was on the superclass, add 1 + return d + 1; + } + return -1; + } catch (final SecurityException ex) { + return -1; + } catch (final NoSuchMethodException ex) { + return -1; + } + } + + /** + * Put a key/boolean pair in the JSONObject. + * + * @param key + * A key string. + * @param value + * A boolean which is the value. + * @return this. + * @throws JSONException + * If the value is non-finite number. + * @throws NullPointerException + * If the key is null. + */ + public JSONObject put(String key, boolean value) throws JSONException { + return this.put(key, value ? Boolean.TRUE : Boolean.FALSE); + } + + public JSONObject put(String key, byte value) throws JSONException { + return this.put(key, Byte.valueOf(value)); + } + + /** + * Put a key/value pair in the JSONObject, where the value will be a + * JSONArray which is produced from a Collection. + * + * @param key + * A key string. + * @param value + * A Collection value. + * @return this. + * @throws JSONException + * If the value is non-finite number. + * @throws NullPointerException + * If the key is null. + */ + public JSONObject put(String key, Collection value) throws JSONException { + return this.put(key, new JSONArray(value)); + } + + /** + * Put a key/double pair in the JSONObject. + * + * @param key + * A key string. + * @param value + * A double which is the value. + * @return this. + * @throws JSONException + * If the value is non-finite number. + * @throws NullPointerException + * If the key is null. + */ + public JSONObject put(String key, double value) throws JSONException { + return this.put(key, Double.valueOf(value)); + } + + /** + * Put a key/float pair in the JSONObject. + * + * @param key + * A key string. + * @param value + * A float which is the value. + * @return this. + * @throws JSONException + * If the value is non-finite number. + * @throws NullPointerException + * If the key is null. + */ + public JSONObject put(String key, float value) throws JSONException { + return this.put(key, Float.valueOf(value)); + } + + /** + * Put a key/int pair in the JSONObject. + * + * @param key + * A key string. + * @param value + * An int which is the value. + * @return this. + * @throws JSONException + * If the value is non-finite number. + * @throws NullPointerException + * If the key is null. + */ + public JSONObject put(String key, int value) throws JSONException { + return this.put(key, Integer.valueOf(value)); + } + + /** + * Put a key/long pair in the JSONObject. + * + * @param key + * A key string. + * @param value + * A long which is the value. + * @return this. + * @throws JSONException + * If the value is non-finite number. + * @throws NullPointerException + * If the key is null. + */ + public JSONObject put(String key, long value) throws JSONException { + return this.put(key, Long.valueOf(value)); + } + + /** + * Put a key/value pair in the JSONObject, where the value will be a + * JSONObject which is produced from a Map. + * + * @param key + * A key string. + * @param value + * A Map value. + * @return this. + * @throws JSONException + * If the value is non-finite number. + * @throws NullPointerException + * If the key is null. + */ + public JSONObject put(String key, Map value) throws JSONException { + return this.put(key, new JSONObject(value)); + } + + /** + * Put a key/value pair in the JSONObject. If the value is null, then the + * key will be removed from the JSONObject if it is present. + * + * @param key + * A key string. + * @param value + * An object which is the value. It should be of one of these + * types: Boolean, Double, Integer, JSONArray, JSONObject, Long, + * String, or the JSONObject.NULL object. + * @return this. + * @throws JSONException + * If the value is non-finite number. + * @throws NullPointerException + * If the key is null. + */ + public JSONObject put(String key, Object value) throws JSONException { + if (key == null) { + throw new NullPointerException("Null key."); + } + if (value != null) { + testValidity(value); + this.map.put(key, value); + } else { + this.remove(key); + } + return this; + } + + public JSONObject put(String key, byte[] bytes) throws JSONException { + return this.put(key, (Object) bytes); + } + + public JSONObject put(String key, UUID uuid) throws JSONException { + return this.put(key, uuid.toString()); + } + + /** + * Put a key/value pair in the JSONObject, but only if the key and the value + * are both non-null, and only if there is not already a member with that + * name. + * + * @param key + * key to insert into + * @param value + * value to insert + * @return this. + * @throws JSONException + * if the key is a duplicate + */ + public JSONObject putOnce(String key, Object value) throws JSONException { + if (key != null && value != null) { + if (this.opt(key) != null) { + throw new JSONException("Duplicate key \"" + key + "\""); + } + return this.put(key, value); + } + return this; + } + + /** + * Put a key/value pair in the JSONObject, but only if the key and the value + * are both non-null. + * + * @param key + * A key string. + * @param value + * An object which is the value. It should be of one of these + * types: Boolean, Double, Integer, JSONArray, JSONObject, Long, + * String, or the JSONObject.NULL object. + * @return this. + * @throws JSONException + * If the value is a non-finite number. + */ + public JSONObject putOpt(String key, Object value) throws JSONException { + if (key != null && value != null) { + return this.put(key, value); + } + return this; + } + + /** + * Creates a JSONPointer using an initialization string and tries to + * match it to an item within this JSONObject. For example, given a + * JSONObject initialized with this document: + *

+     * {
+     *     "a":{"b":"c"}
+     * }
+     * 
+ * and this JSONPointer string: + *
+     * "/a/b"
+     * 
+ * Then this method will return the String "c". + * A JSONPointerException may be thrown from code called by this method. + * + * @param jsonPointer string that can be used to create a JSONPointer + * @return the item matched by the JSONPointer, otherwise null + */ + public Object query(String jsonPointer) { + return query(new JSONPointer(jsonPointer)); + } + /** + * Uses a user initialized JSONPointer and tries to + * match it to an item within this JSONObject. For example, given a + * JSONObject initialized with this document: + *
+     * {
+     *     "a":{"b":"c"}
+     * }
+     * 
+ * and this JSONPointer: + *
+     * "/a/b"
+     * 
+ * Then this method will return the String "c". + * A JSONPointerException may be thrown from code called by this method. + * + * @param jsonPointer string that can be used to create a JSONPointer + * @return the item matched by the JSONPointer, otherwise null + */ + public Object query(JSONPointer jsonPointer) { + return jsonPointer.queryFrom(this); + } + + /** + * Queries and returns a value from this object using {@code jsonPointer}, or + * returns null if the query fails due to a missing key. + * + * @param jsonPointer the string representation of the JSON pointer + * @return the queried value or {@code null} + * @throws IllegalArgumentException if {@code jsonPointer} has invalid syntax + */ + public Object optQuery(String jsonPointer) { + return optQuery(new JSONPointer(jsonPointer)); + } + + /** + * Queries and returns a value from this object using {@code jsonPointer}, or + * returns null if the query fails due to a missing key. + * + * @param jsonPointer The JSON pointer + * @return the queried value or {@code null} + * @throws IllegalArgumentException if {@code jsonPointer} has invalid syntax + */ + public Object optQuery(JSONPointer jsonPointer) { + try { + return jsonPointer.queryFrom(this); + } catch (JSONPointerException e) { + return null; + } + } + + /** + * Produce a string in double quotes with backslash sequences in all the + * right places. A backslash will be inserted within </, producing + * <\/, allowing JSON text to be delivered in HTML. In JSON text, a + * string cannot contain a control character or an unescaped quote or + * backslash. + * + * @param string + * A String + * @return A String correctly formatted for insertion in a JSON text. + */ + public static String quote(String string) { + StringWriter sw = new StringWriter(); + synchronized (sw.getBuffer()) { + try { + return quote(string, sw).toString(); + } catch (IOException ignored) { + // will never happen - we are writing to a string writer + return ""; + } + } + } + + public static Writer quote(String string, Writer w) throws IOException { + if (string == null || string.isEmpty()) { + w.write("\"\""); + return w; + } + + char b; + char c = 0; + String hhhh; + int i; + int len = string.length(); + + w.write('"'); + for (i = 0; i < len; i += 1) { + b = c; + c = string.charAt(i); + switch (c) { + case '\\': + case '"': + w.write('\\'); + w.write(c); + break; + case '/': + if (b == '<') { + w.write('\\'); + } + w.write(c); + break; + case '\b': + w.write("\\b"); + break; + case '\t': + w.write("\\t"); + break; + case '\n': + w.write("\\n"); + break; + case '\f': + w.write("\\f"); + break; + case '\r': + w.write("\\r"); + break; + default: + if (c < ' ' || (c >= '\u0080' && c < '\u00a0') + || (c >= '\u2000' && c < '\u2100')) { + w.write("\\u"); + hhhh = Integer.toHexString(c); + w.write("0000", 0, 4 - hhhh.length()); + w.write(hhhh); + } else { + w.write(c); + } + } + } + w.write('"'); + return w; + } + + /** + * Remove a name and its value, if present. + * + * @param key + * The name to be removed. + * @return The value that was associated with the name, or null if there was + * no value. + */ + public Object remove(String key) { + return this.map.remove(key); + } + + /** + * Determine if two JSONObjects are similar. + * They must contain the same set of names which must be associated with + * similar values. + * + * @param other The other JSONObject + * @return true if they are equal + */ + public boolean similar(Object other) { + try { + if (!(other instanceof JSONObject)) { + return false; + } + if (!this.keySet().equals(((JSONObject)other).keySet())) { + return false; + } + for (final Entry entry : this.entrySet()) { + String name = entry.getKey(); + Object valueThis = entry.getValue(); + Object valueOther = ((JSONObject)other).get(name); + if(valueThis == valueOther) { + continue; + } + if(valueThis == null) { + return false; + } + if (valueThis instanceof JSONObject) { + if (!((JSONObject)valueThis).similar(valueOther)) { + return false; + } + } else if (valueThis instanceof JSONArray) { + if (!((JSONArray)valueThis).similar(valueOther)) { + return false; + } + } else if (!valueThis.equals(valueOther)) { + return false; + } + } + return true; + } catch (Throwable exception) { + return false; + } + } + + /** + * Tests if the value should be tried as a decimal. It makes no test if there are actual digits. + * + * @param val value to test + * @return true if the string is "-0" or if it contains '.', 'e', or 'E', false otherwise. + */ + protected static boolean isDecimalNotation(final String val) { + return val.indexOf('.') > -1 || val.indexOf('e') > -1 + || val.indexOf('E') > -1 || "-0".equals(val); + } + + /** + * Converts a string to a number using the narrowest possible type. Possible + * returns for this function are BigDecimal, Double, BigInteger, Long, and Integer. + * When a Double is returned, it should always be a valid Double and not NaN or +-infinity. + * + * @param val value to convert + * @return Number representation of the value. + * @throws NumberFormatException thrown if the value is not a valid number. A public + * caller should catch this and wrap it in a {@link JSONException} if applicable. + */ + protected static Number stringToNumber(final String val) throws NumberFormatException { + char initial = val.charAt(0); + if ((initial >= '0' && initial <= '9') || initial == '-') { + // decimal representation + if (isDecimalNotation(val)) { + // quick dirty way to see if we need a BigDecimal instead of a Double + // this only handles some cases of overflow or underflow + if (val.length()>14) { + return new BigDecimal(val); + } + final Double d = Double.valueOf(val); + if (d.isInfinite() || d.isNaN()) { + // if we can't parse it as a double, go up to BigDecimal + // this is probably due to underflow like 4.32e-678 + // or overflow like 4.65e5324. The size of the string is small + // but can't be held in a Double. + return new BigDecimal(val); + } + return d; + } + // integer representation. + // This will narrow any values to the smallest reasonable Object representation + // (Integer, Long, or BigInteger) + + // string version + // The compare string length method reduces GC, + // but leads to smaller integers being placed in larger wrappers even though not + // needed. i.e. 1,000,000,000 -> Long even though it's an Integer + // 1,000,000,000,000,000,000 -> BigInteger even though it's a Long + //if(val.length()<=9){ + // return Integer.valueOf(val); + //} + //if(val.length()<=18){ + // return Long.valueOf(val); + //} + //return new BigInteger(val); + + // BigInteger version: We use a similar bitLength compare as + // BigInteger#intValueExact uses. Increases GC, but objects hold + // only what they need. i.e. Less runtime overhead if the value is + // long lived. Which is the better tradeoff? This is closer to what's + // in stringToValue. + BigInteger bi = new BigInteger(val); + if(bi.bitLength()<=31){ + return Integer.valueOf(bi.intValue()); + } + if(bi.bitLength()<=63){ + return Long.valueOf(bi.longValue()); + } + return bi; + } + throw new NumberFormatException("val ["+val+"] is not a valid number."); + } + + /** + * Try to convert a string into a number, boolean, or null. If the string + * can't be converted, return the string. + * + * @param string + * A String. can not be null. + * @return A simple JSON value. + * @throws NullPointerException + * Thrown if the string is null. + */ + // Changes to this method must be copied to the corresponding method in + // the XML class to keep full support for Android + public static Object stringToValue(String string) { + if ("".equals(string)) { + return string; + } + + // check JSON key words true/false/null + if ("true".equalsIgnoreCase(string)) { + return Boolean.TRUE; + } + if ("false".equalsIgnoreCase(string)) { + return Boolean.FALSE; + } + if ("null".equalsIgnoreCase(string)) { + return JSONObject.NULL; + } + + /* + * If it might be a number, try converting it. If a number cannot be + * produced, then the value will just be a string. + */ + + char initial = string.charAt(0); + if ((initial >= '0' && initial <= '9') || initial == '-') { + try { + // if we want full Big Number support the contents of this + // `try` block can be replaced with: + // return stringToNumber(string); + if (isDecimalNotation(string)) { + Double d = Double.valueOf(string); + if (!d.isInfinite() && !d.isNaN()) { + return d; + } + } else { + Long myLong = Long.valueOf(string); + if (string.equals(myLong.toString())) { + if (myLong.longValue() == myLong.intValue()) { + return Integer.valueOf(myLong.intValue()); + } + return myLong; + } + } + } catch (Exception ignore) { + } + } + return string; + } + + /** + * Throw an exception if the object is a NaN or infinite number. + * + * @param o + * The object to test. + * @throws JSONException + * If o is a non-finite number. + */ + public static void testValidity(Object o) throws JSONException { + if (o != null) { + if (o instanceof Double) { + if (((Double) o).isInfinite() || ((Double) o).isNaN()) { + throw new JSONException( + "JSON does not allow non-finite numbers."); + } + } else if (o instanceof Float) { + if (((Float) o).isInfinite() || ((Float) o).isNaN()) { + throw new JSONException( + "JSON does not allow non-finite numbers."); + } + } + } + } + + /** + * Produce a JSONArray containing the values of the members of this + * JSONObject. + * + * @param names + * A JSONArray containing a list of key strings. This determines + * the sequence of the values in the result. + * @return A JSONArray of values. + * @throws JSONException + * If any of the values are non-finite numbers. + */ + public JSONArray toJSONArray(JSONArray names) throws JSONException { + if (names == null || names.isEmpty()) { + return null; + } + JSONArray ja = new JSONArray(); + for (int i = 0; i < names.length(); i += 1) { + ja.put(this.opt(names.getString(i))); + } + return ja; + } + + /** + * Make a JSON text of this JSONObject. For compactness, no whitespace is + * added. If this would not result in a syntactically correct JSON text, + * then null will be returned instead. + *

+ * Warning: This method assumes that the data structure is acyclical. + * + * + * @return a printable, displayable, portable, transmittable representation + * of the object, beginning with { (left + * brace) and ending with } (right + * brace). + */ + @Override + public String toString() { + try { + return this.toString(0); + } catch (Exception e) { + return null; + } + } + + /** + * Make a pretty-printed JSON text of this JSONObject. + * + *

If indentFactor > 0 and the {@link JSONObject} + * has only one key, then the object will be output on a single line: + *

{@code {"key": 1}}
+ * + *

If an object has 2 or more keys, then it will be output across + * multiple lines:

{
+     *  "key1": 1,
+     *  "key2": "value 2",
+     *  "key3": 3
+     * }
+ *

+ * Warning: This method assumes that the data structure is acyclical. + * + * + * @param indentFactor + * The number of spaces to add to each level of indentation. + * @return a printable, displayable, portable, transmittable representation + * of the object, beginning with { (left + * brace) and ending with } (right + * brace). + * @throws JSONException + * If the object contains an invalid number. + */ + public String toString(int indentFactor) throws JSONException { + StringWriter w = new StringWriter(); + synchronized (w.getBuffer()) { + return this.write(w, indentFactor, 0).toString(); + } + } + + /** + * Make a JSON text of an Object value. If the object has an + * value.toJSONString() method, then that method will be used to produce the + * JSON text. The method is required to produce a strictly conforming text. + * If the object does not contain a toJSONString method (which is the most + * common case), then a text will be produced by other means. If the value + * is an array or Collection, then a JSONArray will be made from it and its + * toJSONString method will be called. If the value is a MAP, then a + * JSONObject will be made from it and its toJSONString method will be + * called. Otherwise, the value's toString method will be called, and the + * result will be quoted. + * + *

+ * Warning: This method assumes that the data structure is acyclical. + * + * @param value + * The value to be serialized. + * @return a printable, displayable, transmittable representation of the + * object, beginning with { (left + * brace) and ending with } (right + * brace). + * @throws JSONException + * If the value is or contains an invalid number. + */ + public static String valueToString(Object value) throws JSONException { + // moves the implementation to JSONWriter as: + // 1. It makes more sense to be part of the writer class + // 2. For Android support this method is not available. By implementing it in the Writer + // Android users can use the writer with the built in Android JSONObject implementation. + return JSONWriter.valueToString(value); + } + + /** + * Wrap an object, if necessary. If the object is null, return the NULL + * object. If it is an array or collection, wrap it in a JSONArray. If it is + * a map, wrap it in a JSONObject. If it is a standard property (Double, + * String, et al) then it is already wrapped. Otherwise, if it comes from + * one of the java packages, turn it into a string. And if it doesn't, try + * to wrap it in a JSONObject. If the wrapping fails, then null is returned. + * + * @param object + * The object to wrap + * @return The wrapped value + */ + public static Object wrap(Object object) { + try { + if (object == null) { + return NULL; + } + if (object instanceof JSONObject || object instanceof JSONArray + || NULL.equals(object) || object instanceof JSONString + || object instanceof Byte || object instanceof Character + || object instanceof Short || object instanceof Integer + || object instanceof Long || object instanceof Boolean + || object instanceof Float || object instanceof Double + || object instanceof String || object instanceof BigInteger + || object instanceof BigDecimal || object instanceof Enum) { + return object; + } + + if (object instanceof Collection) { + Collection coll = (Collection) object; + return new JSONArray(coll); + } + if (object.getClass().isArray()) { + return new JSONArray(object); + } + if (object instanceof Map) { + Map map = (Map) object; + return new JSONObject(map); + } + Package objectPackage = object.getClass().getPackage(); + String objectPackageName = objectPackage != null ? objectPackage + .getName() : ""; + if (objectPackageName.startsWith("java.") + || objectPackageName.startsWith("javax.") + || object.getClass().getClassLoader() == null) { + return object.toString(); + } + return new JSONObject(object); + } catch (Exception exception) { + return null; + } + } + + /** + * Write the contents of the JSONObject as JSON text to a writer. For + * compactness, no whitespace is added. + *

+ * Warning: This method assumes that the data structure is acyclical. + * + * + * @return The writer. + * @throws JSONException + */ + public Writer write(Writer writer) throws JSONException { + return this.write(writer, 0, 0); + } + + static final Writer writeValue(Writer writer, Object value, + int indentFactor, int indent) throws JSONException, IOException { + if (value == null || value.equals(null)) { + writer.write("null"); + } else if (value instanceof JSONString) { + Object o; + try { + o = ((JSONString) value).toJSONString(); + } catch (Exception e) { + throw new JSONException(e); + } + writer.write(o != null ? o.toString() : quote(value.toString())); + } else if (value instanceof Number) { + // not all Numbers may match actual JSON Numbers. i.e. fractions or Imaginary + final String numberAsString = numberToString((Number) value); + if(NUMBER_PATTERN.matcher(numberAsString).matches()) { + writer.write(numberAsString); + } else { + // The Number value is not a valid JSON number. + // Instead we will quote it as a string + quote(numberAsString, writer); + } + } else if (value instanceof Boolean) { + writer.write(value.toString()); + } else if (value instanceof Enum) { + writer.write(quote(((Enum)value).name())); + } else if (value instanceof JSONObject) { + ((JSONObject) value).write(writer, indentFactor, indent); + } else if (value instanceof JSONArray) { + ((JSONArray) value).write(writer, indentFactor, indent); + } else if (value instanceof Map) { + Map map = (Map) value; + new JSONObject(map).write(writer, indentFactor, indent); + } else if (value instanceof Collection) { + Collection coll = (Collection) value; + new JSONArray(coll).write(writer, indentFactor, indent); + } else if (value.getClass().isArray()) { + new JSONArray(value).write(writer, indentFactor, indent); + } else { + quote(value.toString(), writer); + } + return writer; + } + + static final void indent(Writer writer, int indent) throws IOException { + for (int i = 0; i < indent; i += 1) { + writer.write(' '); + } + } + + /** + * Write the contents of the JSONObject as JSON text to a writer. + * + *

If indentFactor > 0 and the {@link JSONObject} + * has only one key, then the object will be output on a single line: + *

{@code {"key": 1}}
+ * + *

If an object has 2 or more keys, then it will be output across + * multiple lines:

{
+     *  "key1": 1,
+     *  "key2": "value 2",
+     *  "key3": 3
+     * }
+ *

+ * Warning: This method assumes that the data structure is acyclical. + * + * + * @param writer + * Writes the serialized JSON + * @param indentFactor + * The number of spaces to add to each level of indentation. + * @param indent + * The indentation of the top level. + * @return The writer. + * @throws JSONException + */ + public Writer write(Writer writer, int indentFactor, int indent) + throws JSONException { + try { + boolean needsComma = false; + final int length = this.length(); + writer.write('{'); + + if (length == 1) { + final Entry entry = this.entrySet().iterator().next(); + final String key = entry.getKey(); + writer.write(quote(key)); + writer.write(':'); + if (indentFactor > 0) { + writer.write(' '); + } + try{ + writeValue(writer, entry.getValue(), indentFactor, indent); + } catch (Exception e) { + throw new JSONException("Unable to write JSONObject value for key: " + key, e); + } + } else if (length != 0) { + final int newIndent = indent + indentFactor; + for (final Entry entry : this.entrySet()) { + if (needsComma) { + writer.write(','); + } + if (indentFactor > 0) { + writer.write('\n'); + } + indent(writer, newIndent); + final String key = entry.getKey(); + writer.write(quote(key)); + writer.write(':'); + if (indentFactor > 0) { + writer.write(' '); + } + try { + writeValue(writer, entry.getValue(), indentFactor, newIndent); + } catch (Exception e) { + throw new JSONException("Unable to write JSONObject value for key: " + key, e); + } + needsComma = true; + } + if (indentFactor > 0) { + writer.write('\n'); + } + indent(writer, indent); + } + writer.write('}'); + return writer; + } catch (IOException exception) { + throw new JSONException(exception); + } + } + + /** + * Returns a java.util.Map containing all of the entries in this object. + * If an entry in the object is a JSONArray or JSONObject it will also + * be converted. + *

+ * Warning: This method assumes that the data structure is acyclical. + * + * @return a java.util.Map containing the entries of this object + */ + public Map toMap() { + Map results = new HashMap(); + for (Entry entry : this.entrySet()) { + Object value; + if (entry.getValue() == null || NULL.equals(entry.getValue())) { + value = null; + } else if (entry.getValue() instanceof JSONObject) { + value = ((JSONObject) entry.getValue()).toMap(); + } else if (entry.getValue() instanceof JSONArray) { + value = ((JSONArray) entry.getValue()).toList(); + } else { + value = entry.getValue(); + } + results.put(entry.getKey(), value); + } + return results; + } + + /** + * Create a new JSONException in a common format for incorrect conversions. + * @param key name of the key + * @param valueType the type of value being coerced to + * @param cause optional cause of the coercion failure + * @return JSONException that can be thrown. + */ + private static JSONException wrongValueFormatException( + String key, + String valueType, + Throwable cause) { + return new JSONException( + "JSONObject[" + quote(key) + "] is not a " + valueType + "." + , cause); + } + + /** + * Create a new JSONException in a common format for incorrect conversions. + * @param key name of the key + * @param valueType the type of value being coerced to + * @param cause optional cause of the coercion failure + * @return JSONException that can be thrown. + */ + private static JSONException wrongValueFormatException( + String key, + String valueType, + Object value, + Throwable cause) { + return new JSONException( + "JSONObject[" + quote(key) + "] is not a " + valueType + " (" + value + ")." + , cause); + } +} diff --git a/src/main/java/org/json/JSONPointer.java b/src/main/java/org/json/JSONPointer.java new file mode 100644 index 0000000..c89559e --- /dev/null +++ b/src/main/java/org/json/JSONPointer.java @@ -0,0 +1,293 @@ +package org.json; + +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.net.URLEncoder; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static java.lang.String.format; + +/* +Copyright (c) 2002 JSON.org + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +The Software shall be used for Good, not Evil. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +/** + * A JSON Pointer is a simple query language defined for JSON documents by + * RFC 6901. + * + * In a nutshell, JSONPointer allows the user to navigate into a JSON document + * using strings, and retrieve targeted objects, like a simple form of XPATH. + * Path segments are separated by the '/' char, which signifies the root of + * the document when it appears as the first char of the string. Array + * elements are navigated using ordinals, counting from 0. JSONPointer strings + * may be extended to any arbitrary number of segments. If the navigation + * is successful, the matched item is returned. A matched item may be a + * JSONObject, a JSONArray, or a JSON value. If the JSONPointer string building + * fails, an appropriate exception is thrown. If the navigation fails to find + * a match, a JSONPointerException is thrown. + * + * @author JSON.org + * @version 2016-05-14 + */ +public class JSONPointer { + + // used for URL encoding and decoding + private static final String ENCODING = "utf-8"; + + /** + * This class allows the user to build a JSONPointer in steps, using + * exactly one segment in each step. + */ + public static class Builder { + + // Segments for the eventual JSONPointer string + private final List refTokens = new ArrayList(); + + /** + * Creates a {@code JSONPointer} instance using the tokens previously set using the + * {@link #append(String)} method calls. + */ + public JSONPointer build() { + return new JSONPointer(this.refTokens); + } + + /** + * Adds an arbitrary token to the list of reference tokens. It can be any non-null value. + * + * Unlike in the case of JSON string or URI fragment representation of JSON pointers, the + * argument of this method MUST NOT be escaped. If you want to query the property called + * {@code "a~b"} then you should simply pass the {@code "a~b"} string as-is, there is no + * need to escape it as {@code "a~0b"}. + * + * @param token the new token to be appended to the list + * @return {@code this} + * @throws NullPointerException if {@code token} is null + */ + public Builder append(String token) { + if (token == null) { + throw new NullPointerException("token cannot be null"); + } + this.refTokens.add(token); + return this; + } + + /** + * Adds an integer to the reference token list. Although not necessarily, mostly this token will + * denote an array index. + * + * @param arrayIndex the array index to be added to the token list + * @return {@code this} + */ + public Builder append(int arrayIndex) { + this.refTokens.add(String.valueOf(arrayIndex)); + return this; + } + } + + /** + * Static factory method for {@link Builder}. Example usage: + * + *


+     * JSONPointer pointer = JSONPointer.builder()
+     *       .append("obj")
+     *       .append("other~key").append("another/key")
+     *       .append("\"")
+     *       .append(0)
+     *       .build();
+     * 
+ * + * @return a builder instance which can be used to construct a {@code JSONPointer} instance by chained + * {@link Builder#append(String)} calls. + */ + public static Builder builder() { + return new Builder(); + } + + // Segments for the JSONPointer string + private final List refTokens; + + /** + * Pre-parses and initializes a new {@code JSONPointer} instance. If you want to + * evaluate the same JSON Pointer on different JSON documents then it is recommended + * to keep the {@code JSONPointer} instances due to performance considerations. + * + * @param pointer the JSON String or URI Fragment representation of the JSON pointer. + * @throws IllegalArgumentException if {@code pointer} is not a valid JSON pointer + */ + public JSONPointer(final String pointer) { + if (pointer == null) { + throw new NullPointerException("pointer cannot be null"); + } + if (pointer.isEmpty() || pointer.equals("#")) { + this.refTokens = Collections.emptyList(); + return; + } + String refs; + if (pointer.startsWith("#/")) { + refs = pointer.substring(2); + try { + refs = URLDecoder.decode(refs, ENCODING); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + } else if (pointer.startsWith("/")) { + refs = pointer.substring(1); + } else { + throw new IllegalArgumentException("a JSON pointer should start with '/' or '#/'"); + } + this.refTokens = new ArrayList(); + int slashIdx = -1; + int prevSlashIdx = 0; + do { + prevSlashIdx = slashIdx + 1; + slashIdx = refs.indexOf('/', prevSlashIdx); + if(prevSlashIdx == slashIdx || prevSlashIdx == refs.length()) { + // found 2 slashes in a row ( obj//next ) + // or single slash at the end of a string ( obj/test/ ) + this.refTokens.add(""); + } else if (slashIdx >= 0) { + final String token = refs.substring(prevSlashIdx, slashIdx); + this.refTokens.add(unescape(token)); + } else { + // last item after separator, or no separator at all. + final String token = refs.substring(prevSlashIdx); + this.refTokens.add(unescape(token)); + } + } while (slashIdx >= 0); + // using split does not take into account consecutive separators or "ending nulls" + //for (String token : refs.split("/")) { + // this.refTokens.add(unescape(token)); + //} + } + + public JSONPointer(List refTokens) { + this.refTokens = new ArrayList(refTokens); + } + + private static String unescape(String token) { + return token.replace("~1", "/").replace("~0", "~") + .replace("\\\"", "\"") + .replace("\\\\", "\\"); + } + + /** + * Evaluates this JSON Pointer on the given {@code document}. The {@code document} + * is usually a {@link JSONObject} or a {@link JSONArray} instance, but the empty + * JSON Pointer ({@code ""}) can be evaluated on any JSON values and in such case the + * returned value will be {@code document} itself. + * + * @param document the JSON document which should be the subject of querying. + * @return the result of the evaluation + * @throws JSONPointerException if an error occurs during evaluation + */ + public Object queryFrom(Object document) throws JSONPointerException { + if (this.refTokens.isEmpty()) { + return document; + } + Object current = document; + for (String token : this.refTokens) { + if (current instanceof JSONObject) { + current = ((JSONObject) current).opt(unescape(token)); + } else if (current instanceof JSONArray) { + current = readByIndexToken(current, token); + } else { + throw new JSONPointerException(format( + "value [%s] is not an array or object therefore its key %s cannot be resolved", current, + token)); + } + } + return current; + } + + /** + * Matches a JSONArray element by ordinal position + * @param current the JSONArray to be evaluated + * @param indexToken the array index in string form + * @return the matched object. If no matching item is found a + * @throws JSONPointerException is thrown if the index is out of bounds + */ + private static Object readByIndexToken(Object current, String indexToken) throws JSONPointerException { + try { + int index = Integer.parseInt(indexToken); + JSONArray currentArr = (JSONArray) current; + if (index >= currentArr.length()) { + throw new JSONPointerException(format("index %s is out of bounds - the array has %d elements", indexToken, + Integer.valueOf(currentArr.length()))); + } + try { + return currentArr.get(index); + } catch (JSONException e) { + throw new JSONPointerException("Error reading value at index position " + index, e); + } + } catch (NumberFormatException e) { + throw new JSONPointerException(format("%s is not an array index", indexToken), e); + } + } + + /** + * Returns a string representing the JSONPointer path value using string + * representation + */ + @Override + public String toString() { + StringBuilder rval = new StringBuilder(""); + for (String token: this.refTokens) { + rval.append('/').append(escape(token)); + } + return rval.toString(); + } + + /** + * Escapes path segment values to an unambiguous form. + * The escape char to be inserted is '~'. The chars to be escaped + * are ~, which maps to ~0, and /, which maps to ~1. Backslashes + * and double quote chars are also escaped. + * @param token the JSONPointer segment value to be escaped + * @return the escaped value for the token + */ + private static String escape(String token) { + return token.replace("~", "~0") + .replace("/", "~1") + .replace("\\", "\\\\") + .replace("\"", "\\\""); + } + + /** + * Returns a string representing the JSONPointer path value using URI + * fragment identifier representation + */ + public String toURIFragment() { + try { + StringBuilder rval = new StringBuilder("#"); + for (String token : this.refTokens) { + rval.append('/').append(URLEncoder.encode(token, ENCODING)); + } + return rval.toString(); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + } + +} diff --git a/src/main/java/org/json/JSONPointerException.java b/src/main/java/org/json/JSONPointerException.java new file mode 100644 index 0000000..0ce1aeb --- /dev/null +++ b/src/main/java/org/json/JSONPointerException.java @@ -0,0 +1,45 @@ +package org.json; + +/* +Copyright (c) 2002 JSON.org + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +The Software shall be used for Good, not Evil. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +/** + * The JSONPointerException is thrown by {@link JSONPointer} if an error occurs + * during evaluating a pointer. + * + * @author JSON.org + * @version 2016-05-13 + */ +public class JSONPointerException extends JSONException { + private static final long serialVersionUID = 8872944667561856751L; + + public JSONPointerException(String message) { + super(message); + } + + public JSONPointerException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/src/main/java/org/json/JSONPropertyIgnore.java b/src/main/java/org/json/JSONPropertyIgnore.java new file mode 100644 index 0000000..a58bbe3 --- /dev/null +++ b/src/main/java/org/json/JSONPropertyIgnore.java @@ -0,0 +1,43 @@ +package org.json; + +/* +Copyright (c) 2018 JSON.org + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +The Software shall be used for Good, not Evil. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Documented +@Retention(RUNTIME) +@Target({METHOD}) +/** + * Use this annotation on a getter method to override the Bean name + * parser for Bean -> JSONObject mapping. If this annotation is + * present at any level in the class hierarchy, then the method will + * not be serialized from the bean into the JSONObject. + */ +public @interface JSONPropertyIgnore { } diff --git a/src/main/java/org/json/JSONPropertyName.java b/src/main/java/org/json/JSONPropertyName.java new file mode 100644 index 0000000..ae5eced --- /dev/null +++ b/src/main/java/org/json/JSONPropertyName.java @@ -0,0 +1,47 @@ +package org.json; + +/* +Copyright (c) 2018 JSON.org + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +The Software shall be used for Good, not Evil. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Documented +@Retention(RUNTIME) +@Target({METHOD}) +/** + * Use this annotation on a getter method to override the Bean name + * parser for Bean -> JSONObject mapping. A value set to empty string "" + * will have the Bean parser fall back to the default field name processing. + */ +public @interface JSONPropertyName { + /** + * @return The name of the property as to be used in the JSON Object. + */ + String value(); +} diff --git a/src/main/java/org/json/JSONString.java b/src/main/java/org/json/JSONString.java new file mode 100644 index 0000000..1f2d77d --- /dev/null +++ b/src/main/java/org/json/JSONString.java @@ -0,0 +1,18 @@ +package org.json; +/** + * The JSONString interface allows a toJSONString() + * method so that a class can change the behavior of + * JSONObject.toString(), JSONArray.toString(), + * and JSONWriter.value(Object). The + * toJSONString method will be used instead of the default behavior + * of using the Object's toString() method and quoting the result. + */ +public interface JSONString { + /** + * The toJSONString method allows a class to produce its own JSON + * serialization. + * + * @return A strictly syntactically correct JSON text. + */ + public String toJSONString(); +} diff --git a/src/main/java/org/json/JSONStringer.java b/src/main/java/org/json/JSONStringer.java new file mode 100644 index 0000000..bb9e7a4 --- /dev/null +++ b/src/main/java/org/json/JSONStringer.java @@ -0,0 +1,79 @@ +package org.json; + +/* +Copyright (c) 2006 JSON.org + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +The Software shall be used for Good, not Evil. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +import java.io.StringWriter; + +/** + * JSONStringer provides a quick and convenient way of producing JSON text. + * The texts produced strictly conform to JSON syntax rules. No whitespace is + * added, so the results are ready for transmission or storage. Each instance of + * JSONStringer can produce one JSON text. + *

+ * A JSONStringer instance provides a value method for appending + * values to the + * text, and a key + * method for adding keys before values in objects. There are array + * and endArray methods that make and bound array values, and + * object and endObject methods which make and bound + * object values. All of these methods return the JSONWriter instance, + * permitting cascade style. For example,

+ * myString = new JSONStringer()
+ *     .object()
+ *         .key("JSON")
+ *         .value("Hello, World!")
+ *     .endObject()
+ *     .toString();
which produces the string
+ * {"JSON":"Hello, World!"}
+ *

+ * The first method called must be array or object. + * There are no methods for adding commas or colons. JSONStringer adds them for + * you. Objects and arrays can be nested up to 20 levels deep. + *

+ * This can sometimes be easier than using a JSONObject to build a string. + * @author JSON.org + * @version 2015-12-09 + */ +public class JSONStringer extends JSONWriter { + /** + * Make a fresh JSONStringer. It can be used to build one JSON text. + */ + public JSONStringer() { + super(new StringWriter()); + } + + /** + * Return the JSON text. This method is used to obtain the product of the + * JSONStringer instance. It will return null if there was a + * problem in the construction of the JSON text (such as the calls to + * array were not properly balanced with calls to + * endArray). + * @return The JSON text. + */ + @Override + public String toString() { + return this.mode == 'd' ? this.writer.toString() : null; + } +} diff --git a/src/main/java/org/json/JSONTokener.java b/src/main/java/org/json/JSONTokener.java new file mode 100644 index 0000000..2077c31 --- /dev/null +++ b/src/main/java/org/json/JSONTokener.java @@ -0,0 +1,526 @@ +package org.json; + +import java.io.*; + +/* +Copyright (c) 2002 JSON.org + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +The Software shall be used for Good, not Evil. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + */ + +/** + * A JSONTokener takes a source string and extracts characters and tokens from + * it. It is used by the JSONObject and JSONArray constructors to parse + * JSON source strings. + * @author JSON.org + * @version 2014-05-03 + */ +public class JSONTokener { + /** current read character position on the current line. */ + private long character; + /** flag to indicate if the end of the input has been found. */ + private boolean eof; + /** current read index of the input. */ + private long index; + /** current line of the input. */ + private long line; + /** previous character read from the input. */ + private char previous; + /** Reader for the input. */ + private final Reader reader; + /** flag to indicate that a previous character was requested. */ + private boolean usePrevious; + /** the number of characters read in the previous line. */ + private long characterPreviousLine; + + + /** + * Construct a JSONTokener from a Reader. The caller must close the Reader. + * + * @param reader A reader. + */ + public JSONTokener(Reader reader) { + this.reader = reader.markSupported() + ? reader + : new BufferedReader(reader); + this.eof = false; + this.usePrevious = false; + this.previous = 0; + this.index = 0; + this.character = 1; + this.characterPreviousLine = 0; + this.line = 1; + } + + + /** + * Construct a JSONTokener from an InputStream. The caller must close the input stream. + * @param inputStream The source. + */ + public JSONTokener(InputStream inputStream) { + this(new InputStreamReader(inputStream)); + } + + + /** + * Construct a JSONTokener from a string. + * + * @param s A source string. + */ + public JSONTokener(String s) { + this(new StringReader(s)); + } + + + /** + * Back up one character. This provides a sort of lookahead capability, + * so that you can test for a digit or letter before attempting to parse + * the next number or identifier. + * @throws JSONException Thrown if trying to step back more than 1 step + * or if already at the start of the string + */ + public void back() throws JSONException { + if (this.usePrevious || this.index <= 0) { + throw new JSONException("Stepping back two steps is not supported"); + } + this.decrementIndexes(); + this.usePrevious = true; + this.eof = false; + } + + /** + * Decrements the indexes for the {@link #back()} method based on the previous character read. + */ + private void decrementIndexes() { + this.index--; + if(this.previous=='\r' || this.previous == '\n') { + this.line--; + this.character=this.characterPreviousLine ; + } else if(this.character > 0){ + this.character--; + } + } + + /** + * Get the hex value of a character (base16). + * @param c A character between '0' and '9' or between 'A' and 'F' or + * between 'a' and 'f'. + * @return An int between 0 and 15, or -1 if c was not a hex digit. + */ + public static int dehexchar(char c) { + if (c >= '0' && c <= '9') { + return c - '0'; + } + if (c >= 'A' && c <= 'F') { + return c - ('A' - 10); + } + if (c >= 'a' && c <= 'f') { + return c - ('a' - 10); + } + return -1; + } + + /** + * Checks if the end of the input has been reached. + * + * @return true if at the end of the file and we didn't step back + */ + public boolean end() { + return this.eof && !this.usePrevious; + } + + + /** + * Determine if the source string still contains characters that next() + * can consume. + * @return true if not yet at the end of the source. + * @throws JSONException thrown if there is an error stepping forward + * or backward while checking for more data. + */ + public boolean more() throws JSONException { + if(this.usePrevious) { + return true; + } + try { + this.reader.mark(1); + } catch (IOException e) { + throw new JSONException("Unable to preserve stream position", e); + } + try { + // -1 is EOF, but next() can not consume the null character '\0' + if(this.reader.read() <= 0) { + this.eof = true; + return false; + } + this.reader.reset(); + } catch (IOException e) { + throw new JSONException("Unable to read the next character from the stream", e); + } + return true; + } + + + /** + * Get the next character in the source string. + * + * @return The next character, or 0 if past the end of the source string. + * @throws JSONException Thrown if there is an error reading the source string. + */ + public char next() throws JSONException { + int c; + if (this.usePrevious) { + this.usePrevious = false; + c = this.previous; + } else { + try { + c = this.reader.read(); + } catch (IOException exception) { + throw new JSONException(exception); + } + } + if (c <= 0) { // End of stream + this.eof = true; + return 0; + } + this.incrementIndexes(c); + this.previous = (char) c; + return this.previous; + } + + /** + * Increments the internal indexes according to the previous character + * read and the character passed as the current character. + * @param c the current character read. + */ + private void incrementIndexes(int c) { + if(c > 0) { + this.index++; + if(c=='\r') { + this.line++; + this.characterPreviousLine = this.character; + this.character=0; + }else if (c=='\n') { + if(this.previous != '\r') { + this.line++; + this.characterPreviousLine = this.character; + } + this.character=0; + } else { + this.character++; + } + } + } + + /** + * Consume the next character, and check that it matches a specified + * character. + * @param c The character to match. + * @return The character. + * @throws JSONException if the character does not match. + */ + public char next(char c) throws JSONException { + char n = this.next(); + if (n != c) { + if(n > 0) { + throw this.syntaxError("Expected '" + c + "' and instead saw '" + + n + "'"); + } + throw this.syntaxError("Expected '" + c + "' and instead saw ''"); + } + return n; + } + + + /** + * Get the next n characters. + * + * @param n The number of characters to take. + * @return A string of n characters. + * @throws JSONException + * Substring bounds error if there are not + * n characters remaining in the source string. + */ + public String next(int n) throws JSONException { + if (n == 0) { + return ""; + } + + char[] chars = new char[n]; + int pos = 0; + + while (pos < n) { + chars[pos] = this.next(); + if (this.end()) { + throw this.syntaxError("Substring bounds error"); + } + pos += 1; + } + return new String(chars); + } + + + /** + * Get the next char in the string, skipping whitespace. + * @throws JSONException Thrown if there is an error reading the source string. + * @return A character, or 0 if there are no more characters. + */ + public char nextClean() throws JSONException { + for (;;) { + char c = this.next(); + if (c == 0 || c > ' ') { + return c; + } + } + } + + + /** + * Return the characters up to the next close quote character. + * Backslash processing is done. The formal JSON format does not + * allow strings in single quotes, but an implementation is allowed to + * accept them. + * @param quote The quoting character, either + * " (double quote) or + * ' (single quote). + * @return A String. + * @throws JSONException Unterminated string. + */ + public String nextString(char quote) throws JSONException { + char c; + StringBuilder sb = new StringBuilder(); + for (;;) { + c = this.next(); + switch (c) { + case 0: + case '\n': + case '\r': + throw this.syntaxError("Unterminated string"); + case '\\': + c = this.next(); + switch (c) { + case 'b': + sb.append('\b'); + break; + case 't': + sb.append('\t'); + break; + case 'n': + sb.append('\n'); + break; + case 'f': + sb.append('\f'); + break; + case 'r': + sb.append('\r'); + break; + case 'u': + try { + sb.append((char)Integer.parseInt(this.next(4), 16)); + } catch (NumberFormatException e) { + throw this.syntaxError("Illegal escape.", e); + } + break; + case '"': + case '\'': + case '\\': + case '/': + sb.append(c); + break; + default: + throw this.syntaxError("Illegal escape."); + } + break; + default: + if (c == quote) { + return sb.toString(); + } + sb.append(c); + } + } + } + + + /** + * Get the text up but not including the specified character or the + * end of line, whichever comes first. + * @param delimiter A delimiter character. + * @return A string. + * @throws JSONException Thrown if there is an error while searching + * for the delimiter + */ + public String nextTo(char delimiter) throws JSONException { + StringBuilder sb = new StringBuilder(); + for (;;) { + char c = this.next(); + if (c == delimiter || c == 0 || c == '\n' || c == '\r') { + if (c != 0) { + this.back(); + } + return sb.toString().trim(); + } + sb.append(c); + } + } + + + /** + * Get the text up but not including one of the specified delimiter + * characters or the end of line, whichever comes first. + * @param delimiters A set of delimiter characters. + * @return A string, trimmed. + * @throws JSONException Thrown if there is an error while searching + * for the delimiter + */ + public String nextTo(String delimiters) throws JSONException { + char c; + StringBuilder sb = new StringBuilder(); + for (;;) { + c = this.next(); + if (delimiters.indexOf(c) >= 0 || c == 0 || + c == '\n' || c == '\r') { + if (c != 0) { + this.back(); + } + return sb.toString().trim(); + } + sb.append(c); + } + } + + + /** + * Get the next value. The value can be a Boolean, Double, Integer, + * JSONArray, JSONObject, Long, or String, or the JSONObject.NULL object. + * @throws JSONException If syntax error. + * + * @return An object. + */ + public Object nextValue() throws JSONException { + char c = this.nextClean(); + String string; + + switch (c) { + case '"': + case '\'': + return this.nextString(c); + case '{': + this.back(); + return new JSONObject(this); + case '[': + this.back(); + return new JSONArray(this); + } + + /* + * Handle unquoted text. This could be the values true, false, or + * null, or it can be a number. An implementation (such as this one) + * is allowed to also accept non-standard forms. + * + * Accumulate characters until we reach the end of the text or a + * formatting character. + */ + + StringBuilder sb = new StringBuilder(); + while (c >= ' ' && ",:]}/\\\"[{;=#".indexOf(c) < 0) { + sb.append(c); + c = this.next(); + } + if (!this.eof) { + this.back(); + } + + string = sb.toString().trim(); + if ("".equals(string)) { + throw this.syntaxError("Missing value"); + } + return JSONObject.stringToValue(string); + } + + + /** + * Skip characters until the next character is the requested character. + * If the requested character is not found, no characters are skipped. + * @param to A character to skip to. + * @return The requested character, or zero if the requested character + * is not found. + * @throws JSONException Thrown if there is an error while searching + * for the to character + */ + public char skipTo(char to) throws JSONException { + char c; + try { + long startIndex = this.index; + long startCharacter = this.character; + long startLine = this.line; + this.reader.mark(1000000); + do { + c = this.next(); + if (c == 0) { + // in some readers, reset() may throw an exception if + // the remaining portion of the input is greater than + // the mark size (1,000,000 above). + this.reader.reset(); + this.index = startIndex; + this.character = startCharacter; + this.line = startLine; + return 0; + } + } while (c != to); + this.reader.mark(1); + } catch (IOException exception) { + throw new JSONException(exception); + } + this.back(); + return c; + } + + /** + * Make a JSONException to signal a syntax error. + * + * @param message The error message. + * @return A JSONException object, suitable for throwing + */ + public JSONException syntaxError(String message) { + return new JSONException(message + this.toString()); + } + + /** + * Make a JSONException to signal a syntax error. + * + * @param message The error message. + * @param causedBy The throwable that caused the error. + * @return A JSONException object, suitable for throwing + */ + public JSONException syntaxError(String message, Throwable causedBy) { + return new JSONException(message + this.toString(), causedBy); + } + + /** + * Make a printable string of this JSONTokener. + * + * @return " at {index} [character {character} line {line}]" + */ + @Override + public String toString() { + return " at " + this.index + " [character " + this.character + " line " + + this.line + "]"; + } +} diff --git a/src/main/java/org/json/JSONWriter.java b/src/main/java/org/json/JSONWriter.java new file mode 100644 index 0000000..b61a6f1 --- /dev/null +++ b/src/main/java/org/json/JSONWriter.java @@ -0,0 +1,413 @@ +package org.json; + +import java.io.IOException; +import java.util.Collection; +import java.util.Map; + +/* +Copyright (c) 2006 JSON.org + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +The Software shall be used for Good, not Evil. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +/** + * JSONWriter provides a quick and convenient way of producing JSON text. + * The texts produced strictly conform to JSON syntax rules. No whitespace is + * added, so the results are ready for transmission or storage. Each instance of + * JSONWriter can produce one JSON text. + *

+ * A JSONWriter instance provides a value method for appending + * values to the + * text, and a key + * method for adding keys before values in objects. There are array + * and endArray methods that make and bound array values, and + * object and endObject methods which make and bound + * object values. All of these methods return the JSONWriter instance, + * permitting a cascade style. For example,

+ * new JSONWriter(myWriter)
+ *     .object()
+ *         .key("JSON")
+ *         .value("Hello, World!")
+ *     .endObject();
which writes
+ * {"JSON":"Hello, World!"}
+ *

+ * The first method called must be array or object. + * There are no methods for adding commas or colons. JSONWriter adds them for + * you. Objects and arrays can be nested up to 200 levels deep. + *

+ * This can sometimes be easier than using a JSONObject to build a string. + * @author JSON.org + * @version 2016-08-08 + */ +public class JSONWriter { + private static final int maxdepth = 200; + + /** + * The comma flag determines if a comma should be output before the next + * value. + */ + private boolean comma; + + /** + * The current mode. Values: + * 'a' (array), + * 'd' (done), + * 'i' (initial), + * 'k' (key), + * 'o' (object). + */ + protected char mode; + + /** + * The object/array stack. + */ + private final JSONObject stack[]; + + /** + * The stack top index. A value of 0 indicates that the stack is empty. + */ + private int top; + + /** + * The writer that will receive the output. + */ + protected Appendable writer; + + /** + * Make a fresh JSONWriter. It can be used to build one JSON text. + */ + public JSONWriter(Appendable w) { + this.comma = false; + this.mode = 'i'; + this.stack = new JSONObject[maxdepth]; + this.top = 0; + this.writer = w; + } + + /** + * Append a value. + * @param string A string value. + * @return this + * @throws JSONException If the value is out of sequence. + */ + private JSONWriter append(String string) throws JSONException { + if (string == null) { + throw new JSONException("Null pointer"); + } + if (this.mode == 'o' || this.mode == 'a') { + try { + if (this.comma && this.mode == 'a') { + this.writer.append(','); + } + this.writer.append(string); + } catch (IOException e) { + // Android as of API 25 does not support this exception constructor + // however we won't worry about it. If an exception is happening here + // it will just throw a "Method not found" exception instead. + throw new JSONException(e); + } + if (this.mode == 'o') { + this.mode = 'k'; + } + this.comma = true; + return this; + } + throw new JSONException("Value out of sequence."); + } + + /** + * Begin appending a new array. All values until the balancing + * endArray will be appended to this array. The + * endArray method must be called to mark the array's end. + * @return this + * @throws JSONException If the nesting is too deep, or if the object is + * started in the wrong place (for example as a key or after the end of the + * outermost array or object). + */ + public JSONWriter array() throws JSONException { + if (this.mode == 'i' || this.mode == 'o' || this.mode == 'a') { + this.push(null); + this.append("["); + this.comma = false; + return this; + } + throw new JSONException("Misplaced array."); + } + + /** + * End something. + * @param m Mode + * @param c Closing character + * @return this + * @throws JSONException If unbalanced. + */ + private JSONWriter end(char m, char c) throws JSONException { + if (this.mode != m) { + throw new JSONException(m == 'a' + ? "Misplaced endArray." + : "Misplaced endObject."); + } + this.pop(m); + try { + this.writer.append(c); + } catch (IOException e) { + // Android as of API 25 does not support this exception constructor + // however we won't worry about it. If an exception is happening here + // it will just throw a "Method not found" exception instead. + throw new JSONException(e); + } + this.comma = true; + return this; + } + + /** + * End an array. This method most be called to balance calls to + * array. + * @return this + * @throws JSONException If incorrectly nested. + */ + public JSONWriter endArray() throws JSONException { + return this.end('a', ']'); + } + + /** + * End an object. This method most be called to balance calls to + * object. + * @return this + * @throws JSONException If incorrectly nested. + */ + public JSONWriter endObject() throws JSONException { + return this.end('k', '}'); + } + + /** + * Append a key. The key will be associated with the next value. In an + * object, every value must be preceded by a key. + * @param string A key string. + * @return this + * @throws JSONException If the key is out of place. For example, keys + * do not belong in arrays or if the key is null. + */ + public JSONWriter key(String string) throws JSONException { + if (string == null) { + throw new JSONException("Null key."); + } + if (this.mode == 'k') { + try { + JSONObject topObject = this.stack[this.top - 1]; + // don't use the built in putOnce method to maintain Android support + if(topObject.has(string)) { + throw new JSONException("Duplicate key \"" + string + "\""); + } + topObject.put(string, true); + if (this.comma) { + this.writer.append(','); + } + this.writer.append(JSONObject.quote(string)); + this.writer.append(':'); + this.comma = false; + this.mode = 'o'; + return this; + } catch (IOException e) { + // Android as of API 25 does not support this exception constructor + // however we won't worry about it. If an exception is happening here + // it will just throw a "Method not found" exception instead. + throw new JSONException(e); + } + } + throw new JSONException("Misplaced key."); + } + + + /** + * Begin appending a new object. All keys and values until the balancing + * endObject will be appended to this object. The + * endObject method must be called to mark the object's end. + * @return this + * @throws JSONException If the nesting is too deep, or if the object is + * started in the wrong place (for example as a key or after the end of the + * outermost array or object). + */ + public JSONWriter object() throws JSONException { + if (this.mode == 'i') { + this.mode = 'o'; + } + if (this.mode == 'o' || this.mode == 'a') { + this.append("{"); + this.push(new JSONObject()); + this.comma = false; + return this; + } + throw new JSONException("Misplaced object."); + + } + + + /** + * Pop an array or object scope. + * @param c The scope to close. + * @throws JSONException If nesting is wrong. + */ + private void pop(char c) throws JSONException { + if (this.top <= 0) { + throw new JSONException("Nesting error."); + } + char m = this.stack[this.top - 1] == null ? 'a' : 'k'; + if (m != c) { + throw new JSONException("Nesting error."); + } + this.top -= 1; + this.mode = this.top == 0 + ? 'd' + : this.stack[this.top - 1] == null + ? 'a' + : 'k'; + } + + /** + * Push an array or object scope. + * @param jo The scope to open. + * @throws JSONException If nesting is too deep. + */ + private void push(JSONObject jo) throws JSONException { + if (this.top >= maxdepth) { + throw new JSONException("Nesting too deep."); + } + this.stack[this.top] = jo; + this.mode = jo == null ? 'a' : 'k'; + this.top += 1; + } + + /** + * Make a JSON text of an Object value. If the object has an + * value.toJSONString() method, then that method will be used to produce the + * JSON text. The method is required to produce a strictly conforming text. + * If the object does not contain a toJSONString method (which is the most + * common case), then a text will be produced by other means. If the value + * is an array or Collection, then a JSONArray will be made from it and its + * toJSONString method will be called. If the value is a MAP, then a + * JSONObject will be made from it and its toJSONString method will be + * called. Otherwise, the value's toString method will be called, and the + * result will be quoted. + * + *

+ * Warning: This method assumes that the data structure is acyclical. + * + * @param value + * The value to be serialized. + * @return a printable, displayable, transmittable representation of the + * object, beginning with { (left + * brace) and ending with } (right + * brace). + * @throws JSONException + * If the value is or contains an invalid number. + */ + public static String valueToString(Object value) throws JSONException { + if (value == null || value.equals(null)) { + return "null"; + } + if (value instanceof JSONString) { + String object; + try { + object = ((JSONString) value).toJSONString(); + } catch (Exception e) { + throw new JSONException(e); + } + if (object != null) { + return object; + } + throw new JSONException("Bad value from toJSONString: " + object); + } + if (value instanceof Number) { + // not all Numbers may match actual JSON Numbers. i.e. Fractions or Complex + final String numberAsString = JSONObject.numberToString((Number) value); + if(JSONObject.NUMBER_PATTERN.matcher(numberAsString).matches()) { + // Close enough to a JSON number that we will return it unquoted + return numberAsString; + } + // The Number value is not a valid JSON number. + // Instead we will quote it as a string + return JSONObject.quote(numberAsString); + } + if (value instanceof Boolean || value instanceof JSONObject + || value instanceof JSONArray) { + return value.toString(); + } + if (value instanceof Map) { + Map map = (Map) value; + return new JSONObject(map).toString(); + } + if (value instanceof Collection) { + Collection coll = (Collection) value; + return new JSONArray(coll).toString(); + } + if (value.getClass().isArray()) { + return new JSONArray(value).toString(); + } + if(value instanceof Enum){ + return JSONObject.quote(((Enum)value).name()); + } + return JSONObject.quote(value.toString()); + } + + /** + * Append either the value true or the value + * false. + * @param b A boolean. + * @return this + * @throws JSONException + */ + public JSONWriter value(boolean b) throws JSONException { + return this.append(b ? "true" : "false"); + } + + /** + * Append a double value. + * @param d A double. + * @return this + * @throws JSONException If the number is not finite. + */ + public JSONWriter value(double d) throws JSONException { + return this.value(Double.valueOf(d)); + } + + /** + * Append a long value. + * @param l A long. + * @return this + * @throws JSONException + */ + public JSONWriter value(long l) throws JSONException { + return this.append(Long.toString(l)); + } + + + /** + * Append an object value. + * @param object The object to append. It can be null, or a Boolean, Number, + * String, JSONObject, or JSONArray, or an object that implements JSONString. + * @return this + * @throws JSONException If the value is out of sequence. + */ + public JSONWriter value(Object object) throws JSONException { + return this.append(valueToString(object)); + } +} diff --git a/src/main/resources/lang.yml b/src/main/resources/lang.yml new file mode 100644 index 0000000..4846405 --- /dev/null +++ b/src/main/resources/lang.yml @@ -0,0 +1,9 @@ +command: + no-sender-error: "&f你无法执行此命令" + unknown: "&f未知的指令 &c$0" + short-arg: "&f缺少参数" + unknown-arg: "&f未知的参数 &8(&7第$0个参数&8) &7&o[$1]" + error-arg: "&f参数错误 &8(&7第$0个参数&8) &7&o[$1]" + no-player: "&f该指令只能由玩家执行" + player-no-online: "&f玩家 &e$0 &f不存在或不在线" + no-permission: "&f你没有执行该命令的权限" \ No newline at end of file diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml new file mode 100644 index 0000000..95a7444 --- /dev/null +++ b/src/main/resources/plugin.yml @@ -0,0 +1,5 @@ +name: ElementOriginLib +version: '1.0' +main: com.io.yutian.elementoriginlib.ElementOriginLib +api-version: '1.20' +authors: [ SuperYuTian ] diff --git a/src/main/resources/redis.yml b/src/main/resources/redis.yml new file mode 100644 index 0000000..830dffd --- /dev/null +++ b/src/main/resources/redis.yml @@ -0,0 +1,3 @@ +redis-server: 127.0.0.1 +redis-port: 6379 +redis-password: "none" \ No newline at end of file