diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/collections/ExpireHashMap.java b/ProtocolLib/src/main/java/com/comphenix/protocol/collections/ExpireHashMap.java new file mode 100644 index 00000000..04e83313 --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/collections/ExpireHashMap.java @@ -0,0 +1,246 @@ +package com.comphenix.protocol.collections; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.PriorityQueue; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +import com.google.common.base.Function; +import com.google.common.base.Objects; +import com.google.common.base.Preconditions; +import com.google.common.base.Ticker; +import com.google.common.collect.Maps; +import com.google.common.primitives.Longs; + +/** + * Represents a hash map where each association may expire after a given time has elapsed. + *
+ * Note that replaced key-value associations are only collected once the original expiration time has elapsed.
+ *
+ * @author Kristian Stangeland
+ *
+ * @param
+ * This operation requires a linear scan of the current entries in the map.
+ */
+ public void collect() {
+ // First evict what we can
+ evictExpired();
+
+ // Recreate the eviction queue - this is faster than removing entries in the old queue
+ expireQueue.clear();
+ expireQueue.addAll(keyLookup.values());
+ }
+
+ /**
+ * Clear all the entries in the current map.
+ */
+ public void clear() {
+ keyLookup.clear();
+ expireQueue.clear();
+ }
+
+ /**
+ * Evict any expired entries in the map.
+ *
+ * This is called automatically by any of the read or write operations.
+ */
+ protected void evictExpired() {
+ long currentTime = ticker.read();
+
+ // Remove expired entries
+ while (expireQueue.size() > 0 && expireQueue.peek().expireTime <= currentTime) {
+ ExpireEntry entry = expireQueue.poll();
+
+ if (entry == keyLookup.get(entry.expireKey)) {
+ keyLookup.remove(entry.expireKey);
+ }
+ }
+ }
+
+ @Override
+ public String toString() {
+ return valueView.toString();
+ }
+}
+
\ No newline at end of file
diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/error/DetailedErrorReporter.java b/ProtocolLib/src/main/java/com/comphenix/protocol/error/DetailedErrorReporter.java
index 34849632..02acda82 100644
--- a/ProtocolLib/src/main/java/com/comphenix/protocol/error/DetailedErrorReporter.java
+++ b/ProtocolLib/src/main/java/com/comphenix/protocol/error/DetailedErrorReporter.java
@@ -25,6 +25,7 @@ import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;
import java.util.logging.Logger;
@@ -34,6 +35,7 @@ import org.apache.commons.lang.builder.ToStringStyle;
import org.bukkit.Bukkit;
import org.bukkit.plugin.Plugin;
+import com.comphenix.protocol.collections.ExpireHashMap;
import com.comphenix.protocol.error.Report.ReportBuilder;
import com.comphenix.protocol.events.PacketAdapter;
import com.comphenix.protocol.reflect.PrettyPrinter;
@@ -83,7 +85,11 @@ public class DetailedErrorReporter implements ErrorReporter {
// Map of global objects
protected Map
+ * The default implementation will check for rate limits.
+ * @param report - the report to check.
+ * @return TRUE if we should print it, FALSE otherwise.
+ */
+ protected boolean canReport(Report report) {
+ long rateLimit = report.getRateLimit();
+
+ // Check for rate limit
+ if (rateLimit > 0) {
+ synchronized (rateLock) {
+ if (rateLimited.containsKey(report)) {
+ return false;
+ }
+ rateLimited.put(report, true, rateLimit, TimeUnit.NANOSECONDS);
+ }
+ }
+ return true;
+ }
+
private void reportLevel(Level level, Object sender, Report report) {
String message = "[" + pluginName + "] [" + getSenderName(sender) + "] " + report.getReportMessage();
@@ -294,6 +322,11 @@ public class DetailedErrorReporter implements ErrorReporter {
}
}
+ // Secondary rate limit
+ if (!canReport(report)) {
+ return;
+ }
+
StringWriter text = new StringWriter();
PrintWriter writer = new PrintWriter(text);
diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/error/PluginContext.java b/ProtocolLib/src/main/java/com/comphenix/protocol/error/PluginContext.java
new file mode 100644
index 00000000..3b0cba40
--- /dev/null
+++ b/ProtocolLib/src/main/java/com/comphenix/protocol/error/PluginContext.java
@@ -0,0 +1,106 @@
+package com.comphenix.protocol.error;
+
+import java.io.File;
+import java.io.UnsupportedEncodingException;
+import java.net.URLDecoder;
+import java.security.CodeSource;
+
+import org.bukkit.Bukkit;
+import org.bukkit.plugin.Plugin;
+
+import com.google.common.base.Preconditions;
+
+public final class PluginContext {
+ // Determine plugin folder
+ private static File pluginFolder;
+
+ private PluginContext() {
+ // Not constructable
+ }
+
+ /**
+ * Retrieve the name of the plugin that called the last method(s) in the exception.
+ * @param ex - the exception.
+ * @return The name of the plugin, or NULL.
+ */
+ public static String getPluginCaller(Exception ex) {
+ StackTraceElement[] elements = ex.getStackTrace();
+ String current = getPluginName(elements[0]);
+
+ for (int i = 1; i < elements.length; i++) {
+ String caller = getPluginName(elements[i]);
+
+ if (caller != null && !caller.equals(current)) {
+ return caller;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Lookup the plugin that this method invocation belongs to, and return its file name.
+ * @param element - the method invocation.
+ * @return Pluing name, or NULL if not found.
+ *
+ */
+ public static String getPluginName(StackTraceElement element) {
+ try {
+ CodeSource codeSource = Class.forName(element.getClassName()).getProtectionDomain().getCodeSource();
+
+ if (codeSource != null) {
+ String encoding = codeSource.getLocation().getPath();
+ File path = new File(URLDecoder.decode(encoding, "UTF-8"));
+
+ if (folderContains(getPluginFolder(), path)) {
+ return path.getName();
+ }
+ }
+ return null; // Cannot find it
+
+ } catch (UnsupportedEncodingException e) {
+ throw new RuntimeException("Cannot lookup plugin name.", e);
+ } catch (ClassNotFoundException e) {
+ throw new RuntimeException("Cannot lookup plugin name.", e);
+ }
+ }
+
+ /**
+ * Determine if a folder contains the given file.
+ * @param folder - the folder.
+ * @param file - the file.
+ * @return TRUE if it does, FALSE otherwise.
+ */
+ private static boolean folderContains(File folder, File file) {
+ Preconditions.checkNotNull(folder, "folder cannot be NULL");
+ Preconditions.checkNotNull(file, "file cannot be NULL");
+
+ // Get absolute versions
+ folder = folder.getAbsoluteFile();
+ file = file.getAbsoluteFile();
+
+ while (file != null) {
+ if (folder.equals(file))
+ return true;
+ file = file.getParentFile();
+ }
+ return false;
+ }
+
+ /**
+ * Retrieve the folder that contains every plugin on the server.
+ * @return Folder with every plugin.
+ */
+ private static File getPluginFolder() {
+ File folder = pluginFolder;
+
+ if (folder == null) {
+ Plugin[] plugins = Bukkit.getPluginManager().getPlugins();
+
+ if (plugins.length > 0) {
+ folder = plugins[0].getDataFolder().getParentFile();
+ pluginFolder = folder;
+ }
+ }
+ return folder;
+ }
+}
diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/error/Report.java b/ProtocolLib/src/main/java/com/comphenix/protocol/error/Report.java
index b408d3b8..3bbe2c38 100644
--- a/ProtocolLib/src/main/java/com/comphenix/protocol/error/Report.java
+++ b/ProtocolLib/src/main/java/com/comphenix/protocol/error/Report.java
@@ -1,5 +1,8 @@
package com.comphenix.protocol.error;
+import java.util.Arrays;
+import java.util.concurrent.TimeUnit;
+
import javax.annotation.Nullable;
/**
@@ -7,12 +10,15 @@ import javax.annotation.Nullable;
*
* @author Kristian
*/
-public class Report {
+public class Report {
private final ReportType type;
private final Throwable exception;
private final Object[] messageParameters;
private final Object[] callerParameters;
+ // Used to rate limit reports that are similar
+ private final long rateLimit;
+
/**
* Must be constructed through the factory method in Report.
*/
@@ -21,6 +27,7 @@ public class Report {
private Throwable exception;
private Object[] messageParameters;
private Object[] callerParameters;
+ private long rateLimit;
private ReportBuilder() {
// Don't allow
@@ -39,8 +46,8 @@ public class Report {
}
/**
- * Set the current exception that occured.
- * @param exception - exception that occured.
+ * Set the current exception that occurred.
+ * @param exception - exception that occurred.
* @return This builder, for chaining.
*/
public ReportBuilder error(@Nullable Throwable exception) {
@@ -68,12 +75,35 @@ public class Report {
return this;
}
+ /**
+ * Set the minimum number of nanoseconds to wait until a report of equal type and parameters
+ * is allowed to be printed again.
+ * @param rateLimit - number of nanoseconds, or 0 to disable. Cannot be negative.
+ * @return This builder, for chaining.
+ */
+ public ReportBuilder rateLimit(long rateLimit) {
+ if (rateLimit < 0)
+ throw new IllegalArgumentException("Rate limit cannot be less than zero.");
+ this.rateLimit = rateLimit;
+ return this;
+ }
+
+ /**
+ * Set the minimum time to wait until a report of equal type and parameters is allowed to be printed again.
+ * @param rateLimit - the time, or 0 to disable. Cannot be negative.
+ * @param rateUnit - the unit of the rate limit.
+ * @return This builder, for chaining.
+ */
+ public ReportBuilder rateLimit(long rateLimit, TimeUnit rateUnit) {
+ return rateLimit(TimeUnit.NANOSECONDS.convert(rateLimit, rateUnit));
+ }
+
/**
* Construct a new report with the provided input.
* @return A new report.
*/
public Report build() {
- return new Report(type, exception, messageParameters, callerParameters);
+ return new Report(type, exception, messageParameters, callerParameters, rateLimit);
}
}
@@ -85,7 +115,7 @@ public class Report {
public static ReportBuilder newBuilder(ReportType type) {
return new ReportBuilder().type(type);
}
-
+
/**
* Construct a new report with the given type and parameters.
* @param exception - exception that occured in the caller method.
@@ -93,13 +123,28 @@ public class Report {
* @param messageParameters - parameters used to construct the report message.
* @param callerParameters - parameters from the caller method.
*/
- protected Report(ReportType type, @Nullable Throwable exception, @Nullable Object[] messageParameters, @Nullable Object[] callerParameters) {
+ protected Report(ReportType type, @Nullable Throwable exception,
+ @Nullable Object[] messageParameters, @Nullable Object[] callerParameters) {
+ this(type, exception, messageParameters, callerParameters, 0);
+ }
+
+ /**
+ * Construct a new report with the given type and parameters.
+ * @param exception - exception that occurred in the caller method.
+ * @param type - the report type that will be used to construct the message.
+ * @param messageParameters - parameters used to construct the report message.
+ * @param callerParameters - parameters from the caller method.
+ * @param rateLimit - minimum number of nanoseconds to wait until a report of equal type and parameters is allowed to be printed again.
+ */
+ protected Report(ReportType type, @Nullable Throwable exception,
+ @Nullable Object[] messageParameters, @Nullable Object[] callerParameters, long rateLimit) {
if (type == null)
throw new IllegalArgumentException("type cannot be NULL.");
this.type = type;
this.exception = exception;
this.messageParameters = messageParameters;
this.callerParameters = callerParameters;
+ this.rateLimit = rateLimit;
}
/**
@@ -159,4 +204,37 @@ public class Report {
public boolean hasCallerParameters() {
return callerParameters != null && callerParameters.length > 0;
}
+
+ /**
+ * Retrieve desired minimum number of nanoseconds until a report of the same type and parameters should be reprinted.
+ *
+ * Note that this may be ignored or modified by the error reporter. Zero indicates no rate limit.
+ * @return The number of nanoseconds. Never negative.
+ */
+ public long getRateLimit() {
+ return rateLimit;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + Arrays.hashCode(callerParameters);
+ result = prime * result + Arrays.hashCode(messageParameters);
+ result = prime * result + ((type == null) ? 0 : type.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (obj instanceof Report) {
+ Report other = (Report) obj;
+ return type == other.type &&
+ Arrays.equals(callerParameters, other.callerParameters) &&
+ Arrays.equals(messageParameters, other.messageParameters);
+ }
+ return false;
+ }
}
diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/BukkitConverters.java b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/BukkitConverters.java
index 19d47e5e..07924998 100644
--- a/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/BukkitConverters.java
+++ b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/BukkitConverters.java
@@ -28,7 +28,6 @@ import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
-import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.World;
import org.bukkit.WorldType;
diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/WrappedGameProfile.java b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/WrappedGameProfile.java
index 3c332ddb..2da52be1 100644
--- a/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/WrappedGameProfile.java
+++ b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/WrappedGameProfile.java
@@ -1,28 +1,35 @@
package com.comphenix.protocol.wrappers;
import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+
+import net.minecraft.util.com.mojang.authlib.GameProfile;
+import net.minecraft.util.com.mojang.authlib.properties.Property;
-import org.apache.commons.lang.StringUtils;
import org.bukkit.OfflinePlayer;
import org.bukkit.entity.Player;
+import com.comphenix.protocol.ProtocolLibrary;
+import com.comphenix.protocol.error.PluginContext;
+import com.comphenix.protocol.error.Report;
+import com.comphenix.protocol.error.ReportType;
import com.comphenix.protocol.injector.BukkitUnwrapper;
import com.comphenix.protocol.reflect.accessors.Accessors;
import com.comphenix.protocol.reflect.accessors.ConstructorAccessor;
import com.comphenix.protocol.reflect.accessors.FieldAccessor;
import com.comphenix.protocol.utility.MinecraftReflection;
import com.comphenix.protocol.wrappers.collection.ConvertedMultimap;
+import com.google.common.base.Charsets;
import com.google.common.base.Objects;
import com.google.common.collect.Multimap;
-import net.minecraft.util.com.mojang.authlib.GameProfile;
-import net.minecraft.util.com.mojang.authlib.properties.Property;
-
/**
* Represents a wrapper for a game profile.
* @author Kristian
*/
public class WrappedGameProfile extends AbstractWrapper {
+ public static final ReportType REPORT_INVALID_UUID = new ReportType("Plugin %s created a profile with '%s' as an UUID.");
+
// Version 1.7.2 and 1.7.8 respectively
private static final ConstructorAccessor CREATE_STRING_STRING = Accessors.getConstructorAccessorOrNull(GameProfile.class, String.class, String.class);
private static final FieldAccessor GET_UUID_STRING = Accessors.getFieldAcccessorOrNull(GameProfile.class, "id", String.class);
@@ -82,21 +89,20 @@ public class WrappedGameProfile extends AbstractWrapper {
* Construct a new game profile with the given properties.
*
* Note that this constructor is very lenient when parsing UUIDs for backwards compatibility reasons.
- * Thus - "", " ", "0" and "0-0-0-0" are all equivalent to the the UUID "00000000-0000-0000-0000-000000000000".
+ * IDs that cannot be parsed as an UUID will be hashed and form a version 3 UUID instead.
+ *
+ * This method is deprecated for Minecraft 1.7.8 and above.
* @param id - the UUID of the player.
* @param name - the name of the player.
*/
+ @Deprecated
public WrappedGameProfile(String id, String name) {
super(GameProfile.class);
if (CREATE_STRING_STRING != null) {
setHandle(CREATE_STRING_STRING.invoke(id, name));
} else {
- try {
- setHandle(new GameProfile(parseUUID(id), name));
- } catch (IllegalArgumentException e) {
- throw new IllegalArgumentException("Cannot construct profile [" + id + ", " + name + "]", e);
- }
+ setHandle(new GameProfile(parseUUID(id), name));
}
}
@@ -134,33 +140,19 @@ public class WrappedGameProfile extends AbstractWrapper {
* @throws IllegalArgumentException If we cannot parse the text.
*/
private static UUID parseUUID(String id) {
- if (id == null)
- return null;
- // Interpret as zero
- if (StringUtils.isBlank(id))
- id = "0";
-
- int missing = 4 - StringUtils.countMatches(id, "-");
-
- // Lenient - add missing data
- if (missing > 0) {
- if (id.length() < 12) {
- id += StringUtils.repeat("-0", missing);
- } else if (id.length() >= 32) {
- StringBuilder builder = new StringBuilder(id);
- int position = 8; // Initial position
-
- while (missing > 0 && position < builder.length()) {
- builder.insert(position, "-");
- position += 5; // 4 in length, plus the hyphen
- missing--;
- }
- id = builder.toString();
- } else {
- throw new IllegalArgumentException("Invalid partial UUID: " + id);
- }
+ try {
+ return id != null ? UUID.fromString(id) : null;
+ } catch (IllegalArgumentException e) {
+ // Warn once every hour (per plugin)
+ ProtocolLibrary.getErrorReporter().reportWarning(
+ WrappedGameProfile.class,
+ Report.newBuilder(REPORT_INVALID_UUID).
+ rateLimit(1, TimeUnit.HOURS).
+ messageParam(PluginContext.getPluginCaller(new Exception()), id)
+ );
+
+ return UUID.nameUUIDFromBytes(id.getBytes(Charsets.UTF_8));
}
- return UUID.fromString(id);
}
/**