diff --git a/src/main/java/fr/xephi/authme/data/CaptchaManager.java b/src/main/java/fr/xephi/authme/data/CaptchaManager.java
index 7e83ec23c..b328d5450 100644
--- a/src/main/java/fr/xephi/authme/data/CaptchaManager.java
+++ b/src/main/java/fr/xephi/authme/data/CaptchaManager.java
@@ -5,7 +5,7 @@ import fr.xephi.authme.initialization.SettingsDependent;
import fr.xephi.authme.settings.Settings;
import fr.xephi.authme.settings.properties.SecuritySettings;
import fr.xephi.authme.util.RandomStringUtils;
-import fr.xephi.authme.util.TimedCounter;
+import fr.xephi.authme.util.expiring.TimedCounter;
import javax.inject.Inject;
import java.util.concurrent.ConcurrentHashMap;
diff --git a/src/main/java/fr/xephi/authme/data/SessionManager.java b/src/main/java/fr/xephi/authme/data/SessionManager.java
index 23da3afff..512941db6 100644
--- a/src/main/java/fr/xephi/authme/data/SessionManager.java
+++ b/src/main/java/fr/xephi/authme/data/SessionManager.java
@@ -5,7 +5,7 @@ import fr.xephi.authme.initialization.HasCleanup;
import fr.xephi.authme.initialization.SettingsDependent;
import fr.xephi.authme.settings.Settings;
import fr.xephi.authme.settings.properties.PluginSettings;
-import fr.xephi.authme.util.ExpiringSet;
+import fr.xephi.authme.util.expiring.ExpiringSet;
import javax.inject.Inject;
import java.util.concurrent.TimeUnit;
diff --git a/src/main/java/fr/xephi/authme/data/TempbanManager.java b/src/main/java/fr/xephi/authme/data/TempbanManager.java
index 07019a725..1d55fa8f5 100644
--- a/src/main/java/fr/xephi/authme/data/TempbanManager.java
+++ b/src/main/java/fr/xephi/authme/data/TempbanManager.java
@@ -7,8 +7,8 @@ import fr.xephi.authme.message.Messages;
import fr.xephi.authme.service.BukkitService;
import fr.xephi.authme.settings.Settings;
import fr.xephi.authme.settings.properties.SecuritySettings;
-import fr.xephi.authme.util.TimedCounter;
import fr.xephi.authme.util.PlayerUtils;
+import fr.xephi.authme.util.expiring.TimedCounter;
import org.bukkit.entity.Player;
import javax.inject.Inject;
diff --git a/src/main/java/fr/xephi/authme/service/RecoveryCodeService.java b/src/main/java/fr/xephi/authme/service/RecoveryCodeService.java
index fdc987a21..cae8aaa73 100644
--- a/src/main/java/fr/xephi/authme/service/RecoveryCodeService.java
+++ b/src/main/java/fr/xephi/authme/service/RecoveryCodeService.java
@@ -4,8 +4,8 @@ import fr.xephi.authme.initialization.HasCleanup;
import fr.xephi.authme.initialization.SettingsDependent;
import fr.xephi.authme.settings.Settings;
import fr.xephi.authme.settings.properties.SecuritySettings;
-import fr.xephi.authme.util.ExpiringMap;
import fr.xephi.authme.util.RandomStringUtils;
+import fr.xephi.authme.util.expiring.ExpiringMap;
import javax.inject.Inject;
import java.util.concurrent.TimeUnit;
diff --git a/src/main/java/fr/xephi/authme/util/Utils.java b/src/main/java/fr/xephi/authme/util/Utils.java
index b7628147b..e7ec1657a 100644
--- a/src/main/java/fr/xephi/authme/util/Utils.java
+++ b/src/main/java/fr/xephi/authme/util/Utils.java
@@ -3,6 +3,7 @@ package fr.xephi.authme.util;
import fr.xephi.authme.ConsoleLogger;
import java.util.Collection;
+import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
/**
@@ -70,4 +71,36 @@ public final class Utils {
return Runtime.getRuntime().availableProcessors();
}
+ public static Duration convertMillisToSuitableUnit(long duration) {
+ TimeUnit targetUnit;
+ if (duration > 1000L * 60L * 60L * 24L) {
+ targetUnit = TimeUnit.DAYS;
+ } else if (duration > 1000L * 60L * 60L) {
+ targetUnit = TimeUnit.HOURS;
+ } else if (duration > 1000L * 60L) {
+ targetUnit = TimeUnit.MINUTES;
+ } else if (duration > 1000L) {
+ targetUnit = TimeUnit.SECONDS;
+ } else {
+ targetUnit = TimeUnit.MILLISECONDS;
+ }
+
+ return new Duration(targetUnit, duration);
+ }
+
+ public static final class Duration {
+
+ private final long duration;
+ private final TimeUnit unit;
+
+ Duration(TimeUnit targetUnit, long durationMillis) {
+ this(targetUnit, durationMillis, TimeUnit.MILLISECONDS);
+ }
+
+ Duration(TimeUnit targetUnit, long sourceDuration, TimeUnit sourceUnit) {
+ this.duration = targetUnit.convert(sourceDuration, sourceUnit);
+ this.unit = targetUnit;
+ }
+ }
+
}
diff --git a/src/main/java/fr/xephi/authme/util/expiring/Duration.java b/src/main/java/fr/xephi/authme/util/expiring/Duration.java
new file mode 100644
index 000000000..ecc86299e
--- /dev/null
+++ b/src/main/java/fr/xephi/authme/util/expiring/Duration.java
@@ -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.
+ *
+ * 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.
+ *
+ * Further examples:
+ * createWithSuitableUnit(299, TimeUnit.MINUTES); // 4 hours
+ * createWithSuitableUnit(700, TimeUnit.MILLISECONDS); // 0 seconds
+ *
+ * @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;
+ }
+}
diff --git a/src/main/java/fr/xephi/authme/util/ExpiringMap.java b/src/main/java/fr/xephi/authme/util/expiring/ExpiringMap.java
similarity index 98%
rename from src/main/java/fr/xephi/authme/util/ExpiringMap.java
rename to src/main/java/fr/xephi/authme/util/expiring/ExpiringMap.java
index 519946f89..894b6486e 100644
--- a/src/main/java/fr/xephi/authme/util/ExpiringMap.java
+++ b/src/main/java/fr/xephi/authme/util/expiring/ExpiringMap.java
@@ -1,4 +1,4 @@
-package fr.xephi.authme.util;
+package fr.xephi.authme.util.expiring;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
diff --git a/src/main/java/fr/xephi/authme/util/ExpiringSet.java b/src/main/java/fr/xephi/authme/util/expiring/ExpiringSet.java
similarity index 65%
rename from src/main/java/fr/xephi/authme/util/ExpiringSet.java
rename to src/main/java/fr/xephi/authme/util/expiring/ExpiringSet.java
index 840d5911c..7e711673e 100644
--- a/src/main/java/fr/xephi/authme/util/ExpiringSet.java
+++ b/src/main/java/fr/xephi/authme/util/expiring/ExpiringSet.java
@@ -1,4 +1,4 @@
-package fr.xephi.authme.util;
+package fr.xephi.authme.util.expiring;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@@ -9,9 +9,10 @@ import java.util.concurrent.TimeUnit;
* has expired, the set will act as if the entry no longer exists. Time starts
* counting after the entry has been inserted.
*
- * Internally, expired entries are not cleared automatically. A cleanup can be
- * triggered with {@link #removeExpiredEntries()}. Adding an entry that is
- * already present effectively resets its expiration.
+ * 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 the type of the entries
*/
@@ -47,7 +48,14 @@ public class ExpiringSet {
*/
public boolean contains(E entry) {
Long expiration = entries.get(entry);
- return expiration != null && expiration > System.currentTimeMillis();
+ if (expiration == null) {
+ return false;
+ } else if (expiration > System.currentTimeMillis()) {
+ return true;
+ } else {
+ entries.remove(entry);
+ return false;
+ }
}
/**
@@ -73,6 +81,27 @@ public class ExpiringSet {
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, -1 is returned.
+ *
+ * @param entry the entry whose duration before it expires should be returned
+ * @param unit the unit in which to return the duration
+ * @return duration the entry will remain in the set (if there are not modifications)
+ */
+ public long getExpiration(E entry, TimeUnit unit) {
+ Long expiration = entries.get(entry);
+ if (expiration == null) {
+ return -1;
+ }
+ long stillPresentMillis = expiration - System.currentTimeMillis();
+ if (stillPresentMillis < 0) {
+ entries.remove(entry);
+ return -1;
+ }
+ return unit.convert(stillPresentMillis, TimeUnit.MILLISECONDS);
+ }
+
/**
* Sets a new expiration duration. Note that already present entries
* will still make use of the old expiration.
diff --git a/src/main/java/fr/xephi/authme/util/TimedCounter.java b/src/main/java/fr/xephi/authme/util/expiring/TimedCounter.java
similarity index 97%
rename from src/main/java/fr/xephi/authme/util/TimedCounter.java
rename to src/main/java/fr/xephi/authme/util/expiring/TimedCounter.java
index 59c54d4d6..67839294c 100644
--- a/src/main/java/fr/xephi/authme/util/TimedCounter.java
+++ b/src/main/java/fr/xephi/authme/util/expiring/TimedCounter.java
@@ -1,4 +1,4 @@
-package fr.xephi.authme.util;
+package fr.xephi.authme.util.expiring;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
diff --git a/src/test/java/fr/xephi/authme/data/CaptchaManagerTest.java b/src/test/java/fr/xephi/authme/data/CaptchaManagerTest.java
index d53091db9..6f20d830b 100644
--- a/src/test/java/fr/xephi/authme/data/CaptchaManagerTest.java
+++ b/src/test/java/fr/xephi/authme/data/CaptchaManagerTest.java
@@ -3,7 +3,7 @@ package fr.xephi.authme.data;
import fr.xephi.authme.ReflectionTestUtils;
import fr.xephi.authme.settings.Settings;
import fr.xephi.authme.settings.properties.SecuritySettings;
-import fr.xephi.authme.util.TimedCounter;
+import fr.xephi.authme.util.expiring.TimedCounter;
import org.junit.Test;
import static org.hamcrest.Matchers.equalTo;
diff --git a/src/test/java/fr/xephi/authme/data/SessionManagerTest.java b/src/test/java/fr/xephi/authme/data/SessionManagerTest.java
index 50b181783..738c1c158 100644
--- a/src/test/java/fr/xephi/authme/data/SessionManagerTest.java
+++ b/src/test/java/fr/xephi/authme/data/SessionManagerTest.java
@@ -3,7 +3,7 @@ package fr.xephi.authme.data;
import fr.xephi.authme.ReflectionTestUtils;
import fr.xephi.authme.settings.Settings;
import fr.xephi.authme.settings.properties.PluginSettings;
-import fr.xephi.authme.util.ExpiringSet;
+import fr.xephi.authme.util.expiring.ExpiringSet;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.junit.MockitoJUnitRunner;
diff --git a/src/test/java/fr/xephi/authme/data/TempbanManagerTest.java b/src/test/java/fr/xephi/authme/data/TempbanManagerTest.java
index 4fd8ad117..ab1cbb0b5 100644
--- a/src/test/java/fr/xephi/authme/data/TempbanManagerTest.java
+++ b/src/test/java/fr/xephi/authme/data/TempbanManagerTest.java
@@ -7,7 +7,7 @@ import fr.xephi.authme.message.Messages;
import fr.xephi.authme.service.BukkitService;
import fr.xephi.authme.settings.Settings;
import fr.xephi.authme.settings.properties.SecuritySettings;
-import fr.xephi.authme.util.TimedCounter;
+import fr.xephi.authme.util.expiring.TimedCounter;
import org.bukkit.entity.Player;
import org.junit.Test;
import org.junit.runner.RunWith;
diff --git a/src/test/java/fr/xephi/authme/service/RecoveryCodeServiceTest.java b/src/test/java/fr/xephi/authme/service/RecoveryCodeServiceTest.java
index 576c5da36..eb8af0dc3 100644
--- a/src/test/java/fr/xephi/authme/service/RecoveryCodeServiceTest.java
+++ b/src/test/java/fr/xephi/authme/service/RecoveryCodeServiceTest.java
@@ -6,7 +6,7 @@ import ch.jalu.injector.testing.InjectDelayed;
import fr.xephi.authme.ReflectionTestUtils;
import fr.xephi.authme.settings.Settings;
import fr.xephi.authme.settings.properties.SecuritySettings;
-import fr.xephi.authme.util.ExpiringMap;
+import fr.xephi.authme.util.expiring.ExpiringMap;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
diff --git a/src/test/java/fr/xephi/authme/util/expiring/DurationTest.java b/src/test/java/fr/xephi/authme/util/expiring/DurationTest.java
new file mode 100644
index 000000000..03a0f9d47
--- /dev/null
+++ b/src/test/java/fr/xephi/authme/util/expiring/DurationTest.java
@@ -0,0 +1,43 @@
+package fr.xephi.authme.util.expiring;
+
+import org.junit.Test;
+
+import java.util.concurrent.TimeUnit;
+
+import static org.hamcrest.Matchers.equalTo;
+import static org.junit.Assert.assertThat;
+
+/**
+ * Test for {@link Duration}.
+ */
+public class DurationTest {
+
+ @Test
+ public void shouldConvertToAppropriateTimeUnit() {
+ check(Duration.createWithSuitableUnit(0, TimeUnit.HOURS),
+ 0, TimeUnit.SECONDS);
+
+ check(Duration.createWithSuitableUnit(124, TimeUnit.MINUTES),
+ 2, TimeUnit.HOURS);
+
+ check(Duration.createWithSuitableUnit(300, TimeUnit.HOURS),
+ 12, TimeUnit.DAYS);
+
+ check(Duration.createWithSuitableUnit(60 * 24 * 50 + 8, TimeUnit.MINUTES),
+ 50, TimeUnit.DAYS);
+
+ check(Duration.createWithSuitableUnit(1000L * 60 * 60 * 24 * 7 + 3000, TimeUnit.MILLISECONDS),
+ 7, TimeUnit.DAYS);
+
+ check(Duration.createWithSuitableUnit(1000L * 60 * 60 * 3 + 1400, TimeUnit.MILLISECONDS),
+ 3, TimeUnit.HOURS);
+
+ check(Duration.createWithSuitableUnit(248, TimeUnit.SECONDS),
+ 4, TimeUnit.MINUTES);
+ }
+
+ private static void check(Duration duration, long expectedDuration, TimeUnit expectedUnit) {
+ assertThat(duration.getTimeUnit(), equalTo(expectedUnit));
+ assertThat(duration.getDuration(), equalTo(expectedDuration));
+ }
+}
diff --git a/src/test/java/fr/xephi/authme/util/ExpiringMapTest.java b/src/test/java/fr/xephi/authme/util/expiring/ExpiringMapTest.java
similarity index 98%
rename from src/test/java/fr/xephi/authme/util/ExpiringMapTest.java
rename to src/test/java/fr/xephi/authme/util/expiring/ExpiringMapTest.java
index cdc9c36a5..cdace3065 100644
--- a/src/test/java/fr/xephi/authme/util/ExpiringMapTest.java
+++ b/src/test/java/fr/xephi/authme/util/expiring/ExpiringMapTest.java
@@ -1,4 +1,4 @@
-package fr.xephi.authme.util;
+package fr.xephi.authme.util.expiring;
import org.junit.Test;
diff --git a/src/test/java/fr/xephi/authme/util/ExpiringSetTest.java b/src/test/java/fr/xephi/authme/util/expiring/ExpiringSetTest.java
similarity index 68%
rename from src/test/java/fr/xephi/authme/util/ExpiringSetTest.java
rename to src/test/java/fr/xephi/authme/util/expiring/ExpiringSetTest.java
index f58e03122..15a55aa09 100644
--- a/src/test/java/fr/xephi/authme/util/ExpiringSetTest.java
+++ b/src/test/java/fr/xephi/authme/util/expiring/ExpiringSetTest.java
@@ -1,9 +1,10 @@
-package fr.xephi.authme.util;
+package fr.xephi.authme.util.expiring;
import org.junit.Test;
import java.util.concurrent.TimeUnit;
+import static org.hamcrest.Matchers.either;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertThat;
@@ -88,4 +89,34 @@ public class ExpiringSetTest {
assertThat(set.contains(3), equalTo(false));
assertThat(set.contains(6), equalTo(true));
}
+
+ @Test
+ public void shouldReturnExpiration() {
+ // given
+ ExpiringSet set = new ExpiringSet<>(123, TimeUnit.MINUTES);
+ set.add("my entry");
+
+ // when
+ long expiresInHours = set.getExpiration("my entry", TimeUnit.HOURS);
+ long expiresInMinutes = set.getExpiration("my entry", TimeUnit.MINUTES);
+ long unknownExpires = set.getExpiration("bogus", TimeUnit.SECONDS);
+
+ // then
+ assertThat(expiresInHours, equalTo(2L));
+ assertThat(expiresInMinutes, either(equalTo(122L)).or(equalTo(123L)));
+ assertThat(unknownExpires, equalTo(-1L));
+ }
+
+ @Test
+ public void shouldReturnMinusOneForExpiredEntry() {
+ // given
+ ExpiringSet set = new ExpiringSet<>(-100, TimeUnit.SECONDS);
+ set.add(23);
+
+ // when
+ long expiresInSeconds = set.getExpiration(23, TimeUnit.SECONDS);
+
+ // then
+ assertThat(expiresInSeconds, equalTo(-1L));
+ }
}
diff --git a/src/test/java/fr/xephi/authme/util/TimedCounterTest.java b/src/test/java/fr/xephi/authme/util/expiring/TimedCounterTest.java
similarity index 97%
rename from src/test/java/fr/xephi/authme/util/TimedCounterTest.java
rename to src/test/java/fr/xephi/authme/util/expiring/TimedCounterTest.java
index 9903842de..dcfcb73af 100644
--- a/src/test/java/fr/xephi/authme/util/TimedCounterTest.java
+++ b/src/test/java/fr/xephi/authme/util/expiring/TimedCounterTest.java
@@ -1,4 +1,4 @@
-package fr.xephi.authme.util;
+package fr.xephi.authme.util.expiring;
import org.junit.Test;