diff --git a/cloud-processors-cooldown/build.gradle.kts b/cloud-processors-cooldown/build.gradle.kts index a096a58..9dfe110 100644 --- a/cloud-processors-cooldown/build.gradle.kts +++ b/cloud-processors-cooldown/build.gradle.kts @@ -8,3 +8,9 @@ dependencies { compileOnly(libs.cloud.annotations) } + +// TODO(City): Disable this +// we're getting errors on generated files due to -Werror :( +tasks.withType { + options.compilerArgs.remove("-Werror") +} diff --git a/cloud-processors-cooldown/src/main/java/org/incendo/cloud/processors/cooldown/CooldownConfiguration.java b/cloud-processors-cooldown/src/main/java/org/incendo/cloud/processors/cooldown/CooldownConfiguration.java index c9b71bd..f3bf1e0 100644 --- a/cloud-processors-cooldown/src/main/java/org/incendo/cloud/processors/cooldown/CooldownConfiguration.java +++ b/cloud-processors-cooldown/src/main/java/org/incendo/cloud/processors/cooldown/CooldownConfiguration.java @@ -25,6 +25,7 @@ import cloud.commandframework.context.CommandContext; import java.time.Clock; +import java.util.List; import java.util.function.Predicate; import org.apiguardian.api.API; import org.checkerframework.checker.nullness.qual.NonNull; @@ -69,6 +70,13 @@ public interface CooldownConfiguration { */ @NonNull CooldownNotifier cooldownNotifier(); + /** + * Returns the creation listeners. + * + * @return creation listeners + */ + @NonNull List> creationListeners(); + /** * Returns a predicate that determines whether the {@link CommandContext} * should bypass the cooldown requirement. diff --git a/cloud-processors-cooldown/src/main/java/org/incendo/cloud/processors/cooldown/CooldownCreationListener.java b/cloud-processors-cooldown/src/main/java/org/incendo/cloud/processors/cooldown/CooldownCreationListener.java new file mode 100644 index 0000000..1136a75 --- /dev/null +++ b/cloud-processors-cooldown/src/main/java/org/incendo/cloud/processors/cooldown/CooldownCreationListener.java @@ -0,0 +1,47 @@ +// +// MIT License +// +// Copyright (c) 2024 Incendo +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +package org.incendo.cloud.processors.cooldown; + +import cloud.commandframework.Command; +import org.apiguardian.api.API; +import org.checkerframework.checker.nullness.qual.NonNull; + +/** + * Listener that gets invoked when a new cooldown is created. + * + * @param command sender type + * @since 1.0.0 + */ +@API(status = API.Status.STABLE, since = "1.0.0") +public interface CooldownCreationListener { + + /** + * Invoked when a new cooldown is created. + * + * @param sender sender that the cooldown is created for + * @param command command that triggered the cooldown creation + * @param instance created instance + */ + void cooldownCreated(@NonNull C sender, @NonNull Command command, @NonNull CooldownInstance instance); +} diff --git a/cloud-processors-cooldown/src/main/java/org/incendo/cloud/processors/cooldown/CooldownInstance.java b/cloud-processors-cooldown/src/main/java/org/incendo/cloud/processors/cooldown/CooldownInstance.java index 1d66994..c924e54 100644 --- a/cloud-processors-cooldown/src/main/java/org/incendo/cloud/processors/cooldown/CooldownInstance.java +++ b/cloud-processors-cooldown/src/main/java/org/incendo/cloud/processors/cooldown/CooldownInstance.java @@ -28,6 +28,7 @@ import org.apiguardian.api.API; import org.checkerframework.checker.nullness.qual.NonNull; import org.immutables.value.Value; +import org.incendo.cloud.processors.cooldown.profile.CooldownProfile; import org.incendo.cloud.processors.immutables.StagedImmutableBuilder; /** @@ -45,10 +46,17 @@ public interface CooldownInstance { * * @return the builder */ - static ImmutableCooldownInstance.@NonNull GroupBuildStage builder() { + static ImmutableCooldownInstance.@NonNull ProfileBuildStage builder() { return ImmutableCooldownInstance.builder(); } + /** + * Returns the profile that owns the cooldown. + * + * @return the owning profile + */ + @NonNull CooldownProfile profile(); + /** * Returns the cooldown group. * diff --git a/cloud-processors-cooldown/src/main/java/org/incendo/cloud/processors/cooldown/CooldownPostprocessor.java b/cloud-processors-cooldown/src/main/java/org/incendo/cloud/processors/cooldown/CooldownPostprocessor.java index 335c00e..3cd297e 100644 --- a/cloud-processors-cooldown/src/main/java/org/incendo/cloud/processors/cooldown/CooldownPostprocessor.java +++ b/cloud-processors-cooldown/src/main/java/org/incendo/cloud/processors/cooldown/CooldownPostprocessor.java @@ -84,10 +84,17 @@ public void accept(final @NonNull CommandPostprocessingContext context) { } final CooldownInstance instance = CooldownInstance.builder() + .profile(profile) .group(group) .duration(((DurationFunction) cooldown.duration()).getDuration(context.commandContext())) .creationTime(Instant.now(this.cooldownManager.configuration().clock())) .build(); profile.setCooldown(group, instance); + + this.cooldownManager.configuration().creationListeners().forEach(listener -> listener.cooldownCreated( + context.commandContext().sender(), + context.command(), + instance + )); } } diff --git a/cloud-processors-cooldown/src/main/java/org/incendo/cloud/processors/cooldown/CooldownRepository.java b/cloud-processors-cooldown/src/main/java/org/incendo/cloud/processors/cooldown/CooldownRepository.java index 9d6f36b..3d966e1 100644 --- a/cloud-processors-cooldown/src/main/java/org/incendo/cloud/processors/cooldown/CooldownRepository.java +++ b/cloud-processors-cooldown/src/main/java/org/incendo/cloud/processors/cooldown/CooldownRepository.java @@ -28,6 +28,7 @@ import java.util.function.Function; import org.apiguardian.api.API; import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; import org.incendo.cloud.processors.cache.CloudCache; import org.incendo.cloud.processors.cooldown.profile.CooldownProfile; import org.incendo.cloud.processors.cooldown.profile.CooldownProfileFactory; @@ -97,6 +98,31 @@ public interface CooldownRepository { */ @NonNull CooldownProfile getProfile(@NonNull K key, @NonNull CooldownProfileFactory profileFactory); + /** + * Returns the profile for the given {@code key}, if it exists. + * + * @param key key that identifies the profile + * @return the profile, or {@code null} + */ + @Nullable CooldownProfile getProfileIfExists(@NonNull K key); + + /** + * Deletes the profile identified by the given {@code key}. + * + * @param key the key + */ + void deleteProfile(@NonNull K key); + + /** + * Deletes the cooldown identified by the given {@code key} belonging to the given {@code group}. + * + *

If the profile is empty after the deletion, then the profile is deleted too.

+ * + * @param key key identifying the profile + * @param group group to delete + */ + void deleteCooldown(@NonNull K key, @NonNull CooldownGroup group); + final class MappingCooldownRepository implements CooldownRepository { @@ -113,12 +139,45 @@ private MappingCooldownRepository( } @Override - public @NonNull CooldownProfile getProfile(final @NonNull C key, final @NonNull CooldownProfileFactory profileFactory) { + public @NonNull CooldownProfile getProfile( + final @NonNull C key, + final @NonNull CooldownProfileFactory profileFactory + ) { return this.otherRepository.getProfile(this.mappingFunction.apply(key), profileFactory); } + + @Override + public @Nullable CooldownProfile getProfileIfExists(final @NonNull C key) { + return this.otherRepository.getProfileIfExists(this.mappingFunction.apply(key)); + } + + @Override + public void deleteProfile(final @NonNull C key) { + this.otherRepository.deleteProfile(this.mappingFunction.apply(key)); + } + + @Override + public void deleteCooldown(final @NonNull C key, final @NonNull CooldownGroup group) { + this.otherRepository.deleteCooldown(this.mappingFunction.apply(key), group); + } } - final class MapCooldownRepository implements CooldownRepository { + abstract class AbstractCooldownRepository implements CooldownRepository { + + @Override + public synchronized void deleteCooldown(final @NonNull K key, final @NonNull CooldownGroup group) { + final CooldownProfile profile = this.getProfileIfExists(key); + if (profile == null) { + return; + } + profile.deleteCooldown(group); + if (profile.isEmpty()) { + this.deleteProfile(key); + } + } + } + + final class MapCooldownRepository extends AbstractCooldownRepository { private final Map map; @@ -127,12 +186,25 @@ private MapCooldownRepository(final @NonNull Map map) { } @Override - public @NonNull CooldownProfile getProfile(final @NonNull K key, final @NonNull CooldownProfileFactory profileFactory) { + public synchronized @NonNull CooldownProfile getProfile( + final @NonNull K key, + final @NonNull CooldownProfileFactory profileFactory + ) { return this.map.computeIfAbsent(key, k -> profileFactory.create()); } + + @Override + public synchronized @Nullable CooldownProfile getProfileIfExists(final @NonNull K key) { + return this.map.get(key); + } + + @Override + public synchronized void deleteProfile(final @NonNull K key) { + this.map.remove(key); + } } - final class CacheCooldownRepository implements CooldownRepository { + final class CacheCooldownRepository extends AbstractCooldownRepository { private final CloudCache cache; @@ -141,7 +213,10 @@ private CacheCooldownRepository(final @NonNull CloudCache ca } @Override - public @NonNull CooldownProfile getProfile(final @NonNull K key, final @NonNull CooldownProfileFactory profileFactory) { + public synchronized @NonNull CooldownProfile getProfile( + final @NonNull K key, + final @NonNull CooldownProfileFactory profileFactory + ) { CooldownProfile profile = this.cache.getIfPresent(key); if (profile == null) { profile = profileFactory.create(); @@ -149,5 +224,15 @@ private CacheCooldownRepository(final @NonNull CloudCache ca } return profile; } + + @Override + public synchronized @Nullable CooldownProfile getProfileIfExists(final @NonNull K key) { + return this.cache.getIfPresent(key); + } + + @Override + public synchronized void deleteProfile(final @NonNull K key) { + this.cache.delete(key); + } } } diff --git a/cloud-processors-cooldown/src/main/java/org/incendo/cloud/processors/cooldown/profile/CooldownProfile.java b/cloud-processors-cooldown/src/main/java/org/incendo/cloud/processors/cooldown/profile/CooldownProfile.java index f3cb796..2580632 100644 --- a/cloud-processors-cooldown/src/main/java/org/incendo/cloud/processors/cooldown/profile/CooldownProfile.java +++ b/cloud-processors-cooldown/src/main/java/org/incendo/cloud/processors/cooldown/profile/CooldownProfile.java @@ -47,4 +47,18 @@ public interface CooldownProfile { * @param cooldown cooldown value */ void setCooldown(@NonNull CooldownGroup group, @NonNull CooldownInstance cooldown); + + /** + * Deletes the cooldown for the given {@code group}, if it exists. + * + * @param group the group + */ + void deleteCooldown(@NonNull CooldownGroup group); + + /** + * Returns whether the profile is empty. + * + * @return {@code true} if the profile is empty, else {@code false} + */ + boolean isEmpty(); } diff --git a/cloud-processors-cooldown/src/main/java/org/incendo/cloud/processors/cooldown/profile/CooldownProfileImpl.java b/cloud-processors-cooldown/src/main/java/org/incendo/cloud/processors/cooldown/profile/CooldownProfileImpl.java index 8f94e24..6a51b53 100644 --- a/cloud-processors-cooldown/src/main/java/org/incendo/cloud/processors/cooldown/profile/CooldownProfileImpl.java +++ b/cloud-processors-cooldown/src/main/java/org/incendo/cloud/processors/cooldown/profile/CooldownProfileImpl.java @@ -43,7 +43,7 @@ final class CooldownProfileImpl implements CooldownProfile { } @Override - public synchronized @Nullable CooldownInstance getCooldown(@NonNull final CooldownGroup group) { + public synchronized @Nullable CooldownInstance getCooldown(final @NonNull CooldownGroup group) { final CooldownInstance cooldown = this.cooldowns.get(group); if (cooldown == null) { return null; @@ -58,7 +58,17 @@ final class CooldownProfileImpl implements CooldownProfile { } @Override - public synchronized void setCooldown(@NonNull final CooldownGroup group, @NonNull final CooldownInstance cooldown) { + public synchronized void setCooldown(final @NonNull CooldownGroup group, final @NonNull CooldownInstance cooldown) { this.cooldowns.put(group, cooldown); } + + @Override + public synchronized void deleteCooldown(final @NonNull CooldownGroup group) { + this.cooldowns.remove(group); + } + + @Override + public boolean isEmpty() { + return this.cooldowns.isEmpty(); + } } diff --git a/cloud-processors-cooldown/src/test/java/org/incendo/cloud/processors/confirmation/CooldownManagerTest.java b/cloud-processors-cooldown/src/test/java/org/incendo/cloud/processors/confirmation/CooldownManagerTest.java index e7be627..9dd06ea 100644 --- a/cloud-processors-cooldown/src/test/java/org/incendo/cloud/processors/confirmation/CooldownManagerTest.java +++ b/cloud-processors-cooldown/src/test/java/org/incendo/cloud/processors/confirmation/CooldownManagerTest.java @@ -34,17 +34,21 @@ import org.incendo.cloud.processors.confirmation.util.TestCommandSender; import org.incendo.cloud.processors.cooldown.Cooldown; import org.incendo.cloud.processors.cooldown.CooldownConfiguration; +import org.incendo.cloud.processors.cooldown.CooldownCreationListener; import org.incendo.cloud.processors.cooldown.CooldownGroup; +import org.incendo.cloud.processors.cooldown.CooldownInstance; import org.incendo.cloud.processors.cooldown.CooldownManager; import org.incendo.cloud.processors.cooldown.CooldownNotifier; import org.incendo.cloud.processors.cooldown.CooldownRepository; import org.incendo.cloud.processors.cooldown.DurationFunction; +import org.incendo.cloud.processors.cooldown.profile.CooldownProfile; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.never; @@ -63,6 +67,8 @@ class CooldownManagerTest { private CommandExecutionHandler commandExecutionHandler; @Mock private Clock clock; + @Mock + private CooldownCreationListener listener; private CommandManager commandManager; private CooldownManager cooldownManager; @@ -75,6 +81,7 @@ void setup() { .repository(CooldownRepository.forMap(new HashMap<>())) .cooldownNotifier(this.notifier) .clock(this.clock) + .addCreationListeners(this.listener) .build() ); this.commandManager.registerCommandPostProcessor(this.cooldownManager.createPostprocessor()); @@ -100,6 +107,7 @@ void testAppliesCooldown() { // Assert verify(this.commandExecutionHandler).executeFuture(any()); verify(this.notifier).notify(eq(this.commandSender), any(), any()); + verify(this.listener).cooldownCreated(eq(this.commandSender), any(), any()); } @Test @@ -122,10 +130,12 @@ void testCooldownExpired() { // Assert verify(this.commandExecutionHandler, times(2)).executeFuture(any()); verify(this.notifier, never()).notify(eq(this.commandSender), any(), any()); + verify(this.listener, times(2)).cooldownCreated(eq(this.commandSender), any(), any()); } @Test void testCooldownGroupsDifferentGroups() { + // Arrange final Cooldown decorator1 = Cooldown.of( DurationFunction.constant(Duration.ofHours(1L)), CooldownGroup.named("foo") @@ -155,10 +165,12 @@ void testCooldownGroupsDifferentGroups() { // Assert verify(this.commandExecutionHandler, times(2)).executeFuture(any()); verify(this.notifier, never()).notify(eq(this.commandSender), any(), any()); + verify(this.listener, times(2)).cooldownCreated(eq(this.commandSender), any(), any()); } @Test void testCooldownGroupsSameGroup() { + // Arrange final Cooldown decorator = Cooldown.of( DurationFunction.constant(Duration.ofHours(1L)), CooldownGroup.named("foo") @@ -184,5 +196,28 @@ void testCooldownGroupsSameGroup() { // Assert verify(this.commandExecutionHandler, times(1)).executeFuture(any()); verify(this.notifier).notify(eq(this.commandSender), any(), any()); + verify(this.listener).cooldownCreated(eq(this.commandSender), any(), any()); + } + + @Test + void testCooldownDeletion() { + // Arrange + final CooldownGroup group = CooldownGroup.named("foo"); + final CooldownProfile profile = this.cooldownManager.repository() + .getProfile(this.commandSender, this.cooldownManager.configuration().profileFactory()); + final CooldownInstance cooldown = CooldownInstance.builder() + .profile(profile) + .group(group) + .duration(Duration.ofHours(1L)) + .creationTime(Instant.now()) + .build(); + profile.setCooldown(cooldown.group(), cooldown); + + // Act + this.cooldownManager.repository().deleteCooldown(this.commandSender, group); + + // Assert + assertThat(profile.isEmpty()).isTrue(); + assertThat(this.cooldownManager.repository().getProfileIfExists(this.commandSender)).isNull(); } }