From 87d1c3b5ed3cc8b51685e9c979b05b43c053c69f Mon Sep 17 00:00:00 2001 From: Keuin Date: Thu, 24 Dec 2020 20:31:22 +0800 Subject: Maybe it works. Save in case. --- build.gradle | 3 +- src/main/java/com/keuin/blame/Blame.java | 125 +++++++++++++++------ src/main/java/com/keuin/blame/EventHandler.java | 17 ++- src/main/java/com/keuin/blame/SubmitWorker.java | 51 ++------- .../com/keuin/blame/command/BlameBlockCommand.java | 68 +++++++++++ src/main/java/com/keuin/blame/data/LogEntry.java | 85 ++++++++++---- .../java/com/keuin/blame/data/LogEntryFactory.java | 21 ++-- src/main/java/com/keuin/blame/data/WorldPos.java | 21 +++- .../com/keuin/blame/data/enums/ActionType.java | 21 ++-- .../com/keuin/blame/data/enums/ObjectType.java | 10 +- .../blame/data/enums/codec/LogEntryCodec.java | 104 +++++++++++++++++ .../blame/data/enums/codec/LogEntryNames.java | 14 +++ .../blame/data/enums/codec/WorldPosCodec.java | 51 +++++++++ .../keuin/blame/lookup/AbstractLookupFilter.java | 13 +++ .../keuin/blame/lookup/BlockPosLookupFilter.java | 22 ++++ .../com/keuin/blame/lookup/LookupCallback.java | 7 ++ .../blame/lookup/LookupFilterWithCallback.java | 41 +++++++ .../java/com/keuin/blame/lookup/LookupManager.java | 32 ++++++ .../java/com/keuin/blame/lookup/LookupWorker.java | 82 ++++++++++++++ .../com/keuin/blame/lookup/TestableFilter.java | 14 +++ .../java/com/keuin/blame/util/DatabaseUtil.java | 38 +++++++ src/main/java/com/keuin/blame/util/PrettyUtil.java | 10 ++ src/main/java/com/keuin/blame/util/PrintUtil.java | 4 +- src/main/java/com/keuin/blame/util/UuidUtils.java | 3 + 24 files changed, 719 insertions(+), 138 deletions(-) create mode 100644 src/main/java/com/keuin/blame/command/BlameBlockCommand.java create mode 100644 src/main/java/com/keuin/blame/data/enums/codec/LogEntryCodec.java create mode 100644 src/main/java/com/keuin/blame/data/enums/codec/LogEntryNames.java create mode 100644 src/main/java/com/keuin/blame/data/enums/codec/WorldPosCodec.java create mode 100644 src/main/java/com/keuin/blame/lookup/AbstractLookupFilter.java create mode 100644 src/main/java/com/keuin/blame/lookup/BlockPosLookupFilter.java create mode 100644 src/main/java/com/keuin/blame/lookup/LookupCallback.java create mode 100644 src/main/java/com/keuin/blame/lookup/LookupFilterWithCallback.java create mode 100644 src/main/java/com/keuin/blame/lookup/LookupManager.java create mode 100644 src/main/java/com/keuin/blame/lookup/LookupWorker.java create mode 100644 src/main/java/com/keuin/blame/lookup/TestableFilter.java create mode 100644 src/main/java/com/keuin/blame/util/DatabaseUtil.java create mode 100644 src/main/java/com/keuin/blame/util/PrettyUtil.java diff --git a/build.gradle b/build.gradle index 2cb4da5..adcfe23 100644 --- a/build.gradle +++ b/build.gradle @@ -12,7 +12,8 @@ group = project.maven_group dependencies { - compile 'org.mongodb:mongo-java-driver:3.12.7' + implementation 'junit:junit:4.12' + compile 'org.mongodb:mongo-java-driver:3.12.7' // To change the versions see the gradle.properties file minecraft "com.mojang:minecraft:${project.minecraft_version}" diff --git a/src/main/java/com/keuin/blame/Blame.java b/src/main/java/com/keuin/blame/Blame.java index 1e53a64..6ecfa97 100644 --- a/src/main/java/com/keuin/blame/Blame.java +++ b/src/main/java/com/keuin/blame/Blame.java @@ -2,11 +2,20 @@ package com.keuin.blame; import com.google.gson.Gson; import com.keuin.blame.adapter.*; +import com.keuin.blame.command.BlameBlockCommand; import com.keuin.blame.config.BlameConfig; +import com.keuin.blame.lookup.LookupManager; import com.keuin.blame.util.PrintUtil; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.arguments.IntegerArgumentType; +import com.mojang.brigadier.arguments.StringArgumentType; import net.fabricmc.api.ModInitializer; +import net.fabricmc.fabric.api.command.v1.CommandRegistrationCallback; import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; import net.fabricmc.fabric.api.event.player.*; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.command.CommandManager; +import net.minecraft.server.command.ServerCommandSource; import java.io.File; import java.io.IOException; @@ -17,39 +26,85 @@ import java.util.logging.Logger; public class Blame implements ModInitializer { - private final Logger logger = Logger.getLogger(Blame.class.getName()); - - public static BlameConfig config; - - @Override - public void onInitialize() { - // This code runs as soon as Minecraft is in a mod-load-ready state. - // However, some things (like resources) may still be uninitialized. - // Proceed with mild caution. - - String configFileName = "blame.json"; - try { - // load config - File configFile = new File(configFileName); - if (!configFile.exists()) { - logger.severe(String.format("Failed to read configuration file %s. Blame will be disabled.", configFileName)); - return; - } - - Reader reader = Files.newBufferedReader(configFile.toPath(), StandardCharsets.UTF_8); - config = (new Gson()).fromJson(reader, BlameConfig.class); - } catch (IOException exception) { - logger.severe(String.format("Failed to read configuration file %s: %s. " + - "Blame will be disabled.", configFileName, exception)); - return; - } - - AttackEntityCallback.EVENT.register(new AttackEntityAdapter(EventHandler.INSTANCE)); - PlayerBlockBreakEvents.AFTER.register(new BreakBlockAdapter(EventHandler.INSTANCE)); - UseBlockCallback.EVENT.register(new UseBlockAdapter(EventHandler.INSTANCE)); - UseEntityCallback.EVENT.register(new UseEntityAdapter(EventHandler.INSTANCE)); - UseItemCallback.EVENT.register(new UseItemAdapter(EventHandler.INSTANCE)); - - ServerLifecycleEvents.SERVER_STARTED.register(PrintUtil.INSTANCE); - } + private static final Logger logger = Logger.getLogger(Blame.class.getName()); + + public static BlameConfig config; + + public static boolean loadConfig() { + String configFileName = "blame.json"; + try { + // load config + File configFile = new File(configFileName); + if (!configFile.exists()) { + logger.severe(String.format("Failed to read configuration file %s. Blame will be disabled.", configFileName)); + return false; + } + + Reader reader = Files.newBufferedReader(configFile.toPath(), StandardCharsets.UTF_8); + config = (new Gson()).fromJson(reader, BlameConfig.class); + } catch (IOException exception) { + logger.severe(String.format("Failed to read configuration file %s: %s. " + + "Blame will be disabled.", configFileName, exception)); + return false; + } + return true; + } + + @Override + public void onInitialize() { + // This code runs as soon as Minecraft is in a mod-load-ready state. + // However, some things (like resources) may still be uninitialized. + // Proceed with mild caution. + + if (!loadConfig()) + return; + + // hook disable event + ServerLifecycleEvents.SERVER_STOPPING.register(new ServerLifecycleEvents.ServerStopping() { + @Override + public void onServerStopping(MinecraftServer minecraftServer) { + logger.info("Stopping LookupManager..."); + LookupManager.INSTANCE.stop(); + + logger.info("Stopping SubmitWorker..."); + SubmitWorker.INSTANCE.stop(); + } + }); + + // hook game events + AttackEntityCallback.EVENT.register(new AttackEntityAdapter(EventHandler.INSTANCE)); + PlayerBlockBreakEvents.AFTER.register(new BreakBlockAdapter(EventHandler.INSTANCE)); + UseBlockCallback.EVENT.register(new UseBlockAdapter(EventHandler.INSTANCE)); + UseEntityCallback.EVENT.register(new UseEntityAdapter(EventHandler.INSTANCE)); + UseItemCallback.EVENT.register(new UseItemAdapter(EventHandler.INSTANCE)); + + // initialize PrintUtil + ServerLifecycleEvents.SERVER_STARTED.register(PrintUtil.INSTANCE); + + // register + CommandRegistrationCallback.EVENT.register(new CommandRegistrationCallback() { + @Override + public void register(CommandDispatcher commandDispatcher, boolean b) { + commandDispatcher.register( + CommandManager.literal("blame") + .then( + CommandManager.literal("block") + .then( + CommandManager.argument("x", IntegerArgumentType.integer()) + .then( + CommandManager.argument("y", IntegerArgumentType.integer()) + .then( + CommandManager.argument("z", IntegerArgumentType.integer()) + .then( + CommandManager.argument("world", StringArgumentType.greedyString()) + .executes(BlameBlockCommand::run) + ) + ) + ) + ) + ) + ); + } + }); + } } diff --git a/src/main/java/com/keuin/blame/EventHandler.java b/src/main/java/com/keuin/blame/EventHandler.java index a939759..df10e5b 100644 --- a/src/main/java/com/keuin/blame/EventHandler.java +++ b/src/main/java/com/keuin/blame/EventHandler.java @@ -30,8 +30,7 @@ public class EventHandler implements AttackEntityHandler, BreakBlockHandler, Use String worldString = MinecraftUtil.worldToString(world); String blockId = Registry.BLOCK.getId(world.getBlockState(blockHitResult.getBlockPos()).getBlock()).toString(); LogEntry entry = LogEntryFactory.playerWithBlock( - playerEntity.getUuid(), - playerEntity.getPos(), + playerEntity, worldString, blockId, blockHitResult.getBlockPos(), @@ -49,8 +48,7 @@ public class EventHandler implements AttackEntityHandler, BreakBlockHandler, Use String worldString = MinecraftUtil.worldToString(world); String blockId = Registry.BLOCK.getId(blockState.getBlock()).toString(); LogEntry entry = LogEntryFactory.playerWithBlock( - playerEntity.getUuid(), - playerEntity.getPos(), + playerEntity, worldString, blockId, blockPos, @@ -67,8 +65,7 @@ public class EventHandler implements AttackEntityHandler, BreakBlockHandler, Use String entityId = Registry.ENTITY_TYPE.getId(entity.getType()).toString(); String worldString = MinecraftUtil.worldToString(world); LogEntry entry = LogEntryFactory.playerWithEntity( - playerEntity.getUuid(), - playerEntity.getPos(), + playerEntity, worldString, entityId, entity.getPos(), @@ -85,8 +82,7 @@ public class EventHandler implements AttackEntityHandler, BreakBlockHandler, Use String entityId = Registry.ENTITY_TYPE.getId(entity.getType()).toString(); String worldString = MinecraftUtil.worldToString(world); LogEntry entry = LogEntryFactory.playerWithEntity( - playerEntity.getUuid(), - playerEntity.getPos(), + playerEntity, worldString, entityId, entity.getPos(), @@ -96,6 +92,7 @@ public class EventHandler implements AttackEntityHandler, BreakBlockHandler, Use SubmitWorker.INSTANCE.submit(entry); PrintUtil.broadcast("use_entity; entity_id=" + entityId); // TODO: 增加判断,无效的时候也会触发这个事件 + // TODO: 增加cooldown,过滤掉两个相邻重复事件(时间间隔大概为20ms+) PrintUtil.broadcast(String.format("player %s use entity %s", playerEntity.getName().getString(), entity)); } @@ -103,14 +100,14 @@ public class EventHandler implements AttackEntityHandler, BreakBlockHandler, Use public void onPlayerUseItem(PlayerEntity playerEntity, World world, Hand hand) { String itemId = Registry.ITEM.getId(playerEntity.getStackInHand(hand).getItem()).toString(); LogEntry entry = LogEntryFactory.playerWithItem( - playerEntity.getUuid(), - playerEntity.getPos(), + playerEntity, MinecraftUtil.worldToString(world), itemId, ActionType.ITEM_USE ); SubmitWorker.INSTANCE.submit(entry); PrintUtil.broadcast("use_item; item_id=" + itemId); + // TODO: 增加cooldown,过滤掉两个相邻重复事件(时间间隔大概为20ms+) // PrintUtil.broadcast(String.format("player %s use item %s", playerEntity.getName().getString(), playerEntity.getStackInHand(hand))); } } diff --git a/src/main/java/com/keuin/blame/SubmitWorker.java b/src/main/java/com/keuin/blame/SubmitWorker.java index 1c47aaf..c7334f8 100644 --- a/src/main/java/com/keuin/blame/SubmitWorker.java +++ b/src/main/java/com/keuin/blame/SubmitWorker.java @@ -1,22 +1,12 @@ package com.keuin.blame; -import com.keuin.blame.config.MongoConfig; import com.keuin.blame.data.LogEntry; -import com.keuin.blame.data.enums.ActionType; -import com.keuin.blame.data.enums.ObjectType; -import com.keuin.blame.data.enums.codec.ActionTypeCodec; -import com.keuin.blame.data.enums.codec.ObjectTypeCodec; -import com.keuin.blame.data.enums.transformer.ActionTypeTransformer; -import com.keuin.blame.data.enums.transformer.ObjectTypeTransformer; -import com.mongodb.ConnectionString; -import com.mongodb.MongoClientSettings; +import com.keuin.blame.util.DatabaseUtil; +import com.mongodb.MongoClientException; import com.mongodb.client.MongoClient; import com.mongodb.client.MongoClients; import com.mongodb.client.MongoCollection; import com.mongodb.client.MongoDatabase; -import org.bson.BSON; -import org.bson.codecs.configuration.CodecRegistries; -import org.bson.codecs.configuration.CodecRegistry; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingDeque; @@ -24,37 +14,16 @@ import java.util.logging.Logger; public class SubmitWorker { - public static final SubmitWorker INSTANCE = new SubmitWorker(Blame.config.getMongoConfig()); + public static final SubmitWorker INSTANCE = new SubmitWorker(); private final Logger logger = Logger.getLogger(SubmitWorker.class.getName()); private final BlockingQueue queue = new LinkedBlockingDeque<>(4096); private final Thread thread = new Thread(SubmitWorker.this::run); private boolean run = true; - private final MongoConfig mongoConfig; - private final MongoClientSettings settings; - - private SubmitWorker(MongoConfig mongoConfig) { - if (mongoConfig == null) - throw new IllegalArgumentException("mongo config cannot be null"); - this.mongoConfig = mongoConfig; - logger.fine("Connecting to MongoDB server `" + mongoConfig.getAddress() - + "` with database `" + mongoConfig.getDatabaseName() - + "` and collection `" + mongoConfig.getLogCollectionName() + "`."); - - BSON.addEncodingHook(ActionType.class, new ActionTypeTransformer()); - BSON.addEncodingHook(ObjectType.class, new ObjectTypeTransformer()); - - CodecRegistry codecRegistry = CodecRegistries.fromRegistries( - com.mongodb.MongoClient.getDefaultCodecRegistry(), - CodecRegistries.fromCodecs(new ActionTypeCodec(), new ObjectTypeCodec()) - ); - - settings = MongoClientSettings.builder() - .applyConnectionString(new ConnectionString(mongoConfig.getAddress())) - .codecRegistry(codecRegistry) - .build(); + private SubmitWorker() { + thread.setUncaughtExceptionHandler((t, e) -> logger.severe(String.format("Exception in thread %s: %s", t.getName(), e))); thread.start(); } @@ -70,18 +39,22 @@ public class SubmitWorker { } private void run() { - try (final MongoClient mongoClient = MongoClients.create(settings)) { + try (final MongoClient mongoClient = MongoClients.create(DatabaseUtil.CLIENT_SETTINGS)) { final MongoDatabase db = mongoClient.getDatabase( - mongoConfig.getDatabaseName() + DatabaseUtil.MONGO_CONFIG.getDatabaseName() ); final MongoCollection collection = db.getCollection( - mongoConfig.getLogCollectionName(), LogEntry.class + DatabaseUtil.MONGO_CONFIG.getLogCollectionName(), LogEntry.class ); + // TODO: 第一个事件触发导致延迟很大 while (this.run) { LogEntry entry = queue.take(); collection.insertOne(entry); + logger.info("Entry inserted."); } } catch (InterruptedException ignored) { + } catch (MongoClientException exception) { + logger.severe("Failed to submit: " + exception + ". Worker is quitting..."); } } diff --git a/src/main/java/com/keuin/blame/command/BlameBlockCommand.java b/src/main/java/com/keuin/blame/command/BlameBlockCommand.java new file mode 100644 index 0000000..63d99ec --- /dev/null +++ b/src/main/java/com/keuin/blame/command/BlameBlockCommand.java @@ -0,0 +1,68 @@ +package com.keuin.blame.command; + +import com.keuin.blame.data.LogEntry; +import com.keuin.blame.data.WorldPos; +import com.keuin.blame.lookup.BlockPosLookupFilter; +import com.keuin.blame.lookup.LookupCallback; +import com.keuin.blame.lookup.LookupManager; +import com.keuin.blame.util.PrintUtil; +import com.mojang.brigadier.context.CommandContext; +import net.minecraft.entity.Entity; +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.server.network.ServerPlayerEntity; + +import java.util.Iterator; + +public class BlameBlockCommand { + + private static final int SUCCESS = 1; + private static final int FAILED = -1; + + public static int run(CommandContext context) { + Entity entity = context.getSource().getEntity(); + if (!(entity instanceof ServerPlayerEntity)) { + // can only be executed by player + return FAILED; + } + + ServerPlayerEntity playerEntity = (ServerPlayerEntity) entity; + int x = context.getArgument("x", Integer.class); + int y = context.getArgument("y", Integer.class); + int z = context.getArgument("z", Integer.class); + String world = context.getArgument("world", String.class); +// String world = MinecraftUtil.worldToString(playerEntity.world); + WorldPos blockPos = new WorldPos(world, x, y, z); + LookupManager.INSTANCE.lookup( + new BlockPosLookupFilter(blockPos), + new Callback(context) + ); + return SUCCESS; + } + + private static class Callback implements LookupCallback { + + private final CommandContext context; + + private Callback(CommandContext context) { + this.context = context; + } + + @Override + public void onLookupFinishes(Iterable logEntries) { + StringBuilder printBuilder = new StringBuilder(); + Iterator iterator = logEntries.iterator(); + int printCount; + for (printCount = 0; printCount < 5; ++printCount) { + if (!iterator.hasNext()) + break; + LogEntry logEntry = iterator.next(); + printBuilder.append(logEntry.toString()); + printBuilder.append("\n") + .append("================") + .append("\n"); + } + printBuilder.append(String.format("Displayed the most recent %d items.", printCount)); + PrintUtil.msgInfo(context, printBuilder.toString()); + } + } +} diff --git a/src/main/java/com/keuin/blame/data/LogEntry.java b/src/main/java/com/keuin/blame/data/LogEntry.java index 30037c0..e178dc2 100644 --- a/src/main/java/com/keuin/blame/data/LogEntry.java +++ b/src/main/java/com/keuin/blame/data/LogEntry.java @@ -2,7 +2,9 @@ package com.keuin.blame.data; import com.keuin.blame.data.enums.ActionType; import com.keuin.blame.data.enums.ObjectType; -import org.bson.codecs.pojo.annotations.BsonProperty; +import com.keuin.blame.util.PrettyUtil; +import com.keuin.blame.util.UuidUtils; +import net.minecraft.MinecraftVersion; import java.util.Objects; import java.util.UUID; @@ -34,24 +36,34 @@ public class LogEntry { // } //} - @BsonProperty("version") - private final int version = 1; - @BsonProperty("timestamp_millis") - private final long timeMillis; - @BsonProperty("subject_uuid") - private final String subjectUUID; // TODO: use Binary instead (BasicDBObject("_id", Binary(session.getIp().getAddress()))) (https://stackoverflow.com/questions/30566905/store-byte-in-mongodb-using-java/40843195) - @BsonProperty("subject_pos") - private final WorldPos subjectPos; // TODO: write codec and transformer for this - @BsonProperty("action_type") - private final ActionType actionType; - @BsonProperty("object_type") - private final ObjectType objectType; - @BsonProperty("object_id") - private final String objectId; - @BsonProperty("object_pos") - private final WorldPos objectPos; - - public LogEntry(long timeMillis, UUID subjectUUID, WorldPos subjectPos, ActionType actionType, ObjectType objectType, String objectId, WorldPos objectPos) { + // @BsonProperty("version") + private static int version = 1; + // @BsonProperty("game_version") + private static String gameVersion = MinecraftVersion.field_25319.getName(); + // @BsonProperty("timestamp_millis") + private long timeMillis = 0; + // @BsonProperty("subject_id") + private String subjectId = ""; + // @BsonProperty("subject_uuid") + private String subjectUUID = UuidUtils.UUID_NULL.toString(); // TODO: use Binary instead (BasicDBObject("_id", Binary(session.getIp().getAddress()))) (https://stackoverflow.com/questions/30566905/store-byte-in-mongodb-using-java/40843195) + // @BsonProperty("subject_pos") + private WorldPos subjectPos = WorldPos.NULL_POS; // TODO: write codec and transformer for this + // @BsonProperty("action_type") + private ActionType actionType = ActionType.NULL; + // @BsonProperty("object_type") + private ObjectType objectType = ObjectType.NULL; + // @BsonProperty("object_id") + private String objectId = ""; + // @BsonProperty("object_pos") + private WorldPos objectPos = WorldPos.NULL_POS; + + public static final LogEntry EMPTY_ENTRY = new LogEntry(); + + protected LogEntry() { + } + + public LogEntry(long timeMillis, String subjectId, UUID subjectUUID, WorldPos subjectPos, ActionType actionType, ObjectType objectType, String objectId, WorldPos objectPos) { + this.subjectId = subjectId; // this.subjectUUID = UuidUtils.asBytes(subjectUUID); // this.subjectUUID if (subjectUUID == null) @@ -77,13 +89,21 @@ public class LogEntry { } public int getVersion() { - return 1; + return version; + } + + public String getGameVersion() { + return gameVersion; } public long getTimeMillis() { return timeMillis; } + public String getSubjectId() { + return subjectId; + } + public UUID getSubjectUUID() { return UUID.fromString(subjectUUID); } @@ -113,8 +133,8 @@ public class LogEntry { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; LogEntry entry = (LogEntry) o; - return version == entry.version && - timeMillis == entry.timeMillis && + return timeMillis == entry.timeMillis && + Objects.equals(subjectId, entry.subjectId) && Objects.equals(subjectUUID, entry.subjectUUID) && Objects.equals(subjectPos, entry.subjectPos) && actionType == entry.actionType && @@ -125,6 +145,25 @@ public class LogEntry { @Override public int hashCode() { - return Objects.hash(version, timeMillis, subjectUUID, subjectPos, actionType, objectType, objectId, objectPos); + return Objects.hash(timeMillis, subjectId, subjectUUID, subjectPos, actionType, objectType, objectId, objectPos); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("Time: ").append(PrettyUtil.timestampToString(timeMillis)).append("\n"); + builder.append("Subject: ").append(subjectId).append("{").append(subjectUUID).append("}@") + .append(subjectPos.toString()) + .append("\n"); + builder.append("Action: ").append(actionType.toString()).append("\n"); + builder.append("Object: ").append(objectType.toString()).append("[").append(objectId).append("]@") + .append(objectPos.toString()) + .append("\n"); + builder.append("(entryVersion: ") + .append(version) + .append(", gameVersion:") + .append(gameVersion) + .append(")"); + return builder.toString(); } } diff --git a/src/main/java/com/keuin/blame/data/LogEntryFactory.java b/src/main/java/com/keuin/blame/data/LogEntryFactory.java index 1e2d5d1..bf71b27 100644 --- a/src/main/java/com/keuin/blame/data/LogEntryFactory.java +++ b/src/main/java/com/keuin/blame/data/LogEntryFactory.java @@ -2,16 +2,17 @@ package com.keuin.blame.data; import com.keuin.blame.data.enums.ActionType; import com.keuin.blame.data.enums.ObjectType; +import net.minecraft.entity.player.PlayerEntity; import net.minecraft.util.math.Vec3d; import net.minecraft.util.math.Vec3i; -import java.util.UUID; - public class LogEntryFactory { - public static LogEntry playerWithBlock(UUID playerUUID, Vec3d playerPos, String playerWorld, String blockId, Vec3i blockPos, String blockWorld, ActionType actionType) { + public static LogEntry playerWithBlock(PlayerEntity player, String playerWorld, String blockId, Vec3i blockPos, String blockWorld, ActionType actionType) { + Vec3d playerPos = player.getPos(); return new LogEntry( System.currentTimeMillis(), - playerUUID, + player.getName().asString(), + player.getUuid(), new WorldPos(playerWorld, playerPos.x, playerPos.y, playerPos.z), actionType, ObjectType.BLOCK, @@ -20,10 +21,12 @@ public class LogEntryFactory { ); } - public static LogEntry playerWithEntity(UUID playerUUID, Vec3d playerPos, String playerWorld, String entityId, Vec3d entityPos, String entityWorld, ActionType actionType) { + public static LogEntry playerWithEntity(PlayerEntity player, String playerWorld, String entityId, Vec3d entityPos, String entityWorld, ActionType actionType) { + Vec3d playerPos = player.getPos(); return new LogEntry( System.currentTimeMillis(), - playerUUID, + player.getName().asString(), + player.getUuid(), new WorldPos(playerWorld, playerPos.x, playerPos.y, playerPos.z), actionType, ObjectType.ENTITY, @@ -32,10 +35,12 @@ public class LogEntryFactory { ); } - public static LogEntry playerWithItem(UUID playerUUID, Vec3d playerPos, String playerWorld, String itemId, ActionType actionType) { + public static LogEntry playerWithItem(PlayerEntity player, String playerWorld, String itemId, ActionType actionType) { + Vec3d playerPos = player.getPos(); return new LogEntry( System.currentTimeMillis(), - playerUUID, + player.getName().asString(), + player.getUuid(), new WorldPos(playerWorld, playerPos.x, playerPos.y, playerPos.z), actionType, ObjectType.ENTITY, diff --git a/src/main/java/com/keuin/blame/data/WorldPos.java b/src/main/java/com/keuin/blame/data/WorldPos.java index aeda093..6783fea 100644 --- a/src/main/java/com/keuin/blame/data/WorldPos.java +++ b/src/main/java/com/keuin/blame/data/WorldPos.java @@ -6,10 +6,10 @@ public class WorldPos { // immutable - private final String world; - private final double x; - private final double y; - private final double z; + private String world = ""; + private double x = 0; + private double y = 0; + private double z = 0; public static final WorldPos NULL_POS = new WorldPos("", 0, 0, 0); @@ -20,6 +20,7 @@ public class WorldPos { this.x = x; this.y = y; this.z = z; +// System.out.printf("%s, %f, %f, %f%n", world, x, y, z); } public String getWorld() { @@ -53,4 +54,16 @@ public class WorldPos { public int hashCode() { return Objects.hash(world, x, y, z); } + + @Override + public String toString() { + return String.format("(%s, %s, %s -> %s)", prettyDouble(x), prettyDouble(y), prettyDouble(z), world); + } + + private String prettyDouble(double d) { + if ((d - (int) d) < 1e-3) + return String.valueOf((int) d); + else + return String.format("%.3f", d); + } } diff --git a/src/main/java/com/keuin/blame/data/enums/ActionType.java b/src/main/java/com/keuin/blame/data/enums/ActionType.java index dcf2aa7..58c4a56 100644 --- a/src/main/java/com/keuin/blame/data/enums/ActionType.java +++ b/src/main/java/com/keuin/blame/data/enums/ActionType.java @@ -2,17 +2,20 @@ package com.keuin.blame.data.enums; public enum ActionType implements IntegerEnum { - BLOCK_BREAK(1), - BLOCK_PLACE(2), - BLOCK_USE(3), - ENTITY_ATTACK(4), - ENTITY_USE(5), - ITEM_USE(6); + NULL(0, "NULL"), + BLOCK_BREAK(1, "BREAK_BLOCK"), + BLOCK_PLACE(2, "PLACE_BLOCK"), + BLOCK_USE(3, "USE_BLOCK"), + ENTITY_ATTACK(4, "ATTACK_ENTITY"), + ENTITY_USE(5, "USE_ENTITY"), + ITEM_USE(6, "USE_ITEM"); private final int value; + private final String typeString; - ActionType(int value) { + ActionType(int value, String typeString) { this.value = value; + this.typeString = typeString; } public static ActionType parseInt(int value) { @@ -29,9 +32,7 @@ public enum ActionType implements IntegerEnum { @Override public String toString() { - return "ActionType{" + - "value=" + value + - '}'; + return typeString; } } diff --git a/src/main/java/com/keuin/blame/data/enums/ObjectType.java b/src/main/java/com/keuin/blame/data/enums/ObjectType.java index b3b1d67..4b9312b 100644 --- a/src/main/java/com/keuin/blame/data/enums/ObjectType.java +++ b/src/main/java/com/keuin/blame/data/enums/ObjectType.java @@ -2,12 +2,14 @@ package com.keuin.blame.data.enums; public enum ObjectType implements IntegerEnum { - BLOCK(1), ENTITY(2); + NULL(0, "NULL"), BLOCK(1, "BLOCK"), ENTITY(2, "ENTITY"); private final int value; + private final String typeString; - ObjectType(int value) { + ObjectType(int value, String typeString) { this.value = value; + this.typeString = typeString; } public static ObjectType parseInt(int value) { @@ -24,9 +26,7 @@ public enum ObjectType implements IntegerEnum { @Override public String toString() { - return "ObjectType{" + - "value=" + value + - '}'; + return typeString; } } diff --git a/src/main/java/com/keuin/blame/data/enums/codec/LogEntryCodec.java b/src/main/java/com/keuin/blame/data/enums/codec/LogEntryCodec.java new file mode 100644 index 0000000..4bb61a3 --- /dev/null +++ b/src/main/java/com/keuin/blame/data/enums/codec/LogEntryCodec.java @@ -0,0 +1,104 @@ +package com.keuin.blame.data.enums.codec; + +import com.keuin.blame.data.LogEntry; +import com.keuin.blame.data.WorldPos; +import com.keuin.blame.data.enums.ActionType; +import com.keuin.blame.data.enums.ObjectType; +import org.bson.*; +import org.bson.codecs.*; +import org.bson.codecs.configuration.CodecRegistries; +import org.bson.codecs.configuration.CodecRegistry; +import org.bson.codecs.pojo.PojoCodecProvider; + +import java.util.Objects; +import java.util.UUID; + +import static com.keuin.blame.data.enums.codec.LogEntryNames.*; +import static org.bson.codecs.configuration.CodecRegistries.fromProviders; + +public class LogEntryCodec implements CollectibleCodec { + + private final Codec documentCodec; + + public LogEntryCodec() { + CodecRegistry CODEC_REGISTRY = CodecRegistries.fromRegistries( + com.mongodb.MongoClient.getDefaultCodecRegistry(), + CodecRegistries.fromCodecs( + new ActionTypeCodec(), + new ObjectTypeCodec(), + new WorldPosCodec() + ), + fromProviders(PojoCodecProvider.builder().automatic(true).build()) + ); + documentCodec = new DocumentCodec( + CODEC_REGISTRY + ); + } + + public LogEntryCodec(Codec documentCodec) { + this.documentCodec = documentCodec; + } + + + @Override + public LogEntry decode(BsonReader reader, DecoderContext decoderContext) { + Document document = documentCodec.decode(reader, decoderContext); + Integer entryVersion = document.getInteger("version"); + if (entryVersion == null) + return LogEntry.EMPTY_ENTRY; + if (Objects.equals(LogEntry.EMPTY_ENTRY.getVersion(), entryVersion)) { + LogEntry entry = new LogEntry( + document.getLong(TIMESTAMP_MILLIS), + document.getString(SUBJECT_ID), + document.get(SUBJECT_UUID, UUID.class), + WorldPos.NULL_POS, +// document.get(SUBJECT_POS, WorldPos.class), + ActionType.parseInt(document.getInteger(ACTION_TYPE)), + ObjectType.parseInt(document.getInteger(OBJECT_TYPE)), + document.getString(OBJECT_ID), + WorldPos.NULL_POS +// document.get(OBJECT_POS, WorldPos.class) + ); + return entry; + } + throw new RuntimeException(String.format("unsupported LogEntry version: %d. Perhaps your Blame is too old.", entryVersion)); + } + + @Override + public void encode(BsonWriter writer, LogEntry value, EncoderContext encoderContext) { + Document document = new Document(); + + document.put(VERSION, value.getVersion()); + document.put(GAME_VERSION, value.getGameVersion()); + document.put(TIMESTAMP_MILLIS, value.getTimeMillis()); + document.put(SUBJECT_ID, value.getSubjectId()); + document.put(SUBJECT_UUID, value.getSubjectUUID()); +// document.put(SUBJECT_POS, value.getSubjectPos()); + document.put(ACTION_TYPE, value.getActionType().getValue()); + document.put(OBJECT_TYPE, value.getObjectType().getValue()); + document.put(OBJECT_ID, value.getObjectId()); +// document.put(OBJECT_POS, value.getObjectPos()); + + documentCodec.encode(writer, document, encoderContext); + } + + @Override + public Class getEncoderClass() { + return LogEntry.class; + } + + @Override + public LogEntry generateIdIfAbsentFromDocument(LogEntry document) { + return document; + } + + @Override + public boolean documentHasId(LogEntry document) { + return document.getObjectId() != null; + } + + @Override + public BsonValue getDocumentId(LogEntry document) { + return new BsonString(document.getObjectId()); + } +} diff --git a/src/main/java/com/keuin/blame/data/enums/codec/LogEntryNames.java b/src/main/java/com/keuin/blame/data/enums/codec/LogEntryNames.java new file mode 100644 index 0000000..94edf4a --- /dev/null +++ b/src/main/java/com/keuin/blame/data/enums/codec/LogEntryNames.java @@ -0,0 +1,14 @@ +package com.keuin.blame.data.enums.codec; + +public class LogEntryNames { + public static final String VERSION = "version"; + public static final String GAME_VERSION = "game_version"; + public static final String TIMESTAMP_MILLIS = "timestamp_millis"; + public static final String SUBJECT_ID = "subject_id"; + public static final String SUBJECT_UUID = "subject_uuid"; + public static final String SUBJECT_POS = "subject_pos"; + public static final String ACTION_TYPE = "action_type"; + public static final String OBJECT_TYPE = "object_type"; + public static final String OBJECT_ID = "object_id"; + public static final String OBJECT_POS = "object_pos"; +} diff --git a/src/main/java/com/keuin/blame/data/enums/codec/WorldPosCodec.java b/src/main/java/com/keuin/blame/data/enums/codec/WorldPosCodec.java new file mode 100644 index 0000000..1f0fa91 --- /dev/null +++ b/src/main/java/com/keuin/blame/data/enums/codec/WorldPosCodec.java @@ -0,0 +1,51 @@ +package com.keuin.blame.data.enums.codec; + +import com.keuin.blame.data.WorldPos; +import org.bson.BsonReader; +import org.bson.BsonWriter; +import org.bson.Document; +import org.bson.codecs.Codec; +import org.bson.codecs.DecoderContext; +import org.bson.codecs.DocumentCodec; +import org.bson.codecs.EncoderContext; + +import java.util.Optional; + +public class WorldPosCodec implements Codec { + + private final Codec documentCodec; + + public WorldPosCodec() { + documentCodec = new DocumentCodec(); + } + + public WorldPosCodec(Codec documentCodec) { + this.documentCodec = documentCodec; + } + + @Override + public WorldPos decode(BsonReader reader, DecoderContext decoderContext) { + Document document = documentCodec.decode(reader, decoderContext); + return new WorldPos( + document.getString("world"), + document.getDouble("x"), + document.getDouble("y"), + document.getDouble("z") + ); + } + + @Override + public void encode(BsonWriter writer, WorldPos value, EncoderContext encoderContext) { + Document document = new Document(); + Optional.ofNullable(value.getWorld()).ifPresent(world -> document.put("world", world)); + document.put("x", value.getX()); + document.put("y", value.getY()); + document.put("z", value.getZ()); + documentCodec.encode(writer, document, encoderContext); + } + + @Override + public Class getEncoderClass() { + return WorldPos.class; + } +} diff --git a/src/main/java/com/keuin/blame/lookup/AbstractLookupFilter.java b/src/main/java/com/keuin/blame/lookup/AbstractLookupFilter.java new file mode 100644 index 0000000..f8774e0 --- /dev/null +++ b/src/main/java/com/keuin/blame/lookup/AbstractLookupFilter.java @@ -0,0 +1,13 @@ +package com.keuin.blame.lookup; + +import com.keuin.blame.data.LogEntry; +import com.mongodb.client.FindIterable; + +public abstract class AbstractLookupFilter { + // immutable + + AbstractLookupFilter() { + } + + abstract FindIterable find(FindIterable iterable); +} \ No newline at end of file diff --git a/src/main/java/com/keuin/blame/lookup/BlockPosLookupFilter.java b/src/main/java/com/keuin/blame/lookup/BlockPosLookupFilter.java new file mode 100644 index 0000000..67a564f --- /dev/null +++ b/src/main/java/com/keuin/blame/lookup/BlockPosLookupFilter.java @@ -0,0 +1,22 @@ +package com.keuin.blame.lookup; + +import com.keuin.blame.data.LogEntry; +import com.keuin.blame.data.WorldPos; +import com.mongodb.client.FindIterable; +import com.mongodb.client.model.Filters; + +public class BlockPosLookupFilter extends AbstractLookupFilter { + private final WorldPos blockPos; + + public BlockPosLookupFilter(WorldPos blockPos) { + this.blockPos = blockPos; + } + + @Override + FindIterable find(FindIterable iterable) { + return iterable.filter(Filters.and( + Filters.eq("version", 1), + Filters.eq("object_pos", blockPos) + )); + } +} diff --git a/src/main/java/com/keuin/blame/lookup/LookupCallback.java b/src/main/java/com/keuin/blame/lookup/LookupCallback.java new file mode 100644 index 0000000..eba954c --- /dev/null +++ b/src/main/java/com/keuin/blame/lookup/LookupCallback.java @@ -0,0 +1,7 @@ +package com.keuin.blame.lookup; + +import com.keuin.blame.data.LogEntry; + +public interface LookupCallback { + void onLookupFinishes(Iterable logEntries); +} diff --git a/src/main/java/com/keuin/blame/lookup/LookupFilterWithCallback.java b/src/main/java/com/keuin/blame/lookup/LookupFilterWithCallback.java new file mode 100644 index 0000000..cd6743c --- /dev/null +++ b/src/main/java/com/keuin/blame/lookup/LookupFilterWithCallback.java @@ -0,0 +1,41 @@ +package com.keuin.blame.lookup; + +import java.util.Objects; + +class LookupFilterWithCallback { + + private final LookupCallback callback; + private final AbstractLookupFilter filter; + + LookupFilterWithCallback(LookupCallback callback, AbstractLookupFilter filter) { + if (callback == null) + throw new IllegalArgumentException("callback cannot be null"); + if (filter == null) + throw new IllegalArgumentException("filter cannot be null"); + this.callback = callback; + this.filter = filter; + } + + public LookupCallback getCallback() { + return callback; + } + + public AbstractLookupFilter getFilter() { + return filter; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + LookupFilterWithCallback that = (LookupFilterWithCallback) o; + return callback.equals(that.callback) && + filter.equals(that.filter); + } + + @Override + public int hashCode() { + return Objects.hash(callback, filter); + } + +} diff --git a/src/main/java/com/keuin/blame/lookup/LookupManager.java b/src/main/java/com/keuin/blame/lookup/LookupManager.java new file mode 100644 index 0000000..2ffa86b --- /dev/null +++ b/src/main/java/com/keuin/blame/lookup/LookupManager.java @@ -0,0 +1,32 @@ +package com.keuin.blame.lookup; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingDeque; + +public class LookupManager { + + public static final LookupManager INSTANCE = new LookupManager(); + + private final BlockingQueue queue = new LinkedBlockingDeque<>(); + private final List workers = new ArrayList<>(); + + private LookupManager() { + // initialize workers + for (int i = 0; i < 10; ++i) { + LookupWorker worker = new LookupWorker(i, queue); + worker.start(); + workers.add(worker); + } + } + + public void stop() { + workers.forEach(LookupWorker::disable); + } + + public void lookup(AbstractLookupFilter filter, LookupCallback callback) { + queue.add(new LookupFilterWithCallback(callback, filter)); + } + +} diff --git a/src/main/java/com/keuin/blame/lookup/LookupWorker.java b/src/main/java/com/keuin/blame/lookup/LookupWorker.java new file mode 100644 index 0000000..a8e5b7d --- /dev/null +++ b/src/main/java/com/keuin/blame/lookup/LookupWorker.java @@ -0,0 +1,82 @@ +package com.keuin.blame.lookup; + +import com.keuin.blame.Blame; +import com.keuin.blame.config.MongoConfig; +import com.keuin.blame.data.LogEntry; +import com.keuin.blame.data.enums.codec.ActionTypeCodec; +import com.keuin.blame.data.enums.codec.ObjectTypeCodec; +import com.keuin.blame.data.enums.codec.WorldPosCodec; +import com.keuin.blame.util.DatabaseUtil; +import com.mongodb.ConnectionString; +import com.mongodb.MongoClientSettings; +import com.mongodb.client.*; +import org.bson.codecs.configuration.CodecRegistries; +import org.bson.codecs.configuration.CodecRegistry; +import org.bson.codecs.pojo.PojoCodecProvider; + +import java.util.concurrent.BlockingQueue; +import java.util.logging.Logger; + +import static org.bson.codecs.configuration.CodecRegistries.fromProviders; + +public class LookupWorker extends Thread { + + private final Logger logger; + private final BlockingQueue queue; + private boolean running = true; + + private static final MongoConfig MONGO_CONFIG = Blame.config.getMongoConfig(); + private static final CodecRegistry CODEC_REGISTRY = CodecRegistries.fromRegistries( + com.mongodb.MongoClient.getDefaultCodecRegistry(), + CodecRegistries.fromCodecs( + new ActionTypeCodec(), + new ObjectTypeCodec(), + new WorldPosCodec() +// new LogEntryCodec() + ), + fromProviders(PojoCodecProvider.builder().automatic(true).build()) + ); + private static final MongoClientSettings CLIENT_SETTINGS = MongoClientSettings.builder() + .applyConnectionString(new ConnectionString(MONGO_CONFIG.getAddress())) + .codecRegistry(CODEC_REGISTRY) + .build(); + + public LookupWorker(int id, BlockingQueue queue) { + this.queue = queue; + this.logger = Logger.getLogger(String.format("LookupWorker-%d", id)); + } + + public void disable() { + interrupt(); + running = false; + } + + @Override + public void run() { + try (final MongoClient mongoClient = MongoClients.create(CLIENT_SETTINGS)) { + final MongoDatabase db = mongoClient.getDatabase( + DatabaseUtil.MONGO_CONFIG.getDatabaseName() + ); + final MongoCollection collection = db.getCollection( + DatabaseUtil.MONGO_CONFIG.getLogCollectionName(), LogEntry.class + ); + long time; + while (running) { + LookupFilterWithCallback item = queue.take(); + LookupCallback callback = item.getCallback(); + AbstractLookupFilter filter = item.getFilter(); + + time = System.currentTimeMillis(); +// FindIterable find = filter.find( +// collection.find().sort(Sorts.descending("timestamp_millis")) +// ); + FindIterable find = collection.find();//.sort(Sorts.descending("timestamp_millis")); + time = System.currentTimeMillis() - time; + logger.info(String.format("Lookup finished in %d ms.", time)); + callback.onLookupFinishes(find); + } + } catch (InterruptedException e) { + logger.info("Interrupted. Quitting..."); + } + } +} diff --git a/src/main/java/com/keuin/blame/lookup/TestableFilter.java b/src/main/java/com/keuin/blame/lookup/TestableFilter.java new file mode 100644 index 0000000..8160234 --- /dev/null +++ b/src/main/java/com/keuin/blame/lookup/TestableFilter.java @@ -0,0 +1,14 @@ +package com.keuin.blame.lookup; + +import com.keuin.blame.data.LogEntry; +import com.keuin.blame.data.enums.ActionType; +import com.keuin.blame.data.enums.codec.LogEntryNames; +import com.mongodb.client.FindIterable; +import com.mongodb.client.model.Filters; + +public class TestableFilter extends AbstractLookupFilter { + @Override + protected FindIterable find(FindIterable iterable) { + return iterable.filter(Filters.eq(LogEntryNames.ACTION_TYPE, ActionType.NULL.getValue())); + } +} diff --git a/src/main/java/com/keuin/blame/util/DatabaseUtil.java b/src/main/java/com/keuin/blame/util/DatabaseUtil.java new file mode 100644 index 0000000..4b1d3d4 --- /dev/null +++ b/src/main/java/com/keuin/blame/util/DatabaseUtil.java @@ -0,0 +1,38 @@ +package com.keuin.blame.util; + +import com.keuin.blame.Blame; +import com.keuin.blame.config.MongoConfig; +import com.keuin.blame.data.enums.codec.ActionTypeCodec; +import com.keuin.blame.data.enums.codec.ObjectTypeCodec; +import com.keuin.blame.data.enums.codec.WorldPosCodec; +import com.mongodb.ConnectionString; +import com.mongodb.MongoClientSettings; +import org.bson.codecs.configuration.CodecRegistries; +import org.bson.codecs.configuration.CodecRegistry; +import org.bson.codecs.pojo.PojoCodecProvider; + +import static org.bson.codecs.configuration.CodecRegistries.fromProviders; + +public class DatabaseUtil { + +// static { +// BSON.addEncodingHook(ActionType.class, new ActionTypeTransformer()); +// BSON.addEncodingHook(ObjectType.class, new ObjectTypeTransformer()); +// } + + public static final MongoConfig MONGO_CONFIG = Blame.config.getMongoConfig(); + public static final CodecRegistry CODEC_REGISTRY = CodecRegistries.fromRegistries( + com.mongodb.MongoClient.getDefaultCodecRegistry(), + CodecRegistries.fromCodecs( + new ActionTypeCodec(), + new ObjectTypeCodec(), + new WorldPosCodec() +// new LogEntryCodec() + ), + fromProviders(PojoCodecProvider.builder().automatic(true).build()) + ); + public static final MongoClientSettings CLIENT_SETTINGS = MongoClientSettings.builder() + .applyConnectionString(new ConnectionString(MONGO_CONFIG.getAddress())) + .codecRegistry(CODEC_REGISTRY) + .build(); +} diff --git a/src/main/java/com/keuin/blame/util/PrettyUtil.java b/src/main/java/com/keuin/blame/util/PrettyUtil.java new file mode 100644 index 0000000..0a53b6e --- /dev/null +++ b/src/main/java/com/keuin/blame/util/PrettyUtil.java @@ -0,0 +1,10 @@ +package com.keuin.blame.util; + +import java.text.SimpleDateFormat; +import java.util.Date; + +public class PrettyUtil { + public static String timestampToString(long timeMillis) { + return (new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")).format(new Date(timeMillis)); + } +} diff --git a/src/main/java/com/keuin/blame/util/PrintUtil.java b/src/main/java/com/keuin/blame/util/PrintUtil.java index fdd1ea0..75b73d4 100644 --- a/src/main/java/com/keuin/blame/util/PrintUtil.java +++ b/src/main/java/com/keuin/blame/util/PrintUtil.java @@ -12,7 +12,7 @@ import net.minecraft.util.Formatting; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import java.util.UUID; +import static com.keuin.blame.util.UuidUtils.UUID_NULL; public final class PrintUtil implements ServerLifecycleEvents.ServerStarted { @@ -30,8 +30,6 @@ public final class PrintUtil implements ServerLifecycleEvents.ServerStarted { private static final String LOG_HEADING = "[Blame]"; private static PlayerManager playerManager = null; - private static final UUID UUID_NULL = UUID.fromString("00000000-0000-0000-0000-000000000000"); - // Used to handle server started event, to get player manager // You should put `ServerLifecycleEvents.SERVER_STARTED.register(PrintUtil.INSTANCE);` in the plugin init method public static final PrintUtil INSTANCE = new PrintUtil(); diff --git a/src/main/java/com/keuin/blame/util/UuidUtils.java b/src/main/java/com/keuin/blame/util/UuidUtils.java index b282129..fa6dce1 100644 --- a/src/main/java/com/keuin/blame/util/UuidUtils.java +++ b/src/main/java/com/keuin/blame/util/UuidUtils.java @@ -4,6 +4,9 @@ import java.nio.ByteBuffer; import java.util.UUID; public class UuidUtils { + + public static final UUID UUID_NULL = UUID.fromString("00000000-0000-0000-0000-000000000000"); + public static UUID asUuid(byte[] bytes) { ByteBuffer bb = ByteBuffer.wrap(bytes); long firstLong = bb.getLong(); -- cgit v1.2.3