♻️ Extract handler for sone-locked notification
authorDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Tue, 10 Dec 2019 14:16:01 +0000 (15:16 +0100)
committerDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Wed, 11 Dec 2019 15:59:09 +0000 (16:59 +0100)
src/main/java/net/pterodactylus/sone/web/WebInterface.java
src/main/kotlin/net/pterodactylus/sone/web/notification/NotificationHandler.kt
src/main/kotlin/net/pterodactylus/sone/web/notification/NotificationHandlerModule.kt
src/main/kotlin/net/pterodactylus/sone/web/notification/SoneLockedHandler.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/notification/NotificationHandlerModuleTest.kt
src/test/kotlin/net/pterodactylus/sone/web/notification/SoneLockedHandlerTest.kt [new file with mode: 0644]

index eaa6019..b4c5942 100644 (file)
@@ -179,9 +179,6 @@ public class WebInterface implements SessionProvider {
        /** Sone locked notification ticker objects. */
        private final Map<Sone, ScheduledFuture<?>> lockedSonesTickerObjects = Collections.synchronizedMap(new HashMap<Sone, ScheduledFuture<?>>());
 
-       /** The “Sone locked” notification. */
-       private final ListNotification<Sone> lockedSonesNotification;
-
        /** The “new version” notification. */
        private final TemplateNotification newVersionNotification;
 
@@ -244,9 +241,6 @@ public class WebInterface implements SessionProvider {
                Template mentionNotificationTemplate = loaders.loadTemplate("/templates/notify/mentionNotification.html");
                mentionNotification = new ListNotification<>("mention-notification", "posts", mentionNotificationTemplate, false);
 
-               Template lockedSonesTemplate = loaders.loadTemplate("/templates/notify/lockedSonesNotification.html");
-               lockedSonesNotification = new ListNotification<>("sones-locked-notification", "sones", lockedSonesTemplate);
-
                Template newVersionTemplate = loaders.loadTemplate("/templates/notify/newVersionNotification.html");
                newVersionNotification = new TemplateNotification("new-version-notification", newVersionTemplate);
 
@@ -759,39 +753,6 @@ public class WebInterface implements SessionProvider {
        }
 
        /**
-        * Notifies the web interface that a Sone was locked.
-        *
-        * @param soneLockedEvent
-        *            The event
-        */
-       @Subscribe
-       public void soneLocked(SoneLockedEvent soneLockedEvent) {
-               final Sone sone = soneLockedEvent.getSone();
-               ScheduledFuture<?> tickerObject = ticker.schedule(new Runnable() {
-
-                       @Override
-                       @SuppressWarnings("synthetic-access")
-                       public void run() {
-                               lockedSonesNotification.add(sone);
-                               notificationManager.addNotification(lockedSonesNotification);
-                       }
-               }, 5, TimeUnit.MINUTES);
-               lockedSonesTickerObjects.put(sone, tickerObject);
-       }
-
-       /**
-        * Notifies the web interface that a Sone was unlocked.
-        *
-        * @param soneUnlockedEvent
-        *            The event
-        */
-       @Subscribe
-       public void soneUnlocked(SoneUnlockedEvent soneUnlockedEvent) {
-               lockedSonesNotification.remove(soneUnlockedEvent.getSone());
-               lockedSonesTickerObjects.remove(soneUnlockedEvent.getSone()).cancel(false);
-       }
-
-       /**
         * Notifies the web interface that a {@link Sone} is being inserted.
         *
         * @param soneInsertingEvent
index cc434d2..04449e7 100644 (file)
@@ -28,5 +28,6 @@ class NotificationHandler @Inject constructor(
                markPostKnownDuringFirstStartHandler: MarkPostKnownDuringFirstStartHandler,
                newSoneHandler: NewSoneHandler,
                newRemotePostHandler: NewRemotePostHandler,
-               soneLockedOnStartupHandler: SoneLockedOnStartupHandler
+               soneLockedOnStartupHandler: SoneLockedOnStartupHandler,
+               soneLockedHandler: SoneLockedHandler
 )
index 279944c..7571f67 100644 (file)
@@ -23,6 +23,7 @@ import net.pterodactylus.sone.data.*
 import net.pterodactylus.sone.main.*
 import net.pterodactylus.sone.notify.*
 import net.pterodactylus.util.notify.*
+import java.util.concurrent.Executors.*
 import javax.inject.*
 import javax.inject.Singleton
 
@@ -72,4 +73,15 @@ class NotificationHandlerModule : AbstractModule() {
        fun getNewPostNotification(loaders: Loaders) =
                        ListNotification<Post>("new-post-notification", "posts", loaders.loadTemplate("/templates/notify/newPostNotification.html"), dismissable = false)
 
+       @Provides
+       @Singleton
+       @Named("soneLocked")
+       fun getSoneLockedNotification(loaders: Loaders) =
+                       ListNotification<Sone>("sones-locked-notification", "sones", loaders.loadTemplate("/templates/notify/lockedSonesNotification.html"), dismissable = true)
+
+       @Provides
+       @Singleton
+       fun getSoneLockedHandler(notificationManager: NotificationManager, @Named("soneLocked") soneLockedNotification: ListNotification<Sone>) =
+                       SoneLockedHandler(notificationManager, soneLockedNotification, newScheduledThreadPool(1))
+
 }
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/notification/SoneLockedHandler.kt b/src/main/kotlin/net/pterodactylus/sone/web/notification/SoneLockedHandler.kt
new file mode 100644 (file)
index 0000000..8bfbda4
--- /dev/null
@@ -0,0 +1,61 @@
+/**
+ * Sone - SoneLockedHandler.kt - Copyright © 2019 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.notification
+
+import com.google.common.eventbus.*
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.sone.data.*
+import net.pterodactylus.sone.notify.*
+import net.pterodactylus.util.notify.*
+import java.util.concurrent.*
+import java.util.concurrent.atomic.*
+
+/**
+ * Handler for [SoneLockedEvent]s and [SoneUnlockedEvent]s that can schedule notifications after
+ * a certain timeout.
+ */
+class SoneLockedHandler(private val notificationManager: NotificationManager, private val notification: ListNotification<Sone>, private val executor: ScheduledExecutorService) {
+
+       private val future: AtomicReference<ScheduledFuture<*>> = AtomicReference()
+
+       @Subscribe
+       fun soneLocked(soneLockedEvent: SoneLockedEvent) {
+               synchronized(future) {
+                       future.get()?.also { cancelPreviousFuture(it, soneLockedEvent.sone) }
+                       future.set(executor.schedule(showNotification(soneLockedEvent.sone), 5, TimeUnit.MINUTES))
+               }
+       }
+
+       @Subscribe
+       fun soneUnlocked(soneUnlockedEvent: SoneUnlockedEvent) {
+               synchronized(future) {
+                       future.get()?.also { cancelPreviousFuture(it, soneUnlockedEvent.sone) }
+               }
+       }
+
+       private fun cancelPreviousFuture(future: ScheduledFuture<*>, sone: Sone) {
+               notification.remove(sone)
+               future.cancel(true)
+       }
+
+       private fun showNotification(sone: Sone): () -> Unit = {
+               notification.add(sone)
+               notificationManager.addNotification(notification)
+       }
+
+}
index a37abd8..c66531b 100644 (file)
@@ -187,4 +187,43 @@ class NotificationHandlerModuleTest {
                assertThat(notification.render(), equalTo(posts.toString()))
        }
 
+       @Test
+       fun `sone-locked notification can be created`() {
+               assertThat(injector.getInstance<ListNotification<Sone>>(named("soneLocked")), notNullValue())
+       }
+
+       @Test
+       fun `sone-locked notification is created as singleton`() {
+               injector.verifySingletonInstance<ListNotification<Sone>>(named("soneLocked"))
+       }
+
+       @Test
+       fun `sone-locked notification is dismissable`() {
+               assertThat(injector.getInstance<ListNotification<Sone>>(named("soneLocked")).isDismissable, equalTo(true))
+       }
+
+       @Test
+       fun `sone-locked notification has correct ID`() {
+               assertThat(injector.getInstance<ListNotification<Sone>>(named("soneLocked")).id, equalTo("sones-locked-notification"))
+       }
+
+       @Test
+       fun `sone-locked notification has correct key and template`() {
+               loaders.templates += "/templates/notify/lockedSonesNotification.html" to "<% sones>".asTemplate()
+               val notification = injector.getInstance<ListNotification<Sone>>(named("soneLocked"))
+               val sones = listOf(IdOnlySone("sone1"), IdOnlySone("sone2"))
+               sones.forEach(notification::add)
+               assertThat(notification.render(), equalTo(sones.toString()))
+       }
+
+       @Test
+       fun `sone-locked handler can be created`() {
+               assertThat(injector.getInstance<SoneLockedHandler>(), notNullValue())
+       }
+
+       @Test
+       fun `sone-locked handler is created as singleton`() {
+               injector.verifySingletonInstance<SoneLockedHandler>()
+       }
+
 }
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/notification/SoneLockedHandlerTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/notification/SoneLockedHandlerTest.kt
new file mode 100644 (file)
index 0000000..2daf5c9
--- /dev/null
@@ -0,0 +1,125 @@
+/**
+ * Sone - SoneLockedHandlerTest.kt - Copyright © 2019 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.notification
+
+import com.google.common.eventbus.*
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.sone.data.*
+import net.pterodactylus.sone.data.impl.*
+import net.pterodactylus.sone.notify.*
+import net.pterodactylus.util.notify.*
+import net.pterodactylus.util.template.*
+import org.hamcrest.MatcherAssert.*
+import org.hamcrest.Matchers.*
+import java.util.concurrent.*
+import kotlin.test.*
+
+/**
+ * Unit test for [SoneLockedHandler].
+ */
+@Suppress("UnstableApiUsage")
+class SoneLockedHandlerTest {
+
+       private val eventBus = EventBus()
+       private val notificationManager = NotificationManager()
+       private val notification = ListNotification<Sone>("", "", Template())
+       private val executor = TestScheduledThreadPoolExecutor()
+
+       init {
+               SoneLockedHandler(notificationManager, notification, executor).also(eventBus::register)
+       }
+
+       @Test
+       fun `notification is not added during the first five minutes`() {
+               eventBus.post(SoneLockedEvent(sone))
+               assertThat(notificationManager.notifications, emptyIterable())
+       }
+
+       @Test
+       fun `sone is added to notification from command`() {
+               eventBus.post(SoneLockedEvent(sone))
+               executor.scheduledDelay.single().command.run()
+               assertThat(notification.elements, contains(sone))
+       }
+
+       @Test
+       fun `notification is added to notification manager from command`() {
+               eventBus.post(SoneLockedEvent(sone))
+               executor.scheduledDelay.single().command.run()
+               assertThat(notificationManager.notifications, contains<Any>(notification))
+       }
+
+       @Test
+       fun `command is registered with a delay of five minutes`() {
+               eventBus.post(SoneLockedEvent(sone))
+               with(executor.scheduledDelay.single()) {
+                       assertThat(timeUnit.toNanos(delay), equalTo(TimeUnit.MINUTES.toNanos(5)))
+               }
+       }
+
+       @Test
+       fun `unlocking sone after locking will cancel the future`() {
+               eventBus.post(SoneLockedEvent(sone))
+               eventBus.post(SoneUnlockedEvent(sone))
+               assertThat(executor.scheduledDelay.first().future.isCancelled, equalTo(true))
+       }
+
+       @Test
+       fun `unlocking sone after locking will remove the sone from the notification`() {
+               eventBus.post(SoneLockedEvent(sone))
+               eventBus.post(SoneUnlockedEvent(sone))
+               assertThat(notification.elements, emptyIterable())
+       }
+
+       @Test
+       fun `unlocking sone after showing the notification will remove the sone from the notification`() {
+               eventBus.post(SoneLockedEvent(sone))
+               executor.scheduledDelay.single().command.run()
+               eventBus.post(SoneUnlockedEvent(sone))
+               assertThat(notification.elements, emptyIterable())
+       }
+
+       @Test
+       fun `locking two sones will cancel the first command`() {
+               eventBus.post(SoneLockedEvent(sone))
+               eventBus.post(SoneLockedEvent(sone))
+               assertThat(executor.scheduledDelay.first().future.isCancelled, equalTo(true))
+       }
+
+       @Test
+       fun `locking two sones will schedule a second command`() {
+               eventBus.post(SoneLockedEvent(sone))
+               eventBus.post(SoneLockedEvent(sone))
+               assertThat(executor.scheduledDelay[1], notNullValue())
+       }
+
+}
+
+private val sone: Sone = IdOnlySone("sone")
+
+private data class Scheduled(val command: Runnable, val delay: Long, val timeUnit: TimeUnit, val future: ScheduledFuture<*>)
+
+private class TestScheduledThreadPoolExecutor : ScheduledThreadPoolExecutor(1) {
+
+       val scheduledDelay = mutableListOf<Scheduled>()
+
+       override fun schedule(command: Runnable, delay: Long, unit: TimeUnit): ScheduledFuture<*> =
+                       super.schedule(command, delay, unit)
+                                       .also { scheduledDelay += Scheduled(command, delay, unit, it) }
+
+}