reupload files

This commit is contained in:
HaHaWTH
2023-07-11 20:45:01 +08:00
parent b014da245d
commit 7e49e26735
465 changed files with 93823 additions and 0 deletions
@@ -0,0 +1,53 @@
package fr.xephi.authme.util;
/**
* A thread-safe interval counter, allows to detect if an event happens more than 'threshold' times
* in the given 'interval'.
*/
public class AtomicIntervalCounter {
private final int threshold;
private final int interval;
private int count;
private long lastInsert;
/**
* Constructs a new counter.
*
* @param threshold the threshold value of the counter.
* @param interval the counter interval in milliseconds.
*/
public AtomicIntervalCounter(int threshold, int interval) {
this.threshold = threshold;
this.interval = interval;
reset();
}
/**
* Resets the counter count.
*/
public synchronized void reset() {
count = 0;
lastInsert = 0;
}
/**
* Increments the counter and returns true if the current count has reached the threshold value
* in the given interval, this will also reset the count value.
*
* @return true if the count has reached the threshold value.
*/
public synchronized boolean handle() {
long now = System.currentTimeMillis();
if (now - lastInsert > interval) {
count = 1;
} else {
count++;
}
if (count > threshold) {
reset();
return true;
}
lastInsert = now;
return false;
}
}
@@ -0,0 +1,46 @@
package fr.xephi.authme.util;
import com.google.common.collect.Sets;
import java.util.Set;
/**
* Utilities for exceptions.
*/
public final class ExceptionUtils {
private ExceptionUtils() {
}
/**
* Returns the first throwable of the given {@code wantedThrowableType} by visiting the provided
* throwable and its causes recursively.
*
* @param wantedThrowableType the throwable type to find
* @param throwable the throwable to start with
* @param <T> the desired throwable subtype
* @return the first throwable found of the given type, or null if none found
*/
public static <T extends Throwable> T findThrowableInCause(Class<T> wantedThrowableType, Throwable throwable) {
Set<Throwable> visitedObjects = Sets.newIdentityHashSet();
Throwable currentThrowable = throwable;
while (currentThrowable != null && !visitedObjects.contains(currentThrowable)) {
if (wantedThrowableType.isInstance(currentThrowable)) {
return wantedThrowableType.cast(currentThrowable);
}
visitedObjects.add(currentThrowable);
currentThrowable = currentThrowable.getCause();
}
return null;
}
/**
* Format the information from a Throwable as string, retaining the type and its message.
*
* @param th the throwable to process
* @return string with the type of the Throwable and its message, e.g. "[IOException]: Could not open stream"
*/
public static String formatException(Throwable th) {
return "[" + th.getClass().getSimpleName() + "]: " + th.getMessage();
}
}
@@ -0,0 +1,173 @@
package fr.xephi.authme.util;
import com.google.common.io.Files;
import fr.xephi.authme.AuthMe;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.output.ConsoleLoggerFactory;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import static java.lang.String.format;
/**
* File utilities.
*/
public final class FileUtils {
private static final DateTimeFormatter CURRENT_DATE_STRING_FORMATTER =
DateTimeFormatter.ofPattern("yyyyMMdd_HHmm");
private static ConsoleLogger logger = ConsoleLoggerFactory.get(FileUtils.class);
// Utility class
private FileUtils() {
}
/**
* Copy a resource file (from the JAR) to the given file if it doesn't exist.
*
* @param destinationFile The file to check and copy to (outside of JAR)
* @param resourcePath Local path to the resource file (path to file within JAR)
*
* @return False if the file does not exist and could not be copied, true otherwise
*/
public static boolean copyFileFromResource(File destinationFile, String resourcePath) {
if (destinationFile.exists()) {
return true;
} else if (!createDirectory(destinationFile.getParentFile())) {
logger.warning("Cannot create parent directories for '" + destinationFile + "'");
return false;
}
try (InputStream is = getResourceFromJar(resourcePath)) {
if (is == null) {
logger.warning(format("Cannot copy resource '%s' to file '%s': cannot load resource",
resourcePath, destinationFile.getPath()));
} else {
java.nio.file.Files.copy(is, destinationFile.toPath());
return true;
}
} catch (IOException e) {
logger.logException(format("Cannot copy resource '%s' to file '%s':",
resourcePath, destinationFile.getPath()), e);
}
return false;
}
/**
* Creates the given directory.
*
* @param dir the directory to create
* @return true upon success, false otherwise
*/
public static boolean createDirectory(File dir) {
if (!dir.exists() && !dir.mkdirs()) {
logger.warning("Could not create directory '" + dir + "'");
return false;
}
return dir.isDirectory();
}
/**
* Returns a JAR file as stream. Returns null if it doesn't exist.
*
* @param path the local path (starting from resources project, e.g. "config.yml" for 'resources/config.yml')
* @return the stream if the file exists, or false otherwise
*/
public static InputStream getResourceFromJar(String path) {
// ClassLoader#getResourceAsStream does not deal with the '\' path separator: replace to '/'
final String normalizedPath = path.replace("\\", "/");
return AuthMe.class.getClassLoader().getResourceAsStream(normalizedPath);
}
/**
* Delete a given directory and all its content.
*
* @param directory The directory to remove
*/
public static void purgeDirectory(File directory) {
if (!directory.isDirectory()) {
return;
}
File[] files = directory.listFiles();
if (files == null) {
return;
}
for (File target : files) {
if (target.isDirectory()) {
purgeDirectory(target);
}
delete(target);
}
}
/**
* Delete the given file or directory and log a message if it was unsuccessful.
* Method is null safe and does nothing when null is passed.
*
* @param file the file to delete
*/
public static void delete(File file) {
if (file != null) {
boolean result = file.delete();
if (!result) {
logger.warning("Could not delete file '" + file + "'");
}
}
}
/**
* Creates the given file or throws an exception.
*
* @param file the file to create
*/
public static void create(File file) {
try {
boolean result = file.createNewFile();
if (!result) {
throw new IllegalStateException("Could not create file '" + file + "'");
}
} catch (IOException e) {
throw new IllegalStateException("Error while creating file '" + file + "'", e);
}
}
/**
* Construct a file path from the given elements, i.e. separate the given elements by the file separator.
*
* @param elements The elements to create a path with
*
* @return The created path
*/
public static String makePath(String... elements) {
return String.join(File.separator, elements);
}
/**
* Creates a textual representation of the current time (including minutes), e.g. useful for
* automatically generated backup files.
*
* @return string of the current time for use in file names
*/
public static String createCurrentTimeString() {
return LocalDateTime.now().format(CURRENT_DATE_STRING_FORMATTER);
}
/**
* Returns a path to a new file (which doesn't exist yet) with a timestamp in the name in the same
* folder as the given file and containing the given file's filename.
*
* @param file the file based on which a new file path should be created
* @return path to a file suitably named for storing a backup
*/
public static String createBackupFilePath(File file) {
String filename = "backup_" + Files.getNameWithoutExtension(file.getName())
+ "_" + createCurrentTimeString()
+ "." + Files.getFileExtension(file.getName());
return makePath(file.getParent(), filename);
}
}
@@ -0,0 +1,72 @@
package fr.xephi.authme.util;
import java.net.InetAddress;
import java.net.UnknownHostException;
/**
* Utility class about the InternetProtocol
*/
public final class InternetProtocolUtils {
// Utility class
private InternetProtocolUtils() {
}
/**
* Checks if the specified address is a private or loopback address
*
* @param address address to check
* @return true if the address is a local (site and link) or loopback address, false otherwise
*/
public static boolean isLocalAddress(String address) {
try {
InetAddress inetAddress = InetAddress.getByName(address);
// Examples: 127.0.0.1, localhost or [::1]
return isLoopbackAddress(address)
// Example: 10.0.0.0, 172.16.0.0, 192.168.0.0, fec0::/10 (deprecated)
// Ref: https://en.wikipedia.org/wiki/IP_address#Private_addresses
|| inetAddress.isSiteLocalAddress()
// Example: 169.254.0.0/16, fe80::/10
// Ref: https://en.wikipedia.org/wiki/IP_address#Address_autoconfiguration
|| inetAddress.isLinkLocalAddress()
// non deprecated unique site-local that java doesn't check yet -> fc00::/7
|| isIPv6UniqueSiteLocal(inetAddress);
} catch (UnknownHostException e) {
return false;
}
}
/**
* Checks if the specified address is a loopback address. This can be one of the following:
* <ul>
* <li>127.0.0.1</li>
* <li>localhost</li>
* <li>[::1]</li>
* </ul>
*
* @param address address to check
* @return true if the address is a loopback one
*/
public static boolean isLoopbackAddress(String address) {
try {
InetAddress inetAddress = InetAddress.getByName(address);
return inetAddress.isLoopbackAddress();
} catch (UnknownHostException e) {
return false;
}
}
private static boolean isLoopbackAddress(InetAddress address) {
return address.isLoopbackAddress();
}
private static boolean isIPv6UniqueSiteLocal(InetAddress address) {
// ref: https://en.wikipedia.org/wiki/Unique_local_address
// currently undefined but could be used in the near future fc00::/8
return (address.getAddress()[0] & 0xFF) == 0xFC
// in use for unique site-local fd00::/8
|| (address.getAddress()[0] & 0xFF) == 0xFD;
}
}
@@ -0,0 +1,34 @@
package fr.xephi.authme.util;
import org.bukkit.entity.Player;
/**
* Player utilities.
*/
public final class PlayerUtils {
// Utility class
private PlayerUtils() {
}
/**
* Returns the IP of the given player.
*
* @param player The player to return the IP address for
* @return The player's IP address
*/
public static String getPlayerIp(Player player) {
return player.getAddress().getAddress().getHostAddress();
}
/**
* Returns if the player is an NPC or not.
*
* @param player The player to check
* @return True if the player is an NPC, false otherwise
*/
public static boolean isNpc(Player player) {
return player.hasMetadata("NPC");
}
}
@@ -0,0 +1,75 @@
package fr.xephi.authme.util;
import java.security.SecureRandom;
import java.util.Random;
/**
* Utility for generating random strings.
*/
public final class RandomStringUtils {
private static final char[] CHARS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".toCharArray();
private static final Random RANDOM = new SecureRandom();
private static final int NUM_INDEX = 10;
private static final int LOWER_ALPHANUMERIC_INDEX = 36;
private static final int HEX_MAX_INDEX = 16;
// Utility class
private RandomStringUtils() {
}
/**
* Generate a string of the given length consisting of random characters within the range [0-9a-z].
*
* @param length The length of the random string to generate
* @return The random string
*/
public static String generate(int length) {
return generateString(length, LOWER_ALPHANUMERIC_INDEX);
}
/**
* Generate a random hexadecimal string of the given length. In other words, the generated string
* contains characters only within the range [0-9a-f].
*
* @param length The length of the random string to generate
* @return The random hexadecimal string
*/
public static String generateHex(int length) {
return generateString(length, HEX_MAX_INDEX);
}
/**
* Generate a random numbers string of the given length. In other words, the generated string
* contains characters only within the range [0-9].
*
* @param length The length of the random string to generate
* @return The random numbers string
*/
public static String generateNum(int length) {
return generateString(length, NUM_INDEX);
}
/**
* Generate a random string with digits and lowercase and uppercase letters. The result of this
* method matches the pattern [0-9a-zA-Z].
*
* @param length The length of the random string to generate
* @return The random string
*/
public static String generateLowerUpper(int length) {
return generateString(length, CHARS.length);
}
private static String generateString(int length, int maxIndex) {
if (length < 0) {
throw new IllegalArgumentException("Length must be positive but was " + length);
}
StringBuilder sb = new StringBuilder(length);
for (int i = 0; i < length; ++i) {
sb.append(CHARS[RANDOM.nextInt(maxIndex)]);
}
return sb.toString();
}
}
@@ -0,0 +1,80 @@
package fr.xephi.authme.util;
import net.ricecode.similarity.LevenshteinDistanceStrategy;
import net.ricecode.similarity.StringSimilarityService;
import net.ricecode.similarity.StringSimilarityServiceImpl;
/**
* Utility class for String operations.
*/
public final class StringUtils {
// Utility class
private StringUtils() {
}
/**
* Calculates the difference of two strings.
*
* @param first first string
* @param second second string
* @return the difference value
*/
public static double getDifference(String first, String second) {
// Make sure the strings are valid.
if (first == null || second == null) {
return 1.0;
}
// Create a string similarity service instance, to allow comparison
StringSimilarityService service = new StringSimilarityServiceImpl(new LevenshteinDistanceStrategy());
// Determine the difference value, return the result
return Math.abs(service.score(first, second) - 1.0);
}
/**
* Returns whether the given string contains any of the provided elements.
*
* @param str the string to analyze
* @param pieces the items to check the string for
* @return true if the string contains at least one of the items
*/
public static boolean containsAny(String str, Iterable<String> pieces) {
if (str == null) {
return false;
}
for (String piece : pieces) {
if (piece != null && str.contains(piece)) {
return true;
}
}
return false;
}
/**
* Null-safe method for checking whether a string is empty. Note that the string
* is trimmed, so this method also considers a string with whitespace as empty.
*
* @param str the string to verify
* @return true if the string is empty, false otherwise
*/
public static boolean isBlank(String str) {
return str == null || str.trim().isEmpty();
}
/**
* Checks that the given needle is in the middle of the haystack, i.e. that the haystack
* contains the needle and that it is not at the very start or end.
*
* @param needle the needle to search for
* @param haystack the haystack to search in
* @return true if the needle is in the middle of the word, false otherwise
*/
// Note ljacqu 20170314: `needle` is restricted to char type intentionally because something like
// isInsideString("11", "2211") would unexpectedly return true...
public static boolean isInsideString(char needle, String haystack) {
int index = haystack.indexOf(needle);
return index > 0 && index < haystack.length() - 1;
}
}
@@ -0,0 +1,55 @@
package fr.xephi.authme.util;
import fr.xephi.authme.ConsoleLogger;
import org.bukkit.Location;
import org.bukkit.entity.Player;
import javax.inject.Inject;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.logging.Level;
import java.util.logging.Logger;
//
//public class TeleportUtils {
// public static void teleport(Player player, Location location) {
// try {
// Class<?> paperClass = Class.forName("com.destroystokyo.paper.PaperConfig");
// // Paper API is loaded, use teleportAsync
// Method teleportAsyncMethod = player.getClass().getMethod("teleportAsync", Location.class);
// teleportAsyncMethod.setAccessible(true);
// teleportAsyncMethod.invoke(player, location);
// } catch (ClassNotFoundException | InvocationTargetException | NoSuchMethodException | IllegalAccessException e) {
// // Paper API is not loaded, use normal teleport
// player.teleport(location);
// }
// }
//}
public class TeleportUtils {
private static Method teleportAsyncMethod;
static {
try {//Detect Paper class
Class<?> paperClass = Class.forName("com.destroystokyo.paper.PaperConfig");
teleportAsyncMethod = Player.class.getMethod("teleportAsync", Location.class);
// if detected,use teleportAsync()
} catch (ClassNotFoundException | NoSuchMethodException e) {
teleportAsyncMethod = null;
//if not, set method to null
}
}
public static void teleport(Player player, Location location) {
if (teleportAsyncMethod != null) {
try {
teleportAsyncMethod.setAccessible(true);
teleportAsyncMethod.invoke(player, location);
} catch (IllegalAccessException | InvocationTargetException e) {
player.teleport(location);
}
} else {
player.teleport(location);
}
}
}
@@ -0,0 +1,110 @@
package fr.xephi.authme.util;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.output.ConsoleLoggerFactory;
import org.bukkit.ChatColor;
import org.bukkit.command.CommandSender;
import org.bukkit.command.ConsoleCommandSender;
import java.util.Collection;
import java.util.regex.Pattern;
/**
* Utility class for various operations used in the codebase.
*/
public final class Utils {
/** Number of milliseconds in a minute. */
public static final long MILLIS_PER_MINUTE = 60_000L;
private static ConsoleLogger logger = ConsoleLoggerFactory.get(Utils.class);
// Utility class
private Utils() {
}
/**
* Compile Pattern sneaky without throwing Exception.
*
* @param pattern pattern string to compile
*
* @return the given regex compiled into Pattern object.
*/
public static Pattern safePatternCompile(String pattern) {
try {
return Pattern.compile(pattern);
} catch (Exception e) {
logger.warning("Failed to compile pattern '" + pattern + "' - defaulting to allowing everything");
return Pattern.compile(".*?");
}
}
/**
* Returns whether the class exists in the current class loader.
*
* @param className the class name to check
*
* @return true if the class is loaded, false otherwise
*/
public static boolean isClassLoaded(String className) {
try {
Class.forName(className);
return true;
} catch (ClassNotFoundException e) {
return false;
}
}
/**
* Sends a message to the given sender (null safe), and logs the message to the console.
* This method is aware that the command sender might be the console sender and avoids
* displaying the message twice in this case.
*
* @param sender the sender to inform
* @param message the message to log and send
*/
public static void logAndSendMessage(CommandSender sender, String message) {
logger.info(message);
// Make sure sender is not console user, which will see the message from ConsoleLogger already
if (sender != null && !(sender instanceof ConsoleCommandSender)) {
sender.sendMessage(message);
}
}
/**
* Sends a warning to the given sender (null safe), and logs the warning to the console.
* This method is aware that the command sender might be the console sender and avoids
* displaying the message twice in this case.
*
* @param sender the sender to inform
* @param message the warning to log and send
*/
public static void logAndSendWarning(CommandSender sender, String message) {
logger.warning(message);
// Make sure sender is not console user, which will see the message from ConsoleLogger already
if (sender != null && !(sender instanceof ConsoleCommandSender)) {
sender.sendMessage(ChatColor.RED + message);
}
}
/**
* Null-safe way to check whether a collection is empty or not.
*
* @param coll The collection to verify
* @return True if the collection is null or empty, false otherwise
*/
public static boolean isCollectionEmpty(Collection<?> coll) {
return coll == null || coll.isEmpty();
}
/**
* Returns whether the given email is empty or equal to the standard "undefined" email address.
*
* @param email the email to check
*
* @return true if the email is empty
*/
public static boolean isEmailEmpty(String email) {
return StringUtils.isBlank(email) || "your@email.com".equalsIgnoreCase(email);
}
}
@@ -0,0 +1,27 @@
package fr.xephi.authme.util;
import java.util.UUID;
/**
* Utility class for various operations on UUID.
*/
public final class UuidUtils {
// Utility class
private UuidUtils() {
}
/**
* Returns the given string as an UUID or null.
*
* @param string the uuid to parse
* @return parsed UUID if succeeded or null
*/
public static UUID parseUuidSafely(String string) {
try {
return string == null ? null : UUID.fromString(string);
} catch (IllegalArgumentException ex) {
return null;
}
}
}
@@ -0,0 +1,72 @@
package fr.xephi.authme.util.expiring;
import java.util.concurrent.TimeUnit;
/**
* Represents a duration in time, defined by a time unit and a duration.
*/
public class Duration {
private final long duration;
private final TimeUnit unit;
/**
* Constructor.
*
* @param duration the duration
* @param unit the time unit in which {@code duration} is expressed
*/
public Duration(long duration, TimeUnit unit) {
this.duration = duration;
this.unit = unit;
}
/**
* Creates a Duration object for the given duration and unit in the most suitable time unit.
* For example, {@code createWithSuitableUnit(120, TimeUnit.SECONDS)} will return a Duration
* object of 2 minutes.
* <p>
* This method only considers the time units days, hours, minutes, and seconds for the objects
* it creates. Conversion is done with {@link TimeUnit#convert} and so always rounds the
* results down.
* <p>
* Further examples:
* <code>createWithSuitableUnit(299, TimeUnit.MINUTES); // 4 hours</code>
* <code>createWithSuitableUnit(700, TimeUnit.MILLISECONDS); // 0 seconds</code>
*
* @param sourceDuration the duration
* @param sourceUnit the time unit the duration is expressed in
* @return Duration object using the most suitable time unit
*/
public static Duration createWithSuitableUnit(long sourceDuration, TimeUnit sourceUnit) {
long durationMillis = Math.abs(TimeUnit.MILLISECONDS.convert(sourceDuration, sourceUnit));
TimeUnit targetUnit;
if (durationMillis > 1000L * 60L * 60L * 24L) {
targetUnit = TimeUnit.DAYS;
} else if (durationMillis > 1000L * 60L * 60L) {
targetUnit = TimeUnit.HOURS;
} else if (durationMillis > 1000L * 60L) {
targetUnit = TimeUnit.MINUTES;
} else {
targetUnit = TimeUnit.SECONDS;
}
long durationInTargetUnit = targetUnit.convert(sourceDuration, sourceUnit);
return new Duration(durationInTargetUnit, targetUnit);
}
/**
* @return the duration
*/
public long getDuration() {
return duration;
}
/**
* @return the time unit in which the duration is expressed
*/
public TimeUnit getTimeUnit() {
return unit;
}
}
@@ -0,0 +1,138 @@
package fr.xephi.authme.util.expiring;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
/**
* Map with expiring entries. Following a configured amount of time after
* an entry has been inserted, the map will act as if the entry does not
* exist.
* <p>
* Time starts counting directly after insertion. Inserting a new entry with
* a key that already has a value will "reset" the expiration. Although the
* expiration can be redefined later on, only entries which are inserted
* afterwards will use the new expiration.
* <p>
* An expiration of {@code <= 0} will make the map expire all entries
* immediately after insertion. Note that the map does not remove expired
* entries automatically; this is only done when calling
* {@link #removeExpiredEntries()}.
*
* @param <K> the key type
* @param <V> the value type
*/
public class ExpiringMap<K, V> {
private final Map<K, ExpiringEntry<V>> entries = new ConcurrentHashMap<>();
private long expirationMillis;
/**
* Constructor.
*
* @param duration the duration of time after which entries expire
* @param unit the time unit in which {@code duration} is expressed
*/
public ExpiringMap(long duration, TimeUnit unit) {
setExpiration(duration, unit);
}
/**
* Returns the value associated with the given key,
* if available and not expired.
*
* @param key the key to look up
* @return the associated value, or {@code null} if not available
*/
public V get(K key) {
ExpiringEntry<V> value = entries.get(key);
if (value == null) {
return null;
} else if (System.currentTimeMillis() > value.getExpiration()) {
entries.remove(key);
return null;
}
return value.getValue();
}
/**
* Inserts a value for the given key. Overwrites a previous value
* for the key if it exists.
*
* @param key the key to insert a value for
* @param value the value to insert
*/
public void put(K key, V value) {
long expiration = System.currentTimeMillis() + expirationMillis;
entries.put(key, new ExpiringEntry<>(value, expiration));
}
/**
* Removes the value for the given key, if available.
*
* @param key the key to remove the value for
*/
public void remove(K key) {
entries.remove(key);
}
/**
* Removes all entries which have expired from the internal structure.
*/
public void removeExpiredEntries() {
entries.entrySet().removeIf(entry -> System.currentTimeMillis() > entry.getValue().getExpiration());
}
/**
* Sets a new expiration duration. Note that already present entries
* will still make use of the old expiration.
*
* @param duration the duration of time after which entries expire
* @param unit the time unit in which {@code duration} is expressed
*/
public void setExpiration(long duration, TimeUnit unit) {
this.expirationMillis = unit.toMillis(duration);
}
/**
* Returns whether this map is empty. This reflects the state of the
* internal map, which may contain expired entries only. The result
* may change after running {@link #removeExpiredEntries()}.
*
* @return true if map is really empty, false otherwise
*/
public boolean isEmpty() {
return entries.isEmpty();
}
/**
* @return the internal map
*/
protected Map<K, ExpiringEntry<V>> getEntries() {
return entries;
}
/**
* Class holding a value paired with an expiration timestamp.
*
* @param <V> the value type
*/
protected static final class ExpiringEntry<V> {
private final V value;
private final long expiration;
ExpiringEntry(V value, long expiration) {
this.value = value;
this.expiration = expiration;
}
V getValue() {
return value;
}
long getExpiration() {
return expiration;
}
}
}
@@ -0,0 +1,125 @@
package fr.xephi.authme.util.expiring;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
/**
* Set whose entries expire after a configurable amount of time. Once an entry
* has expired, the set will act as if the entry no longer exists. Time starts
* counting after the entry has been inserted.
* <p>
* Internally, expired entries are not guaranteed to be cleared automatically.
* A cleanup of all expired entries may be triggered with
* {@link #removeExpiredEntries()}. Adding an entry that is already present
* effectively resets its expiration.
*
* @param <E> the type of the entries
*/
public class ExpiringSet<E> {
private Map<E, Long> entries = new ConcurrentHashMap<>();
private long expirationMillis;
/**
* Constructor.
*
* @param duration the duration of time after which entries expire
* @param unit the time unit in which {@code duration} is expressed
*/
public ExpiringSet(long duration, TimeUnit unit) {
setExpiration(duration, unit);
}
/**
* Adds an entry to the set.
*
* @param entry the entry to add
*/
public void add(E entry) {
entries.put(entry, System.currentTimeMillis() + expirationMillis);
}
/**
* Returns whether this set contains the given entry, if it hasn't expired.
*
* @param entry the entry to check
* @return true if the entry is present and not expired, false otherwise
*/
public boolean contains(E entry) {
Long expiration = entries.get(entry);
if (expiration == null) {
return false;
} else if (expiration > System.currentTimeMillis()) {
return true;
} else {
entries.remove(entry);
return false;
}
}
/**
* Removes the given entry from the set (if present).
*
* @param entry the entry to remove
*/
public void remove(E entry) {
entries.remove(entry);
}
/**
* Removes all entries from the set.
*/
public void clear() {
entries.clear();
}
/**
* Removes all entries which have expired from the internal structure.
*/
public void removeExpiredEntries() {
entries.entrySet().removeIf(entry -> System.currentTimeMillis() > entry.getValue());
}
/**
* Returns the duration of the entry until it expires (provided it is not removed or re-added).
* If the entry does not exist, a duration of -1 seconds is returned.
*
* @param entry the entry whose duration before it expires should be returned
* @return duration the entry will remain in the set (if there are not modifications)
*/
public Duration getExpiration(E entry) {
Long expiration = entries.get(entry);
if (expiration == null) {
return new Duration(-1, TimeUnit.SECONDS);
}
long stillPresentMillis = expiration - System.currentTimeMillis();
if (stillPresentMillis < 0) {
entries.remove(entry);
return new Duration(-1, TimeUnit.SECONDS);
}
return Duration.createWithSuitableUnit(stillPresentMillis, TimeUnit.MILLISECONDS);
}
/**
* Sets a new expiration duration. Note that already present entries
* will still make use of the old expiration.
*
* @param duration the duration of time after which entries expire
* @param unit the time unit in which {@code duration} is expressed
*/
public void setExpiration(long duration, TimeUnit unit) {
this.expirationMillis = unit.toMillis(duration);
}
/**
* Returns whether this map is empty. This reflects the state of the
* internal map, which may contain expired entries only. The result
* may change after running {@link #removeExpiredEntries()}.
*
* @return true if map is really empty, false otherwise
*/
public boolean isEmpty() {
return entries.isEmpty();
}
}
@@ -0,0 +1,70 @@
package fr.xephi.authme.util.expiring;
import java.util.concurrent.TimeUnit;
/**
* Keeps a count per key which expires after a configurable amount of time.
* <p>
* Once the expiration of an entry has been reached, the counter resets
* to 0. The counter returns 0 rather than {@code null} for any given key.
*
* @param <K> the type of the key
*/
public class TimedCounter<K> extends ExpiringMap<K, Integer> {
/**
* Constructor.
*
* @param duration the duration of time after which entries expire
* @param unit the time unit in which {@code duration} is expressed
*/
public TimedCounter(long duration, TimeUnit unit) {
super(duration, unit);
}
@Override
public Integer get(K key) {
Integer value = super.get(key);
return value == null ? 0 : value;
}
/**
* Increments the value stored for the provided key.
*
* @param key the key to increment the counter for
*/
public void increment(K key) {
put(key, get(key) + 1);
}
/**
* Decrements the value stored for the provided key.
* This method will NOT update the expiration.
*
* @param key the key to increment the counter for
*/
public void decrement(K key) {
ExpiringEntry<Integer> e = getEntries().get(key);
if (e != null) {
if (e.getValue() <= 0) {
remove(key);
} else {
getEntries().put(key, new ExpiringEntry<>(e.getValue() - 1, e.getExpiration()));
}
}
}
/**
* Calculates the total of all non-expired entries in this counter.
*
* @return the total of all valid entries
*/
public int total() {
long currentTime = System.currentTimeMillis();
return getEntries().values().stream()
.filter(entry -> currentTime <= entry.getExpiration())
.map(ExpiringEntry::getValue)
.reduce(0, Integer::sum);
}
}
@@ -0,0 +1,35 @@
package fr.xephi.authme.util.lazytags;
import java.util.function.Function;
/**
* Replaceable tag whose value depends on an argument.
*
* @param <A> the argument type
*/
public class DependentTag<A> implements Tag<A> {
private final String name;
private final Function<A, String> replacementFunction;
/**
* Constructor.
*
* @param name the tag (placeholder) that will be replaced
* @param replacementFunction the function producing the replacement
*/
public DependentTag(String name, Function<A, String> replacementFunction) {
this.name = name;
this.replacementFunction = replacementFunction;
}
@Override
public String getName() {
return name;
}
@Override
public String getValue(A argument) {
return replacementFunction.apply(argument);
}
}
@@ -0,0 +1,29 @@
package fr.xephi.authme.util.lazytags;
import java.util.function.Supplier;
/**
* Tag to be replaced that does not depend on an argument.
*
* @param <A> type of the argument (not used in this implementation)
*/
public class SimpleTag<A> implements Tag<A> {
private final String name;
private final Supplier<String> replacementFunction;
public SimpleTag(String name, Supplier<String> replacementFunction) {
this.name = name;
this.replacementFunction = replacementFunction;
}
@Override
public String getName() {
return name;
}
@Override
public String getValue(A argument) {
return replacementFunction.get();
}
}
@@ -0,0 +1,22 @@
package fr.xephi.authme.util.lazytags;
/**
* Represents a tag in a text to be replaced with a value (which may depend on some argument).
*
* @param <A> argument type the replacement may depend on
*/
public interface Tag<A> {
/**
* @return the tag to replace
*/
String getName();
/**
* Returns the value to replace the tag with for the given argument.
*
* @param argument the argument to evaluate the replacement for
* @return the replacement
*/
String getValue(A argument);
}
@@ -0,0 +1,21 @@
package fr.xephi.authme.util.lazytags;
import java.util.function.Function;
import java.util.function.Supplier;
/**
* Utility class for creating tags.
*/
public final class TagBuilder {
private TagBuilder() {
}
public static <A> Tag<A> createTag(String name, Function<A, String> replacementFunction) {
return new DependentTag<>(name, replacementFunction);
}
public static <A> Tag<A> createTag(String name, Supplier<String> replacementFunction) {
return new SimpleTag<>(name, replacementFunction);
}
}
@@ -0,0 +1,102 @@
package fr.xephi.authme.util.lazytags;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import java.util.stream.Collectors;
/**
* Replaces tags lazily by first determining which tags are being used
* and only applying those replacements afterwards.
*
* @param <A> the argument type
*/
public final class TagReplacer<A> {
private final List<Tag<A>> tags;
private final Collection<String> messages;
/**
* Private constructor. Use {@link #newReplacer(Collection, Collection)}.
*
* @param tags the tags that are being used in the messages
* @param messages the messages
*/
private TagReplacer(List<Tag<A>> tags, Collection<String> messages) {
this.tags = tags;
this.messages = messages;
}
/**
* Creates a new instance of this class, which will provide the given
* messages adapted with the provided tags.
*
* @param allTags all available tags
* @param messages the messages to use
* @param <A> the argument type
* @return new tag replacer instance
*/
public static <A> TagReplacer<A> newReplacer(Collection<Tag<A>> allTags, Collection<String> messages) {
List<Tag<A>> usedTags = determineUsedTags(allTags, messages);
return new TagReplacer<>(usedTags, messages);
}
/**
* Returns the messages with the tags applied for the given argument.
*
* @param argument the argument to get the messages for
* @return the adapted messages
*/
public List<String> getAdaptedMessages(A argument) {
// Note ljacqu 20170121: Using a Map might seem more natural here but we avoid doing so for performance
// Although the performance gain here is probably minimal...
List<TagValue> tagValues = new LinkedList<>();
for (Tag<A> tag : tags) {
tagValues.add(new TagValue(tag.getName(), tag.getValue(argument)));
}
List<String> adaptedMessages = new LinkedList<>();
for (String line : messages) {
String adaptedLine = line;
for (TagValue tagValue : tagValues) {
adaptedLine = adaptedLine.replace(tagValue.tag, tagValue.value);
}
adaptedMessages.add(adaptedLine);
}
return adaptedMessages;
}
/**
* Determines which tags are used somewhere in the given list of messages.
*
* @param allTags all available tags
* @param messages the messages
* @param <A> argument type
* @return tags used at least once
*/
private static <A> List<Tag<A>> determineUsedTags(Collection<Tag<A>> allTags, Collection<String> messages) {
return allTags.stream()
.filter(tag -> messages.stream().anyMatch(msg -> msg.contains(tag.getName())))
.collect(Collectors.toList());
}
/** (Tag, value) pair. */
private static final class TagValue {
/** The tag to replace. */
private final String tag;
/** The value to replace with. */
private final String value;
TagValue(String tag, String value) {
this.tag = tag;
this.value = value;
}
@Override
public String toString() {
return "TagValue[tag='" + tag + "', value='" + value + "']";
}
}
}
@@ -0,0 +1,61 @@
package fr.xephi.authme.util.lazytags;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* Applies tags lazily to the String property of an item. This class wraps
* a {@link TagReplacer} with the extraction of the String property and
* the creation of new items with the adapted string property.
*
* @param <T> the item type
* @param <A> the argument type to evaluate the replacements
*/
public class WrappedTagReplacer<T, A> {
private final Collection<T> items;
private final BiFunction<T, String, ? extends T> itemCreator;
private final TagReplacer<A> tagReplacer;
/**
* Constructor.
*
* @param allTags all available tags
* @param items the items to apply the replacements on
* @param stringGetter getter of the String property to adapt on the items
* @param itemCreator a function taking (T, String): the original item and the adapted String, returning a new item
*/
public WrappedTagReplacer(Collection<Tag<A>> allTags,
Collection<T> items,
Function<? super T, String> stringGetter,
BiFunction<T, String, ? extends T> itemCreator) {
this.items = items;
this.itemCreator = itemCreator;
List<String> stringItems = items.stream().map(stringGetter).collect(Collectors.toList());
tagReplacer = TagReplacer.newReplacer(allTags, stringItems);
}
/**
* Creates adapted items for the given argument.
*
* @param argument the argument to adapt the items for
* @return the adapted items
*/
public List<T> getAdaptedItems(A argument) {
List<String> adaptedStrings = tagReplacer.getAdaptedMessages(argument);
List<T> adaptedItems = new LinkedList<>();
Iterator<T> originalItemsIter = items.iterator();
Iterator<String> newStringsIter = adaptedStrings.iterator();
while (originalItemsIter.hasNext() && newStringsIter.hasNext()) {
adaptedItems.add(itemCreator.apply(originalItemsIter.next(), newStringsIter.next()));
}
return adaptedItems;
}
}