/** 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;
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);
}
/**
- * 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
markPostKnownDuringFirstStartHandler: MarkPostKnownDuringFirstStartHandler,
newSoneHandler: NewSoneHandler,
newRemotePostHandler: NewRemotePostHandler,
- soneLockedOnStartupHandler: SoneLockedOnStartupHandler
+ soneLockedOnStartupHandler: SoneLockedOnStartupHandler,
+ soneLockedHandler: SoneLockedHandler
)
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
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))
+
}
--- /dev/null
+/**
+ * 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)
+ }
+
+}
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>()
+ }
+
}
--- /dev/null
+/**
+ * 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) }
+
+}