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;