From: David ‘Bombe’ Roden Date: Sun, 9 Feb 2020 11:16:48 +0000 (+0100) Subject: 🔀 Merge “release/v81” into “master” X-Git-Tag: v81^0 X-Git-Url: https://git.pterodactylus.net/?p=Sone.git;a=commitdiff_plain;h=faf66247a34f64946990a985d2ea3003465969cb;hp=b4d2d68b5ea4f4edc7337f380cfe078756678126 🔀 Merge “release/v81” into “master” --- diff --git a/.builds/jdk-1.8.yml b/.builds/jdk-1.8.yml index 5edd26d..2c43d38 100644 --- a/.builds/jdk-1.8.yml +++ b/.builds/jdk-1.8.yml @@ -9,7 +9,7 @@ tasks: ./gradlew clean - build: | cd sone - ./gradlew build + ./gradlew -x findbugsTest build - test: | cd sone ./gradlew test diff --git a/.gitignore b/.gitignore index fce4db9..abaaab1 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ /.gradle/ /build/ + +sone.properties diff --git a/build.gradle b/build.gradle index 0beb42a..3730ead 100644 --- a/build.gradle +++ b/build.gradle @@ -1,18 +1,13 @@ -group = 'net.pterodactylus' -version = '80' -buildscript { - ext.kotlinVersion = '1.2.71' - repositories { - mavenCentral() - } - dependencies { - classpath group: 'info.solidsoft.gradle.pitest', name: 'gradle-pitest-plugin', version: '1.4.0' - classpath group: 'org.jetbrains.kotlin', name: 'kotlin-gradle-plugin', version: kotlinVersion - classpath group: 'org.jetbrains.kotlin', name: 'kotlin-noarg', version: kotlinVersion - } +plugins { + id 'org.jetbrains.kotlin.jvm' version '1.3.61' + id 'org.jetbrains.kotlin.plugin.noarg' version '1.3.61' + id 'info.solidsoft.pitest' version '1.4.5' } +group = 'net.pterodactylus' +version = '81' + repositories { mavenCentral() maven { url "https://maven.pterodactylus.net/" } @@ -20,15 +15,13 @@ repositories { apply plugin: 'java' -sourceCompatibility = 1.7 -targetCompatibility = 1.7 +sourceCompatibility = 1.8 +targetCompatibility = 1.8 tasks.withType(JavaCompile) { options.encoding = 'UTF-8' } -apply plugin: 'kotlin' - configurations { provided { dependencies.all { dep -> @@ -43,25 +36,44 @@ dependencies { provided group: 'org.freenetproject', name: 'freenet-ext', version: '29' provided group: 'org.bouncycastle', name: 'bcprov-jdk15on', version: '1.54' - compile group: 'org.jetbrains.kotlin', name: 'kotlin-stdlib' - compile group: 'net.pterodactylus', name: 'utils', version: '0.12.4' + compile group: 'org.jetbrains.kotlin', name: 'kotlin-stdlib-jdk8' + compile group: 'org.jetbrains.kotlinx', name: 'kotlinx-coroutines-core', version: '1.3.0-RC' + + compile group: 'net.pterodactylus', name: 'utils', version: '0.13.1' compile group: 'com.google.inject', name: 'guice', version: '4.2.2' - compile group: 'com.google.guava', name: 'guava', version: '27.0.1-android' + compile group: 'com.google.guava', name: 'guava', version: '27.0.1-jre' compile group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.9.1' compile group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-yaml', version: '2.9.1' compile group: 'com.google.code.findbugs', name: 'jsr305', version: '3.0.2' compile group: 'org.jsoup', name: 'jsoup', version: '1.10.2' + compile group: 'io.dropwizard.metrics', name: 'metrics-core', version: '4.1.0' + compile group: 'javax.activation', name: 'javax.activation-api', version: '1.2.0' testCompile group: 'org.jetbrains.kotlin', name: 'kotlin-test-junit' testCompile group: 'junit', name: 'junit', version: '4.11' - testCompile group: 'org.mockito', name: 'mockito-core', version: '2.10.0' + testCompile group: 'org.mockito', name: 'mockito-core', version: '2.28.2' testCompile group: 'org.hamcrest', name: 'hamcrest-all', version: '1.3' } apply from: 'version.gradle' -test { +task parallelTest(type: Test) { maxParallelForks = Runtime.runtime.availableProcessors().intdiv(2) ?: 1 + useJUnit { + excludeCategories 'net.pterodactylus.sone.test.NotParallel' + } +} + +task notParallelTest(type: Test) { + maxParallelForks = 1 + useJUnit { + includeCategories 'net.pterodactylus.sone.test.NotParallel' + } +} + +test { + exclude '**' + dependsOn parallelTest, notParallelTest } task fatJar(type: Jar) { @@ -86,14 +98,13 @@ javadoc { apply plugin: 'jacoco' jacoco { - toolVersion = '0.7.9' + toolVersion = '0.8.4' } jacocoTestReport.dependsOn test -apply plugin: 'info.solidsoft.pitest' - pitest { + pitestVersion = '1.4.10' outputFormats = ['HTML', 'XML'] timestampedReports = false timeoutFactor = 3.0 @@ -125,8 +136,6 @@ task countLines { dependsOn tasks.countLinesTest } -apply plugin: 'kotlin-noarg' - noArg { annotation('net.pterodactylus.sone.main.NoArg') } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index c19a936..1cdded7 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.5-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.1-all.zip diff --git a/src/main/java/net/pterodactylus/sone/core/Core.java b/src/main/java/net/pterodactylus/sone/core/Core.java index b0df7a9..18588a1 100644 --- a/src/main/java/net/pterodactylus/sone/core/Core.java +++ b/src/main/java/net/pterodactylus/sone/core/Core.java @@ -1,5 +1,5 @@ /* - * Sone - Core.java - Copyright © 2010–2019 David Roden + * Sone - Core.java - Copyright © 2010–2020 David 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 @@ -24,6 +24,7 @@ import static com.google.common.primitives.Longs.tryParse; import static java.lang.String.format; import static java.util.logging.Level.WARNING; import static java.util.logging.Logger.getLogger; +import static net.pterodactylus.sone.data.AlbumsKt.getAllImages; import java.util.ArrayList; import java.util.Collection; @@ -37,30 +38,20 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.*; import java.util.logging.Level; import java.util.logging.Logger; import javax.annotation.Nonnull; import javax.annotation.Nullable; +import com.codahale.metrics.*; import net.pterodactylus.sone.core.ConfigurationSoneParser.InvalidAlbumFound; import net.pterodactylus.sone.core.ConfigurationSoneParser.InvalidImageFound; import net.pterodactylus.sone.core.ConfigurationSoneParser.InvalidParentAlbumFound; import net.pterodactylus.sone.core.ConfigurationSoneParser.InvalidPostFound; import net.pterodactylus.sone.core.ConfigurationSoneParser.InvalidPostReplyFound; -import net.pterodactylus.sone.core.event.ImageInsertFinishedEvent; -import net.pterodactylus.sone.core.event.InsertionDelayChangedEvent; -import net.pterodactylus.sone.core.event.MarkPostKnownEvent; -import net.pterodactylus.sone.core.event.MarkPostReplyKnownEvent; -import net.pterodactylus.sone.core.event.MarkSoneKnownEvent; -import net.pterodactylus.sone.core.event.NewPostFoundEvent; -import net.pterodactylus.sone.core.event.NewPostReplyFoundEvent; -import net.pterodactylus.sone.core.event.NewSoneFoundEvent; -import net.pterodactylus.sone.core.event.PostRemovedEvent; -import net.pterodactylus.sone.core.event.PostReplyRemovedEvent; -import net.pterodactylus.sone.core.event.SoneLockedEvent; -import net.pterodactylus.sone.core.event.SoneRemovedEvent; -import net.pterodactylus.sone.core.event.SoneUnlockedEvent; +import net.pterodactylus.sone.core.event.*; import net.pterodactylus.sone.data.Album; import net.pterodactylus.sone.data.Client; import net.pterodactylus.sone.data.Image; @@ -98,7 +89,7 @@ import net.pterodactylus.util.service.AbstractService; import net.pterodactylus.util.thread.NamedThreadFactory; import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.Optional; +import com.google.common.base.Stopwatch; import com.google.common.collect.FluentIterable; import com.google.common.collect.HashMultimap; import com.google.common.collect.Multimap; @@ -121,6 +112,8 @@ public class Core extends AbstractService implements SoneProvider, PostProvider, /** The start time. */ private final long startupTime = System.currentTimeMillis(); + private final AtomicBoolean debug = new AtomicBoolean(false); + /** The preferences. */ private final Preferences preferences; @@ -184,6 +177,9 @@ public class Core extends AbstractService implements SoneProvider, PostProvider, /** The time the configuration was last touched. */ private volatile long lastConfigurationUpdate; + private final MetricRegistry metricRegistry; + private final Histogram configurationSaveTimeHistogram; + /** * Creates a new core. * @@ -201,7 +197,7 @@ public class Core extends AbstractService implements SoneProvider, PostProvider, * The database */ @Inject - public Core(Configuration configuration, FreenetInterface freenetInterface, IdentityManager identityManager, SoneDownloader soneDownloader, ImageInserter imageInserter, UpdateChecker updateChecker, WebOfTrustUpdater webOfTrustUpdater, EventBus eventBus, Database database) { + public Core(Configuration configuration, FreenetInterface freenetInterface, IdentityManager identityManager, SoneDownloader soneDownloader, ImageInserter imageInserter, UpdateChecker updateChecker, WebOfTrustUpdater webOfTrustUpdater, EventBus eventBus, Database database, MetricRegistry metricRegistry) { super("Sone Core"); this.configuration = configuration; this.freenetInterface = freenetInterface; @@ -212,7 +208,9 @@ public class Core extends AbstractService implements SoneProvider, PostProvider, this.webOfTrustUpdater = webOfTrustUpdater; this.eventBus = eventBus; this.database = database; + this.metricRegistry = metricRegistry; preferences = new Preferences(eventBus); + this.configurationSaveTimeHistogram = metricRegistry.histogram("configuration.save.duration", () -> new Histogram(new ExponentiallyDecayingReservoir(3000, 0))); } // @@ -228,6 +226,16 @@ public class Core extends AbstractService implements SoneProvider, PostProvider, return startupTime; } + @Nonnull + public boolean getDebug() { + return debug.get(); + } + + public void setDebug() { + debug.set(true); + eventBus.post(new DebugActivatedEvent()); + } + /** * Returns the options used by the core. * @@ -618,7 +626,7 @@ public class Core extends AbstractService implements SoneProvider, PostProvider, sone.setLatestEdition(fromNullable(tryParse(property)).or(0L)); sone.setClient(new Client("Sone", SonePlugin.getPluginVersion())); sone.setKnown(true); - SoneInserter soneInserter = new SoneInserter(this, eventBus, freenetInterface, ownIdentity.getId()); + SoneInserter soneInserter = new SoneInserter(this, eventBus, freenetInterface, metricRegistry, ownIdentity.getId()); soneInserter.insertionDelayChanged(new InsertionDelayChangedEvent(preferences.getInsertionDelay())); eventBus.register(soneInserter); synchronized (soneInserters) { @@ -627,6 +635,11 @@ public class Core extends AbstractService implements SoneProvider, PostProvider, loadSone(sone); database.storeSone(sone); sone.setStatus(SoneStatus.idle); + if (sone.getPosts().isEmpty() && sone.getReplies().isEmpty() && getAllImages(sone.getRootAlbum()).isEmpty()) { + // dirty hack + lockSone(sone); + eventBus.post(new SoneLockedOnStartup(sone)); + } soneInserter.start(); return sone; } @@ -738,75 +751,6 @@ public class Core extends AbstractService implements SoneProvider, PostProvider, } /** - * Sets the trust value of the given origin Sone for the target Sone. - * - * @param origin - * The origin Sone - * @param target - * The target Sone - * @param trustValue - * The trust value (from {@code -100} to {@code 100}) - */ - public void setTrust(Sone origin, Sone target, int trustValue) { - checkNotNull(origin, "origin must not be null"); - checkArgument(origin.getIdentity() instanceof OwnIdentity, "origin must be a local Sone"); - checkNotNull(target, "target must not be null"); - checkArgument((trustValue >= -100) && (trustValue <= 100), "trustValue must be within [-100, 100]"); - webOfTrustUpdater.setTrust((OwnIdentity) origin.getIdentity(), target.getIdentity(), trustValue, preferences.getTrustComment()); - } - - /** - * Removes any trust assignment for the given target Sone. - * - * @param origin - * The trust origin - * @param target - * The trust target - */ - public void removeTrust(Sone origin, Sone target) { - checkNotNull(origin, "origin must not be null"); - checkNotNull(target, "target must not be null"); - checkArgument(origin.getIdentity() instanceof OwnIdentity, "origin must be a local Sone"); - webOfTrustUpdater.setTrust((OwnIdentity) origin.getIdentity(), target.getIdentity(), null, null); - } - - /** - * Assigns the configured positive trust value for the given target. - * - * @param origin - * The trust origin - * @param target - * The trust target - */ - public void trustSone(Sone origin, Sone target) { - setTrust(origin, target, preferences.getPositiveTrust()); - } - - /** - * Assigns the configured negative trust value for the given target. - * - * @param origin - * The trust origin - * @param target - * The trust target - */ - public void distrustSone(Sone origin, Sone target) { - setTrust(origin, target, preferences.getNegativeTrust()); - } - - /** - * Removes the trust assignment for the given target. - * - * @param origin - * The trust origin - * @param target - * The trust target - */ - public void untrustSone(Sone origin, Sone target) { - removeTrust(origin, target); - } - - /** * Updates the stored Sone with the given Sone. * * @param sone @@ -1063,7 +1007,7 @@ public class Core extends AbstractService implements SoneProvider, PostProvider, * The text of the post * @return The created post */ - public Post createPost(Sone sone, Optional recipient, String text) { + public Post createPost(Sone sone, @Nullable Sone recipient, String text) { checkNotNull(text, "text must not be null"); checkArgument(text.trim().length() > 0, "text must not be empty"); if (!sone.isLocal()) { @@ -1072,8 +1016,8 @@ public class Core extends AbstractService implements SoneProvider, PostProvider, } PostBuilder postBuilder = database.newPostBuilder(); postBuilder.from(sone.getId()).randomId().currentTime().withText(text.trim()); - if (recipient.isPresent()) { - postBuilder.to(recipient.get().getId()); + if (recipient != null) { + postBuilder.to(recipient.getId()); } final Post post = postBuilder.build(); database.storePost(post); @@ -1359,7 +1303,8 @@ public class Core extends AbstractService implements SoneProvider, PostProvider, synchronized (soneInserters) { for (Entry soneInserter : soneInserters.entrySet()) { soneInserter.getValue().stop(); - saveSone(soneInserter.getKey()); + Sone latestSone = getLocalSone(soneInserter.getKey().getId()); + saveSone(latestSone); } } synchronized (soneRescuers) { @@ -1501,8 +1446,6 @@ public class Core extends AbstractService implements SoneProvider, PostProvider, configuration.getStringValue(sonePrefix + "/Options/ShowCustomAvatars").setValue(sone.getOptions().getShowCustomAvatars().name()); configuration.getStringValue(sonePrefix + "/Options/LoadLinkedImages").setValue(sone.getOptions().getLoadLinkedImages().name()); - configuration.save(); - webOfTrustUpdater.setProperty((OwnIdentity) sone.getIdentity(), "Sone.LatestEdition", String.valueOf(sone.getLatestEdition())); logger.log(Level.INFO, String.format("Sone %s saved.", sone)); @@ -1540,7 +1483,9 @@ public class Core extends AbstractService implements SoneProvider, PostProvider, database.save(); /* now save it. */ + Stopwatch stopwatch = Stopwatch.createStarted(); configuration.save(); + configurationSaveTimeHistogram.update(stopwatch.elapsed(TimeUnit.MICROSECONDS)); } catch (ConfigurationException ce1) { logger.log(Level.SEVERE, "Could not store configuration!", ce1); @@ -1580,7 +1525,7 @@ public class Core extends AbstractService implements SoneProvider, PostProvider, */ @Subscribe public void ownIdentityAdded(OwnIdentityAddedEvent ownIdentityAddedEvent) { - OwnIdentity ownIdentity = ownIdentityAddedEvent.ownIdentity(); + OwnIdentity ownIdentity = ownIdentityAddedEvent.getOwnIdentity(); logger.log(Level.FINEST, String.format("Adding OwnIdentity: %s", ownIdentity)); if (ownIdentity.hasContext("Sone")) { addLocalSone(ownIdentity); @@ -1595,7 +1540,7 @@ public class Core extends AbstractService implements SoneProvider, PostProvider, */ @Subscribe public void ownIdentityRemoved(OwnIdentityRemovedEvent ownIdentityRemovedEvent) { - OwnIdentity ownIdentity = ownIdentityRemovedEvent.ownIdentity(); + OwnIdentity ownIdentity = ownIdentityRemovedEvent.getOwnIdentity(); logger.log(Level.FINEST, String.format("Removing OwnIdentity: %s", ownIdentity)); trustedIdentities.removeAll(ownIdentity); } @@ -1608,9 +1553,9 @@ public class Core extends AbstractService implements SoneProvider, PostProvider, */ @Subscribe public void identityAdded(IdentityAddedEvent identityAddedEvent) { - Identity identity = identityAddedEvent.identity(); + Identity identity = identityAddedEvent.getIdentity(); logger.log(Level.FINEST, String.format("Adding Identity: %s", identity)); - trustedIdentities.put(identityAddedEvent.ownIdentity(), identity); + trustedIdentities.put(identityAddedEvent.getOwnIdentity(), identity); addRemoteSone(identity); } @@ -1622,7 +1567,7 @@ public class Core extends AbstractService implements SoneProvider, PostProvider, */ @Subscribe public void identityUpdated(IdentityUpdatedEvent identityUpdatedEvent) { - Identity identity = identityUpdatedEvent.identity(); + Identity identity = identityUpdatedEvent.getIdentity(); final Sone sone = getRemoteSone(identity.getId()); if (sone.isLocal()) { return; @@ -1646,8 +1591,8 @@ public class Core extends AbstractService implements SoneProvider, PostProvider, */ @Subscribe public void identityRemoved(IdentityRemovedEvent identityRemovedEvent) { - OwnIdentity ownIdentity = identityRemovedEvent.ownIdentity(); - Identity identity = identityRemovedEvent.identity(); + OwnIdentity ownIdentity = identityRemovedEvent.getOwnIdentity(); + Identity identity = identityRemovedEvent.getIdentity(); trustedIdentities.remove(ownIdentity, identity); for (Entry> trustedIdentity : trustedIdentities.asMap().entrySet()) { if (trustedIdentity.getKey().equals(ownIdentity)) { @@ -1680,9 +1625,9 @@ public class Core extends AbstractService implements SoneProvider, PostProvider, */ @Subscribe public void imageInsertFinished(ImageInsertFinishedEvent imageInsertFinishedEvent) { - logger.log(Level.WARNING, String.format("Image insert finished for %s: %s", imageInsertFinishedEvent.image(), imageInsertFinishedEvent.resultingUri())); - imageInsertFinishedEvent.image().modify().setKey(imageInsertFinishedEvent.resultingUri().toString()).update(); - deleteTemporaryImage(imageInsertFinishedEvent.image().getId()); + logger.log(Level.WARNING, String.format("Image insert finished for %s: %s", imageInsertFinishedEvent.getImage(), imageInsertFinishedEvent.getResultingUri())); + imageInsertFinishedEvent.getImage().modify().setKey(imageInsertFinishedEvent.getResultingUri().toString()).update(); + deleteTemporaryImage(imageInsertFinishedEvent.getImage().getId()); touchConfiguration(); } diff --git a/src/main/java/net/pterodactylus/sone/core/FreenetInterface.java b/src/main/java/net/pterodactylus/sone/core/FreenetInterface.java index 1ed2024..a01a2bb 100644 --- a/src/main/java/net/pterodactylus/sone/core/FreenetInterface.java +++ b/src/main/java/net/pterodactylus/sone/core/FreenetInterface.java @@ -1,5 +1,5 @@ /* - * Sone - FreenetInterface.java - Copyright © 2010–2019 David Roden + * Sone - FreenetInterface.java - Copyright © 2010–2020 David 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 @@ -21,7 +21,6 @@ import static freenet.keys.USK.create; import static java.lang.String.format; import static java.util.logging.Level.WARNING; import static java.util.logging.Logger.getLogger; -import static net.pterodactylus.sone.freenet.Key.routingKey; import java.io.IOException; import java.net.MalformedURLException; @@ -75,6 +74,7 @@ import freenet.support.api.Bucket; import freenet.support.api.RandomAccessBucket; import freenet.support.io.ArrayBucket; import freenet.support.io.ResumeFailedException; +import net.pterodactylus.sone.freenet.*; /** * Contains all necessary functionality for interacting with the Freenet node. @@ -255,7 +255,7 @@ public class FreenetInterface { public void registerActiveUsk(FreenetURI requestUri, USKCallback uskCallback) { try { - soneUskCallbacks.put(routingKey(requestUri), uskCallback); + soneUskCallbacks.put(FreenetURIsKt.getRoutingKeyString(requestUri), uskCallback); node.clientCore.uskManager.subscribe(create(requestUri), uskCallback, true, requestClient); } catch (MalformedURLException mue1) { @@ -267,7 +267,7 @@ public class FreenetInterface { public void registerPassiveUsk(FreenetURI requestUri, USKCallback uskCallback) { try { - soneUskCallbacks.put(routingKey(requestUri), uskCallback); + soneUskCallbacks.put(FreenetURIsKt.getRoutingKeyString(requestUri), uskCallback); node.clientCore .uskManager .subscribe(create(requestUri), uskCallback, false, requestClient); @@ -327,7 +327,7 @@ public class FreenetInterface { }; try { node.clientCore.uskManager.subscribe(USK.create(uri), uskCallback, true, requestClient); - uriUskCallbacks.put(uri, uskCallback); + uriUskCallbacks.put(USK.create(uri).clearCopy().getURI(), uskCallback); } catch (MalformedURLException mue1) { logger.log(Level.WARNING, String.format("Could not subscribe to USK: %s", uri), mue1); } @@ -340,12 +340,12 @@ public class FreenetInterface { * The URI to unregister the USK watcher for */ public void unregisterUsk(FreenetURI uri) { - USKCallback uskCallback = uriUskCallbacks.remove(uri); - if (uskCallback == null) { - logger.log(Level.INFO, String.format("Could not unregister unknown USK: %s", uri)); - return; - } try { + USKCallback uskCallback = uriUskCallbacks.remove(USK.create(uri).clearCopy().getURI()); + if (uskCallback == null) { + logger.log(Level.INFO, String.format("Could not unregister unknown USK: %s", uri)); + return; + } node.clientCore.uskManager.unsubscribe(USK.create(uri), uskCallback); } catch (MalformedURLException mue1) { logger.log(Level.INFO, String.format("Could not unregister invalid USK: %s", uri), mue1); diff --git a/src/main/java/net/pterodactylus/sone/core/ImageInserter.java b/src/main/java/net/pterodactylus/sone/core/ImageInserter.java index fc76ca9..76519f4 100644 --- a/src/main/java/net/pterodactylus/sone/core/ImageInserter.java +++ b/src/main/java/net/pterodactylus/sone/core/ImageInserter.java @@ -1,5 +1,5 @@ /* - * Sone - ImageInserter.java - Copyright © 2011–2019 David Roden + * Sone - ImageInserter.java - Copyright © 2011–2020 David 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 diff --git a/src/main/java/net/pterodactylus/sone/core/Options.java b/src/main/java/net/pterodactylus/sone/core/Options.java index af88dd8..88e33a0 100644 --- a/src/main/java/net/pterodactylus/sone/core/Options.java +++ b/src/main/java/net/pterodactylus/sone/core/Options.java @@ -1,5 +1,5 @@ /* - * Sone - Options.java - Copyright © 2010–2019 David Roden + * Sone - Options.java - Copyright © 2010–2020 David 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 diff --git a/src/main/java/net/pterodactylus/sone/core/PreferencesLoader.java b/src/main/java/net/pterodactylus/sone/core/PreferencesLoader.java deleted file mode 100644 index a730983..0000000 --- a/src/main/java/net/pterodactylus/sone/core/PreferencesLoader.java +++ /dev/null @@ -1,103 +0,0 @@ -package net.pterodactylus.sone.core; - -import net.pterodactylus.sone.fcp.FcpInterface.FullAccessRequired; -import net.pterodactylus.util.config.Configuration; -import net.pterodactylus.util.config.ConfigurationException; - -/** - * Loads preferences stored in a {@link Configuration} into a {@link - * Preferences} object. - */ -public class PreferencesLoader { - - private final Preferences preferences; - - public PreferencesLoader(Preferences preferences) { - this.preferences = preferences; - } - - public void loadFrom(Configuration configuration) { - loadInsertionDelay(configuration); - loadPostsPerPage(configuration); - loadImagesPerPage(configuration); - loadCharactersPerPost(configuration); - loadPostCutOffLength(configuration); - loadRequireFullAccess(configuration); - loadPositiveTrust(configuration); - loadNegativeTrust(configuration); - loadTrustComment(configuration); - loadFcpInterfaceActive(configuration); - loadFcpFullAccessRequired(configuration); - } - - private void loadInsertionDelay(Configuration configuration) { - preferences.setNewInsertionDelay(configuration.getIntValue( - "Option/InsertionDelay").getValue(null)); - } - - private void loadPostsPerPage(Configuration configuration) { - preferences.setNewPostsPerPage( - configuration.getIntValue("Option/PostsPerPage") - .getValue(null)); - } - - private void loadImagesPerPage(Configuration configuration) { - preferences.setNewImagesPerPage( - configuration.getIntValue("Option/ImagesPerPage") - .getValue(null)); - } - - private void loadCharactersPerPost(Configuration configuration) { - preferences.setNewCharactersPerPost( - configuration.getIntValue("Option/CharactersPerPost") - .getValue(null)); - } - - private void loadPostCutOffLength(Configuration configuration) { - try { - preferences.setNewPostCutOffLength( - configuration.getIntValue("Option/PostCutOffLength") - .getValue(null)); - } catch (IllegalArgumentException iae1) { - /* previous versions allowed -1, ignore and use default. */ - } - } - - private void loadRequireFullAccess(Configuration configuration) { - preferences.setNewRequireFullAccess( - configuration.getBooleanValue("Option/RequireFullAccess") - .getValue(null)); - } - - private void loadPositiveTrust(Configuration configuration) { - preferences.setNewPositiveTrust( - configuration.getIntValue("Option/PositiveTrust") - .getValue(null)); - } - - private void loadNegativeTrust(Configuration configuration) { - preferences.setNewNegativeTrust( - configuration.getIntValue("Option/NegativeTrust") - .getValue(null)); - } - - private void loadTrustComment(Configuration configuration) { - preferences.setNewTrustComment( - configuration.getStringValue("Option/TrustComment") - .getValue(null)); - } - - private void loadFcpInterfaceActive(Configuration configuration) { - preferences.setNewFcpInterfaceActive(configuration.getBooleanValue( - "Option/ActivateFcpInterface").getValue(null)); - } - - private void loadFcpFullAccessRequired(Configuration configuration) { - Integer fullAccessRequiredInteger = configuration - .getIntValue("Option/FcpFullAccessRequired").getValue(null); - preferences.setNewFcpFullAccessRequired( - (fullAccessRequiredInteger == null) ? null : - FullAccessRequired.values()[fullAccessRequiredInteger]); - } - -} diff --git a/src/main/java/net/pterodactylus/sone/core/PreferencesLoader.kt b/src/main/java/net/pterodactylus/sone/core/PreferencesLoader.kt new file mode 100644 index 0000000..32c35cb --- /dev/null +++ b/src/main/java/net/pterodactylus/sone/core/PreferencesLoader.kt @@ -0,0 +1,58 @@ +package net.pterodactylus.sone.core + +import net.pterodactylus.sone.fcp.FcpInterface.* +import net.pterodactylus.util.config.* + +/** + * Loads preferences stored in a [Configuration] into a [Preferences] object. + */ +class PreferencesLoader(private val preferences: Preferences) { + + fun loadFrom(configuration: Configuration) { + loadInsertionDelay(configuration) + loadPostsPerPage(configuration) + loadImagesPerPage(configuration) + loadCharactersPerPost(configuration) + loadPostCutOffLength(configuration) + loadRequireFullAccess(configuration) + loadFcpInterfaceActive(configuration) + loadFcpFullAccessRequired(configuration) + } + + private fun loadInsertionDelay(configuration: Configuration) { + preferences.newInsertionDelay = configuration.getIntValue("Option/InsertionDelay").getValue(null) + } + + private fun loadPostsPerPage(configuration: Configuration) { + preferences.newPostsPerPage = configuration.getIntValue("Option/PostsPerPage").getValue(null) + } + + private fun loadImagesPerPage(configuration: Configuration) { + preferences.newImagesPerPage = configuration.getIntValue("Option/ImagesPerPage").getValue(null) + } + + private fun loadCharactersPerPost(configuration: Configuration) { + preferences.newCharactersPerPost = configuration.getIntValue("Option/CharactersPerPost").getValue(null) + } + + private fun loadPostCutOffLength(configuration: Configuration) { + try { + preferences.newPostCutOffLength = configuration.getIntValue("Option/PostCutOffLength").getValue(null) + } catch (iae1: IllegalArgumentException) { /* previous versions allowed -1, ignore and use default. */ + } + } + + private fun loadRequireFullAccess(configuration: Configuration) { + preferences.newRequireFullAccess = configuration.getBooleanValue("Option/RequireFullAccess").getValue(null) + } + + private fun loadFcpInterfaceActive(configuration: Configuration) { + preferences.newFcpInterfaceActive = configuration.getBooleanValue("Option/ActivateFcpInterface").getValue(null) + } + + private fun loadFcpFullAccessRequired(configuration: Configuration) { + val fullAccessRequiredInteger = configuration.getIntValue("Option/FcpFullAccessRequired").getValue(null) + preferences.newFcpFullAccessRequired = fullAccessRequiredInteger?.let { FullAccessRequired.values()[it] } + } + +} diff --git a/src/main/java/net/pterodactylus/sone/core/SoneDownloaderImpl.java b/src/main/java/net/pterodactylus/sone/core/SoneDownloaderImpl.java index ba1c632..0538c8b 100644 --- a/src/main/java/net/pterodactylus/sone/core/SoneDownloaderImpl.java +++ b/src/main/java/net/pterodactylus/sone/core/SoneDownloaderImpl.java @@ -1,5 +1,5 @@ /* - * Sone - SoneDownloaderImpl.java - Copyright © 2010–2019 David Roden + * Sone - SoneDownloaderImpl.java - Copyright © 2010–2020 David 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 @@ -183,7 +183,7 @@ public class SoneDownloaderImpl extends AbstractService implements SoneDownloade * @return The parsed Sone, or {@code null} if the Sone could not be parsed */ private Sone parseSone(Sone originalSone, FetchResult fetchResult, FreenetURI requestUri) { - logger.log(Level.FINEST, String.format("Parsing FetchResult (%d bytes, %s) for %s…", fetchResult.size(), fetchResult.getMimeType(), originalSone)); + logger.finest(() -> format("Parsing FetchResult (%d bytes, %s) for %s…", fetchResult.size(), fetchResult.getMimeType(), originalSone)); Bucket soneBucket = fetchResult.asBucket(); InputStream soneInputStream = null; try { @@ -191,12 +191,12 @@ public class SoneDownloaderImpl extends AbstractService implements SoneDownloade Sone parsedSone = soneParser.parseSone(originalSone, soneInputStream); if (parsedSone != null) { - logger.log(Level.FINER, "Sone %s was successfully parsed.", parsedSone); + logger.finer(() -> format("Sone %s was successfully parsed.", parsedSone)); parsedSone.setLatestEdition(requestUri.getEdition()); } return parsedSone; } catch (Exception e1) { - logger.log(Level.WARNING, String.format("Could not parse Sone from %s!", requestUri), e1); + logger.log(Level.WARNING, e1, () -> format("Could not parse Sone from %s!", requestUri)); } finally { close(soneInputStream); close(soneBucket); diff --git a/src/main/java/net/pterodactylus/sone/core/SoneException.java b/src/main/java/net/pterodactylus/sone/core/SoneException.java index e460ee7..152f629 100644 --- a/src/main/java/net/pterodactylus/sone/core/SoneException.java +++ b/src/main/java/net/pterodactylus/sone/core/SoneException.java @@ -1,5 +1,5 @@ /* - * Sone - SoneException.java - Copyright © 2010–2019 David Roden + * Sone - SoneException.java - Copyright © 2010–2020 David 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 diff --git a/src/main/java/net/pterodactylus/sone/core/SoneInsertException.java b/src/main/java/net/pterodactylus/sone/core/SoneInsertException.java index 8a0ff53..4aa71c1 100644 --- a/src/main/java/net/pterodactylus/sone/core/SoneInsertException.java +++ b/src/main/java/net/pterodactylus/sone/core/SoneInsertException.java @@ -1,5 +1,5 @@ /* - * Sone - SoneInsertException.java - Copyright © 2011–2019 David Roden + * Sone - SoneInsertException.java - Copyright © 2011–2020 David 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 diff --git a/src/main/java/net/pterodactylus/sone/core/SoneInserter.java b/src/main/java/net/pterodactylus/sone/core/SoneInserter.java index 79a6259..7ccb374 100644 --- a/src/main/java/net/pterodactylus/sone/core/SoneInserter.java +++ b/src/main/java/net/pterodactylus/sone/core/SoneInserter.java @@ -1,5 +1,5 @@ /* - * Sone - SoneInserter.java - Copyright © 2010–2019 David Roden + * Sone - SoneInserter.java - Copyright © 2010–2020 David 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 @@ -19,6 +19,7 @@ package net.pterodactylus.sone.core; import static java.lang.String.format; import static java.lang.System.currentTimeMillis; +import static java.util.concurrent.TimeUnit.*; import static java.util.logging.Logger.getLogger; import static net.pterodactylus.sone.data.Album.NOT_EMPTY; @@ -31,10 +32,13 @@ import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; +import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicInteger; import java.util.logging.Level; import java.util.logging.Logger; +import com.codahale.metrics.*; +import com.google.common.base.*; import net.pterodactylus.sone.core.SoneModificationDetector.LockableFingerprintProvider; import net.pterodactylus.sone.core.event.InsertionDelayChangedEvent; import net.pterodactylus.sone.core.event.SoneInsertAbortedEvent; @@ -58,7 +62,6 @@ import net.pterodactylus.util.template.TemplateParser; import net.pterodactylus.util.template.XmlFilter; import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.Charsets; import com.google.common.collect.FluentIterable; import com.google.common.collect.Ordering; import com.google.common.eventbus.EventBus; @@ -105,6 +108,8 @@ public class SoneInserter extends AbstractService { private final SoneModificationDetector soneModificationDetector; private final long delay; private final String soneId; + private final Histogram soneInsertDurationHistogram; + private final Meter soneInsertErrorMeter; /** * Creates a new Sone inserter. @@ -118,8 +123,8 @@ public class SoneInserter extends AbstractService { * @param soneId * The ID of the Sone to insert */ - public SoneInserter(final Core core, EventBus eventBus, FreenetInterface freenetInterface, final String soneId) { - this(core, eventBus, freenetInterface, soneId, new SoneModificationDetector(new LockableFingerprintProvider() { + public SoneInserter(final Core core, EventBus eventBus, FreenetInterface freenetInterface, MetricRegistry metricRegistry, final String soneId) { + this(core, eventBus, freenetInterface, metricRegistry, soneId, new SoneModificationDetector(new LockableFingerprintProvider() { @Override public boolean isLocked() { Sone sone = core.getSone(soneId); @@ -141,11 +146,13 @@ public class SoneInserter extends AbstractService { } @VisibleForTesting - SoneInserter(Core core, EventBus eventBus, FreenetInterface freenetInterface, String soneId, SoneModificationDetector soneModificationDetector, long delay) { + SoneInserter(Core core, EventBus eventBus, FreenetInterface freenetInterface, MetricRegistry metricRegistry, String soneId, SoneModificationDetector soneModificationDetector, long delay) { super("Sone Inserter for “" + soneId + "”", false); this.core = core; this.eventBus = eventBus; this.freenetInterface = freenetInterface; + this.soneInsertDurationHistogram = metricRegistry.histogram("sone.insert.duration", () -> new Histogram(new ExponentiallyDecayingReservoir(3000, 0))); + this.soneInsertErrorMeter = metricRegistry.meter("sone.insert.errors"); this.soneId = soneId; this.soneModificationDetector = soneModificationDetector; this.delay = delay; @@ -229,8 +236,11 @@ public class SoneInserter extends AbstractService { sone.setStatus(SoneStatus.inserting); long insertTime = currentTimeMillis(); eventBus.post(new SoneInsertingEvent(sone)); + Stopwatch stopwatch = Stopwatch.createStarted(); FreenetURI finalUri = freenetInterface.insertDirectory(sone.getInsertUri(), insertInformation.generateManifestEntries(), "index.html"); - eventBus.post(new SoneInsertedEvent(sone, currentTimeMillis() - insertTime, insertInformation.getFingerprint())); + stopwatch.stop(); + soneInsertDurationHistogram.update(stopwatch.elapsed(MICROSECONDS)); + eventBus.post(new SoneInsertedEvent(sone, stopwatch.elapsed(MILLISECONDS), insertInformation.getFingerprint())); /* at this point we might already be stopped. */ if (shouldStop()) { /* if so, bail out, don’t change anything. */ @@ -242,6 +252,7 @@ public class SoneInserter extends AbstractService { success = true; logger.log(Level.INFO, String.format("Inserted Sone “%s” at %s.", sone.getName(), finalUri)); } catch (SoneException se1) { + soneInsertErrorMeter.mark(); eventBus.post(new SoneInsertAbortedEvent(sone, se1)); logger.log(Level.WARNING, String.format("Could not insert Sone “%s”!", sone.getName()), se1); } finally { diff --git a/src/main/java/net/pterodactylus/sone/core/SoneModificationDetector.java b/src/main/java/net/pterodactylus/sone/core/SoneModificationDetector.java index 92fa3dc..e5e20d9 100644 --- a/src/main/java/net/pterodactylus/sone/core/SoneModificationDetector.java +++ b/src/main/java/net/pterodactylus/sone/core/SoneModificationDetector.java @@ -24,7 +24,7 @@ class SoneModificationDetector { private final Ticker ticker; private final LockableFingerprintProvider lockableFingerprintProvider; private final AtomicInteger insertionDelay; - private Optional lastModificationTime; + private Long lastModificationTime; private String lastInsertFingerprint; private String lastCheckFingerprint; @@ -42,18 +42,18 @@ class SoneModificationDetector { public boolean isEligibleForInsert() { if (lockableFingerprintProvider.isLocked()) { - lastModificationTime = absent(); + lastModificationTime = null; lastCheckFingerprint = ""; return false; } String fingerprint = lockableFingerprintProvider.getFingerprint(); if (fingerprint.equals(lastInsertFingerprint)) { - lastModificationTime = absent(); + lastModificationTime = null; lastCheckFingerprint = fingerprint; return false; } if (!Objects.equal(lastCheckFingerprint, fingerprint)) { - lastModificationTime = of(ticker.read()); + lastModificationTime = ticker.read(); lastCheckFingerprint = fingerprint; return false; } @@ -67,11 +67,11 @@ class SoneModificationDetector { public void setFingerprint(String fingerprint) { lastInsertFingerprint = fingerprint; lastCheckFingerprint = lastInsertFingerprint; - lastModificationTime = absent(); + lastModificationTime = null; } private boolean insertionDelayHasPassed() { - return NANOSECONDS.toSeconds(ticker.read() - lastModificationTime.get()) >= insertionDelay.get(); + return NANOSECONDS.toSeconds(ticker.read() - lastModificationTime) >= insertionDelay.get(); } public boolean isModified() { diff --git a/src/main/java/net/pterodactylus/sone/core/SoneParser.java b/src/main/java/net/pterodactylus/sone/core/SoneParser.java index 1f30565..e1fd9ff 100644 --- a/src/main/java/net/pterodactylus/sone/core/SoneParser.java +++ b/src/main/java/net/pterodactylus/sone/core/SoneParser.java @@ -1,38 +1,24 @@ package net.pterodactylus.sone.core; -import static java.util.logging.Logger.getLogger; -import static net.pterodactylus.sone.utils.NumberParsers.parseInt; -import static net.pterodactylus.sone.utils.NumberParsers.parseLong; +import static java.util.concurrent.TimeUnit.*; +import static java.util.logging.Logger.*; +import static net.pterodactylus.sone.utils.NumberParsers.*; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.logging.Level; -import java.util.logging.Logger; +import java.io.*; +import java.util.*; +import java.util.logging.*; -import javax.inject.Inject; +import javax.annotation.*; +import javax.inject.*; -import net.pterodactylus.sone.data.Album; -import net.pterodactylus.sone.data.Client; -import net.pterodactylus.sone.data.Image; -import net.pterodactylus.sone.data.Post; -import net.pterodactylus.sone.data.PostReply; -import net.pterodactylus.sone.data.Profile; -import net.pterodactylus.sone.data.Profile.DuplicateField; -import net.pterodactylus.sone.data.Profile.EmptyFieldName; -import net.pterodactylus.sone.data.Sone; -import net.pterodactylus.sone.database.Database; -import net.pterodactylus.sone.database.PostBuilder; -import net.pterodactylus.sone.database.PostReplyBuilder; -import net.pterodactylus.sone.database.SoneBuilder; -import net.pterodactylus.util.xml.SimpleXML; -import net.pterodactylus.util.xml.XML; +import net.pterodactylus.sone.data.*; +import net.pterodactylus.sone.data.Profile.*; +import net.pterodactylus.sone.database.*; +import net.pterodactylus.util.xml.*; -import org.w3c.dom.Document; +import com.codahale.metrics.*; +import com.google.common.base.*; +import org.w3c.dom.*; /** * Parses a {@link Sone} from an XML {@link InputStream}. @@ -42,15 +28,19 @@ public class SoneParser { private static final Logger logger = getLogger(SoneParser.class.getName()); private static final int MAX_PROTOCOL_VERSION = 0; private final Database database; + private final Histogram soneParsingDurationHistogram; @Inject - public SoneParser(Database database) { + public SoneParser(Database database, MetricRegistry metricRegistry) { this.database = database; + this.soneParsingDurationHistogram = metricRegistry.histogram("sone.parse.duration", () -> new Histogram(new ExponentiallyDecayingReservoir(3000, 0))); } + @Nullable public Sone parseSone(Sone originalSone, InputStream soneInputStream) throws SoneException { /* TODO - impose a size limit? */ + Stopwatch stopwatch = Stopwatch.createStarted(); Document document; /* XML parsing is not thread-safe. */ synchronized (this) { @@ -257,6 +247,7 @@ public class SoneParser { SimpleXML albumsXml = soneXml.getNode("albums"); Map allImages = new HashMap<>(); List topLevelAlbums = new ArrayList<>(); + Map allAlbums = new HashMap<>(); if (albumsXml != null) { for (SimpleXML albumXml : albumsXml.getNodes("album")) { String id = albumXml.getValue("id", null); @@ -269,7 +260,7 @@ public class SoneParser { } Album parent = null; if (parentId != null) { - parent = database.getAlbum(parentId); + parent = allAlbums.get(parentId); if (parent == null) { logger.log(Level.WARNING, String.format("Downloaded Sone %s has album with invalid parent!", sone)); return null; @@ -288,6 +279,7 @@ public class SoneParser { } else { topLevelAlbums.add(album); } + allAlbums.put(album.getId(), album); SimpleXML imagesXml = albumXml.getNode("images"); if (imagesXml != null) { for (SimpleXML imageXml : imagesXml.getNodes("image")) { @@ -334,6 +326,11 @@ public class SoneParser { sone.getRootAlbum().addAlbum(album); } + // record the duration + stopwatch.stop(); + soneParsingDurationHistogram.update(stopwatch.elapsed(MICROSECONDS)); + logger.fine(() -> "Parsed " + originalSone.getIdentity().getId() + "@" + originalSone.getLatestEdition() + " in " + stopwatch.elapsed(MICROSECONDS) + "μs."); + return sone; } diff --git a/src/main/java/net/pterodactylus/sone/core/SoneRescuer.java b/src/main/java/net/pterodactylus/sone/core/SoneRescuer.java index 2246ac9..2a1d14d 100644 --- a/src/main/java/net/pterodactylus/sone/core/SoneRescuer.java +++ b/src/main/java/net/pterodactylus/sone/core/SoneRescuer.java @@ -1,5 +1,5 @@ /* - * Sone - SoneRescuer.java - Copyright © 2011–2019 David Roden + * Sone - SoneRescuer.java - Copyright © 2011–2020 David 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 diff --git a/src/main/java/net/pterodactylus/sone/core/SoneUri.java b/src/main/java/net/pterodactylus/sone/core/SoneUri.java index 373077f..a929950 100644 --- a/src/main/java/net/pterodactylus/sone/core/SoneUri.java +++ b/src/main/java/net/pterodactylus/sone/core/SoneUri.java @@ -1,5 +1,5 @@ /* - * Sone - SoneUri.java - Copyright © 2013–2019 David Roden + * Sone - SoneUri.java - Copyright © 2013–2020 David 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 diff --git a/src/main/java/net/pterodactylus/sone/core/UpdateChecker.java b/src/main/java/net/pterodactylus/sone/core/UpdateChecker.java deleted file mode 100644 index 9f6963f..0000000 --- a/src/main/java/net/pterodactylus/sone/core/UpdateChecker.java +++ /dev/null @@ -1,246 +0,0 @@ -/* - * Sone - UpdateChecker.java - Copyright © 2011–2019 David 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 . - */ - -package net.pterodactylus.sone.core; - -import static java.util.logging.Logger.getLogger; - -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.net.MalformedURLException; -import java.util.Date; -import java.util.Properties; -import java.util.logging.Level; -import java.util.logging.Logger; - -import javax.inject.Singleton; - -import net.pterodactylus.sone.core.event.UpdateFoundEvent; -import net.pterodactylus.sone.main.PluginHomepage; -import net.pterodactylus.sone.main.SonePlugin; -import net.pterodactylus.util.io.Closer; -import net.pterodactylus.util.version.Version; - -import com.google.common.eventbus.EventBus; -import com.google.inject.Inject; - -import freenet.keys.FreenetURI; -import freenet.support.api.Bucket; - -/** - * Watches the official Sone homepage for new releases. - */ -@Singleton -public class UpdateChecker { - - /** The logger. */ - private static final Logger logger = getLogger(UpdateChecker.class.getName()); - - /** The event bus. */ - private final EventBus eventBus; - - /** The Freenet interface. */ - private final FreenetInterface freenetInterface; - - /** The current URI of the homepage. */ - private FreenetURI currentUri; - - /** The latest known edition. */ - private long latestEdition = SonePlugin.getLatestEdition(); - - /** The current latest known version. */ - private Version currentLatestVersion; - private final Version currentRunningVersion; - - /** The release date of the latest version. */ - private long latestVersionDate; - - private final PluginHomepage pluginHomepage; - - /** - * Creates a new update checker. - * - * @param eventBus - * The event bus - * @param freenetInterface - * The freenet interface to use - */ - @Inject - public UpdateChecker(EventBus eventBus, FreenetInterface freenetInterface, Version currentVersion, PluginHomepage pluginHomepage) { - this.eventBus = eventBus; - this.freenetInterface = freenetInterface; - this.currentRunningVersion = currentVersion; - this.currentLatestVersion = currentVersion; - this.pluginHomepage = pluginHomepage; - } - - // - // ACCESSORS - // - - /** - * Returns whether a version that is later than the currently running - * version has been found. - * - * @return {@code true} if a new version was found - */ - public boolean hasLatestVersion() { - return currentLatestVersion.compareTo(currentRunningVersion) > 0; - } - - /** - * Returns the latest version. If no new latest version has been found, the - * current version is returned. - * - * @return The latest known version - */ - public Version getLatestVersion() { - return currentLatestVersion; - } - - /** - * Returns the release time of the latest version. If no new latest version - * has been found, the returned value is undefined. - * - * @return The release time of the latest version, if a new version was - * found - */ - public long getLatestVersionDate() { - return latestVersionDate; - } - - /** - * Returns the latest known edition of the Sone homepage. - * - * @return The latest edition of the Sone homepage - */ - public long getLatestEdition() { - return latestEdition; - } - - // - // ACTIONS - // - - /** - * Starts the update checker. - */ - public void start() { - try { - currentUri = new FreenetURI(pluginHomepage.getHomepage()); - } catch (MalformedURLException mue1) { - /* this can not really happen unless I screw up. */ - logger.log(Level.SEVERE, "Sone Homepage URI invalid!", mue1); - } - freenetInterface.registerUsk(currentUri, new FreenetInterface.Callback() { - - @Override - @SuppressWarnings("synthetic-access") - public void editionFound(FreenetURI uri, long edition, boolean newKnownGood, boolean newSlot) { - logger.log(Level.FINEST, String.format("Found update for %s: %d, %s, %s", uri, edition, newKnownGood, newSlot)); - if (newKnownGood || newSlot) { - Fetched uriResult = freenetInterface.fetchUri(uri.setMetaString(new String[] { "sone.properties" })); - if (uriResult == null) { - logger.log(Level.WARNING, String.format("Could not fetch properties of latest homepage: %s", uri)); - return; - } - Bucket resultBucket = uriResult.getFetchResult().asBucket(); - try { - parseProperties(resultBucket.getInputStream(), edition); - latestEdition = edition; - } catch (IOException ioe1) { - logger.log(Level.WARNING, String.format("Could not parse sone.properties of %s!", uri), ioe1); - } finally { - resultBucket.free(); - } - } - } - }); - } - - /** - * Stops the update checker. - */ - public void stop() { - freenetInterface.unregisterUsk(currentUri); - } - - // - // PRIVATE ACTIONS - // - - /** - * Parses the properties of the latest version and fires events, if - * necessary. - * - * @see UpdateFoundEvent - * @param propertiesInputStream - * The input stream to parse - * @param edition - * The latest edition of the Sone homepage - * @throws IOException - * if an I/O error occured - */ - private void parseProperties(InputStream propertiesInputStream, long edition) throws IOException { - Properties properties = new Properties(); - InputStreamReader inputStreamReader = null; - try { - inputStreamReader = new InputStreamReader(propertiesInputStream, "UTF-8"); - properties.load(inputStreamReader); - } finally { - Closer.close(inputStreamReader); - } - String versionString = properties.getProperty("CurrentVersion/Version"); - String releaseTimeString = properties.getProperty("CurrentVersion/ReleaseTime"); - if ((versionString == null) || (releaseTimeString == null)) { - logger.log(Level.INFO, "Invalid data parsed from properties."); - return; - } - Version version = Version.parse(versionString); - long releaseTime = 0; - try { - releaseTime = Long.parseLong(releaseTimeString); - } catch (NumberFormatException nfe1) { - /* ignore. */ - } - if ((version == null) || (releaseTime == 0)) { - logger.log(Level.INFO, "Could not parse data from properties."); - return; - } - if (version.compareTo(currentLatestVersion) > 0) { - currentLatestVersion = version; - latestVersionDate = releaseTime; - boolean disruptive = disruptiveVersionBetweenCurrentAndFound(properties); - logger.log(Level.INFO, String.format("Found new version: %s (%tc%s)", version, new Date(releaseTime), disruptive ? ", disruptive" : "")); - eventBus.post(new UpdateFoundEvent(version, releaseTime, edition, disruptive)); - } - } - - private boolean disruptiveVersionBetweenCurrentAndFound(Properties properties) { - for (String key : properties.stringPropertyNames()) { - if (key.startsWith("DisruptiveVersion/")) { - Version disruptiveVersion = Version.parse(key.substring("DisruptiveVersion/".length())); - if (disruptiveVersion.compareTo(currentRunningVersion) > 0) { - return true; - } - } - } - return false; - } - -} diff --git a/src/main/java/net/pterodactylus/sone/core/WebOfTrustUpdater.java b/src/main/java/net/pterodactylus/sone/core/WebOfTrustUpdater.java index 35c3203..95d066d 100644 --- a/src/main/java/net/pterodactylus/sone/core/WebOfTrustUpdater.java +++ b/src/main/java/net/pterodactylus/sone/core/WebOfTrustUpdater.java @@ -1,6 +1,5 @@ package net.pterodactylus.sone.core; -import net.pterodactylus.sone.freenet.wot.Identity; import net.pterodactylus.sone.freenet.wot.OwnIdentity; import net.pterodactylus.util.service.Service; @@ -12,7 +11,6 @@ import com.google.inject.ImplementedBy; @ImplementedBy(WebOfTrustUpdaterImpl.class) public interface WebOfTrustUpdater extends Service { - void setTrust(OwnIdentity truster, Identity trustee, Integer score, String comment); boolean addContextWait(OwnIdentity ownIdentity, String context); void removeContext(OwnIdentity ownIdentity, String context); void setProperty(OwnIdentity ownIdentity, String propertyName, String propertyValue); diff --git a/src/main/java/net/pterodactylus/sone/core/WebOfTrustUpdaterImpl.java b/src/main/java/net/pterodactylus/sone/core/WebOfTrustUpdaterImpl.java index 809ca20..da27152 100644 --- a/src/main/java/net/pterodactylus/sone/core/WebOfTrustUpdaterImpl.java +++ b/src/main/java/net/pterodactylus/sone/core/WebOfTrustUpdaterImpl.java @@ -1,5 +1,5 @@ /* - * Sone - WebOfTrustUpdaterImpl.java - Copyright © 2013–2019 David Roden + * Sone - WebOfTrustUpdaterImpl.java - Copyright © 2013–2020 David 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 @@ -26,11 +26,8 @@ import java.util.logging.Level; import java.util.logging.Logger; import net.pterodactylus.sone.freenet.plugin.PluginException; -import net.pterodactylus.sone.freenet.wot.Identity; import net.pterodactylus.sone.freenet.wot.OwnIdentity; -import net.pterodactylus.sone.freenet.wot.Trust; import net.pterodactylus.sone.freenet.wot.WebOfTrustConnector; -import net.pterodactylus.sone.freenet.wot.WebOfTrustException; import net.pterodactylus.util.service.AbstractService; import com.google.common.annotations.VisibleForTesting; @@ -74,34 +71,6 @@ public class WebOfTrustUpdaterImpl extends AbstractService implements WebOfTrust // /** - * Updates the trust relation between the truster and the trustee. This method - * will return immediately and perform a trust update in the background. - * - * @param truster - * The identity giving the trust - * @param trustee - * The identity receiving the trust - * @param score - * The new level of trust (from -100 to 100, may be {@code null} to remove - * the trust completely) - * @param comment - * The comment of the trust relation - */ - @Override - public void setTrust(OwnIdentity truster, Identity trustee, Integer score, String comment) { - SetTrustJob setTrustJob = new SetTrustJob(truster, trustee, score, comment); - if (updateJobs.contains(setTrustJob)) { - updateJobs.remove(setTrustJob); - } - logger.log(Level.FINER, "Adding Trust Update Job: " + setTrustJob); - try { - updateJobs.put(setTrustJob); - } catch (InterruptedException e) { - /* the queue is unbounded so it should never block. */ - } - } - - /** * Adds the given context to the given own identity, waiting for completion of * the operation. * @@ -297,91 +266,6 @@ public class WebOfTrustUpdaterImpl extends AbstractService implements WebOfTrust } /** - * Update job that sets the trust relation between two identities. - */ - @VisibleForTesting - class SetTrustJob extends WebOfTrustUpdateJob { - - /** The identity giving the trust. */ - private final OwnIdentity truster; - - /** The identity receiving the trust. */ - private final Identity trustee; - - /** The score of the relation. */ - private final Integer score; - - /** The comment of the relation. */ - private final String comment; - - /** - * Creates a new set trust job. - * - * @param truster - * The identity giving the trust - * @param trustee - * The identity receiving the trust - * @param score - * The score of the trust (from -100 to 100, may be {@code null} to remote - * the trust relation completely) - * @param comment - * The comment of the trust relation - */ - public SetTrustJob(OwnIdentity truster, Identity trustee, Integer score, String comment) { - this.truster = checkNotNull(truster, "truster must not be null"); - this.trustee = checkNotNull(trustee, "trustee must not be null"); - this.score = score; - this.comment = comment; - } - - /** {@inheritDoc} */ - @Override - @SuppressWarnings("synthetic-access") - public void run() { - try { - if (score != null) { - webOfTrustConnector.setTrust(truster, trustee, score, comment); - trustee.setTrust(truster, new Trust(score, null, 0)); - } else { - webOfTrustConnector.removeTrust(truster, trustee); - trustee.removeTrust(truster); - } - finish(true); - } catch (WebOfTrustException wote1) { - logger.log(Level.WARNING, "Could not set Trust value for " + truster + " -> " + trustee + " to " + score + " (" + comment + ")!", wote1); - finish(false); - } - } - - // - // OBJECT METHODS - // - - /** {@inheritDoc} */ - @Override - public boolean equals(Object object) { - if ((object == null) || !object.getClass().equals(getClass())) { - return false; - } - SetTrustJob updateJob = (SetTrustJob) object; - return updateJob.truster.equals(truster) && updateJob.trustee.equals(trustee); - } - - /** {@inheritDoc} */ - @Override - public int hashCode() { - return getClass().hashCode() ^ truster.hashCode() ^ trustee.hashCode(); - } - - /** {@inheritDoc} */ - @Override - public String toString() { - return String.format("%s[truster=%s,trustee=%s]", getClass().getSimpleName(), truster.getId(), trustee.getId()); - } - - } - - /** * Base class for context updates of an {@link OwnIdentity}. */ @VisibleForTesting diff --git a/src/main/java/net/pterodactylus/sone/core/event/ImageEvent.java b/src/main/java/net/pterodactylus/sone/core/event/ImageEvent.java deleted file mode 100644 index 6daef1a..0000000 --- a/src/main/java/net/pterodactylus/sone/core/event/ImageEvent.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Sone - ImageEvent.java - Copyright © 2013–2019 David 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 . - */ - -package net.pterodactylus.sone.core.event; - -import net.pterodactylus.sone.data.Image; - -/** - * Base class for {@link Image} events. - */ -public abstract class ImageEvent { - - /** The image this event is about. */ - private final Image image; - - /** - * Creates a new image event. - * - * @param image - * The image this event is about - */ - protected ImageEvent(Image image) { - this.image = image; - } - - // - // ACCESSORS - // - - /** - * Returns the image this event is about. - * - * @return The image this event is about - */ - public Image image() { - return image; - } - -} diff --git a/src/main/java/net/pterodactylus/sone/core/event/ImageInsertAbortedEvent.java b/src/main/java/net/pterodactylus/sone/core/event/ImageInsertAbortedEvent.java deleted file mode 100644 index 2bfa3a2..0000000 --- a/src/main/java/net/pterodactylus/sone/core/event/ImageInsertAbortedEvent.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Sone - ImageInsertAbortedEvent.java - Copyright © 2013–2019 David 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 . - */ - -package net.pterodactylus.sone.core.event; - -import net.pterodactylus.sone.data.Image; - -/** - * Event that signals that an {@link Image} insert is aborted. - */ -public class ImageInsertAbortedEvent extends ImageEvent { - - /** - * Creates a new “image insert aborted” event. - * - * @param image - * The image whose insert aborted - */ - public ImageInsertAbortedEvent(Image image) { - super(image); - } - -} diff --git a/src/main/java/net/pterodactylus/sone/core/event/ImageInsertFailedEvent.java b/src/main/java/net/pterodactylus/sone/core/event/ImageInsertFailedEvent.java deleted file mode 100644 index 430c64b..0000000 --- a/src/main/java/net/pterodactylus/sone/core/event/ImageInsertFailedEvent.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Sone - ImageInsertFailedEvent.java - Copyright © 2013–2019 David 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 . - */ - -package net.pterodactylus.sone.core.event; - -import net.pterodactylus.sone.data.Image; - -/** - * Event that signals that an {@link Image} insert has failed. - */ -public class ImageInsertFailedEvent extends ImageEvent { - - /** The cause of the insert failure. */ - private final Throwable cause; - - /** - * Creates a new “image insert failed” event. - * - * @param image - * The image whose insert failed - * @param cause - * The cause of the insert failure - */ - public ImageInsertFailedEvent(Image image, Throwable cause) { - super(image); - this.cause = cause; - } - - // - // ACCESSORS - // - - /** - * Returns the cause of the insert failure. - * - * @return The cause of the insert failure - */ - public Throwable cause() { - return cause; - } - -} diff --git a/src/main/java/net/pterodactylus/sone/core/event/ImageInsertFinishedEvent.java b/src/main/java/net/pterodactylus/sone/core/event/ImageInsertFinishedEvent.java deleted file mode 100644 index 4419311..0000000 --- a/src/main/java/net/pterodactylus/sone/core/event/ImageInsertFinishedEvent.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Sone - ImageInsertFinishedEvent.java - Copyright © 2013–2019 David 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 . - */ - -package net.pterodactylus.sone.core.event; - -import net.pterodactylus.sone.data.Image; -import freenet.keys.FreenetURI; - -/** - * Event that signals that an {@link Image} insert is finished. - */ -public class ImageInsertFinishedEvent extends ImageEvent { - - /** The URI of the image. */ - private final FreenetURI resultingUri; - - /** - * Creates a new “image insert finished” event. - * - * @param image - * The image whose insert finished - * @param resultingUri - * The resulting URI of the image - */ - public ImageInsertFinishedEvent(Image image, FreenetURI resultingUri) { - super(image); - this.resultingUri = resultingUri; - } - - // - // ACCESSORS - // - - /** - * Returns the URI of the image. - * - * @return The URI of the image - */ - public FreenetURI resultingUri() { - return resultingUri; - } - -} diff --git a/src/main/java/net/pterodactylus/sone/core/event/ImageInsertStartedEvent.java b/src/main/java/net/pterodactylus/sone/core/event/ImageInsertStartedEvent.java deleted file mode 100644 index ac9e3e3..0000000 --- a/src/main/java/net/pterodactylus/sone/core/event/ImageInsertStartedEvent.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Sone - ImageInsertStartedEvent.java - Copyright © 2013–2019 David 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 . - */ - -package net.pterodactylus.sone.core.event; - -import net.pterodactylus.sone.data.Image; - -/** - * Event that signals that an {@link Image} is not being inserted. - */ -public class ImageInsertStartedEvent extends ImageEvent { - - /** - * Creates a new “image is inserted” event. - * - * @param image - * The image that is being inserted - */ - public ImageInsertStartedEvent(Image image) { - super(image); - } - -} diff --git a/src/main/java/net/pterodactylus/sone/core/event/MarkPostKnownEvent.java b/src/main/java/net/pterodactylus/sone/core/event/MarkPostKnownEvent.java deleted file mode 100644 index 7c8a6c3..0000000 --- a/src/main/java/net/pterodactylus/sone/core/event/MarkPostKnownEvent.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Sone - MarkPostKnownEvent.java - Copyright © 2013–2019 David 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 . - */ - -package net.pterodactylus.sone.core.event; - -import net.pterodactylus.sone.data.Post; - -/** - * Event that signals that a {@link Post} has been marked as - * {@link Post#isKnown() known}. - */ -public class MarkPostKnownEvent extends PostEvent { - - /** - * Creates a new “post marked known” event. - * - * @param post - * The post that was marked as known - */ - public MarkPostKnownEvent(Post post) { - super(post); - } - -} diff --git a/src/main/java/net/pterodactylus/sone/core/event/MarkPostReplyKnownEvent.java b/src/main/java/net/pterodactylus/sone/core/event/MarkPostReplyKnownEvent.java deleted file mode 100644 index 4c0e2fc..0000000 --- a/src/main/java/net/pterodactylus/sone/core/event/MarkPostReplyKnownEvent.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Sone - MarkPostReplyKnownEvent.java - Copyright © 2013–2019 David 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 . - */ - -package net.pterodactylus.sone.core.event; - -import net.pterodactylus.sone.data.PostReply; - -/** - * Event that signals that a {@link PostReply} has been marked as - * {@link PostReply#isKnown() known}. - */ -public class MarkPostReplyKnownEvent extends PostReplyEvent { - - /** - * Creates a new “post reply marked known” event. - * - * @param postReply - * The post reply that was marked as known - */ - public MarkPostReplyKnownEvent(PostReply postReply) { - super(postReply); - } - -} diff --git a/src/main/java/net/pterodactylus/sone/core/event/MarkSoneKnownEvent.java b/src/main/java/net/pterodactylus/sone/core/event/MarkSoneKnownEvent.java deleted file mode 100644 index 5bbdf5e..0000000 --- a/src/main/java/net/pterodactylus/sone/core/event/MarkSoneKnownEvent.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Sone - MarkSoneKnownEvent.java - Copyright © 2013–2019 David 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 . - */ - -package net.pterodactylus.sone.core.event; - -import net.pterodactylus.sone.data.Sone; - -/** - * Event that signals that a {@link Sone} has been marked as - * {@link Sone#isKnown() known}. - */ -public class MarkSoneKnownEvent extends SoneEvent { - - /** - * Creates a new “Sone marked known” event. - * - * @param sone - * The Sone that was marked as known - */ - public MarkSoneKnownEvent(Sone sone) { - super(sone); - } - -} diff --git a/src/main/java/net/pterodactylus/sone/core/event/NewSoneFoundEvent.java b/src/main/java/net/pterodactylus/sone/core/event/NewSoneFoundEvent.java deleted file mode 100644 index 8fb17d2..0000000 --- a/src/main/java/net/pterodactylus/sone/core/event/NewSoneFoundEvent.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Sone - NewSoneFoundEvent.java - Copyright © 2013–2019 David 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 . - */ - -package net.pterodactylus.sone.core.event; - -import net.pterodactylus.sone.data.Sone; - -/** - * Event that signals that a new remote Sone was found. - */ -public class NewSoneFoundEvent extends SoneEvent { - - /** - * Creates a new “new Sone found” event. - * - * @param sone - * The Sone that was found - */ - public NewSoneFoundEvent(Sone sone) { - super(sone); - } - -} diff --git a/src/main/java/net/pterodactylus/sone/core/event/PostEvent.java b/src/main/java/net/pterodactylus/sone/core/event/PostEvent.java deleted file mode 100644 index 93a61c6..0000000 --- a/src/main/java/net/pterodactylus/sone/core/event/PostEvent.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Sone - PostEvent.java - Copyright © 2013–2019 David 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 . - */ - -package net.pterodactylus.sone.core.event; - -import net.pterodactylus.sone.data.Post; - -/** - * Base class for post events. - */ -public class PostEvent { - - /** The post the event is about. */ - private final Post post; - - /** - * Creates a new post event. - * - * @param post - * The post the event is about - */ - protected PostEvent(Post post) { - this.post = post; - } - - // - // ACCESSORS - // - - /** - * Returns the post the event is about. - * - * @return The post the event is about - */ - public Post post() { - return post; - } - -} diff --git a/src/main/java/net/pterodactylus/sone/core/event/PostReplyEvent.java b/src/main/java/net/pterodactylus/sone/core/event/PostReplyEvent.java deleted file mode 100644 index e31870b..0000000 --- a/src/main/java/net/pterodactylus/sone/core/event/PostReplyEvent.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Sone - PostReplyEvent.java - Copyright © 2013–2019 David 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 . - */ - -package net.pterodactylus.sone.core.event; - -import net.pterodactylus.sone.data.PostReply; - -/** - * Base class for {@link PostReply} events. - */ -public class PostReplyEvent { - - /** The post reply the event is about. */ - private final PostReply postReply; - - /** - * Creates a new post reply event. - * - * @param postReply - * The post reply the event is about - */ - protected PostReplyEvent(PostReply postReply) { - this.postReply = postReply; - } - - // - // ACCESSORS - // - - /** - * Returns the post reply the event is about. - * - * @return The post reply the event is about - */ - public PostReply postReply() { - return postReply; - } - -} diff --git a/src/main/java/net/pterodactylus/sone/core/event/SoneEvent.java b/src/main/java/net/pterodactylus/sone/core/event/SoneEvent.java deleted file mode 100644 index b7497ff..0000000 --- a/src/main/java/net/pterodactylus/sone/core/event/SoneEvent.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Sone - SoneEvent.java - Copyright © 2013–2019 David 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 . - */ - -package net.pterodactylus.sone.core.event; - -import net.pterodactylus.sone.data.Sone; - -/** - * Base class for Sone events. - */ -public abstract class SoneEvent { - - /** The Sone this event is about. */ - private final Sone sone; - - /** - * Creates a new Sone event. - * - * @param sone - * The Sone this event is about - */ - protected SoneEvent(Sone sone) { - this.sone = sone; - } - - // - // ACCESSORS - // - - /** - * Returns the Sone this event is about. - * - * @return The Sone this event is about - */ - public Sone sone() { - return sone; - } - -} diff --git a/src/main/java/net/pterodactylus/sone/core/event/SoneInsertAbortedEvent.java b/src/main/java/net/pterodactylus/sone/core/event/SoneInsertAbortedEvent.java deleted file mode 100644 index a8c9bb6..0000000 --- a/src/main/java/net/pterodactylus/sone/core/event/SoneInsertAbortedEvent.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Sone - SoneInsertAbortedEvent.java - Copyright © 2013–2019 David 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 . - */ - -package net.pterodactylus.sone.core.event; - -import net.pterodactylus.sone.data.Sone; - -/** - * Event that signals that a {@link Sone} insert was aborted. - */ -public class SoneInsertAbortedEvent extends SoneEvent { - - /** The cause of the abortion. */ - private final Throwable cause; - - /** - * Creates a new “Sone was inserted” event. - * - * @param sone - * The Sone that was inserted - * @param cause - * The cause of the abortion - */ - public SoneInsertAbortedEvent(Sone sone, Throwable cause) { - super(sone); - this.cause = cause; - } - - // - // ACCESSORS - // - - /** - * Returns the cause of the abortion. - * - * @return The cause of the abortion (may be {@code null}) - */ - public Throwable cause() { - return cause; - } - -} diff --git a/src/main/java/net/pterodactylus/sone/core/event/SoneInsertedEvent.java b/src/main/java/net/pterodactylus/sone/core/event/SoneInsertedEvent.java deleted file mode 100644 index 53fa935..0000000 --- a/src/main/java/net/pterodactylus/sone/core/event/SoneInsertedEvent.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Sone - SoneInsertedEvent.java - Copyright © 2013–2019 David 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 . - */ - -package net.pterodactylus.sone.core.event; - -import net.pterodactylus.sone.data.Sone; - -/** - * Event that signals that a {@link Sone} was inserted. - */ -public class SoneInsertedEvent extends SoneEvent { - - private final long insertDuration; - private final String insertFingerprint; - - public SoneInsertedEvent(Sone sone, long insertDuration, String insertFingerprint) { - super(sone); - this.insertDuration = insertDuration; - this.insertFingerprint = insertFingerprint; - } - - public long insertDuration() { - return insertDuration; - } - - public String insertFingerprint() { - return insertFingerprint; - } - -} diff --git a/src/main/java/net/pterodactylus/sone/core/event/SoneInsertingEvent.java b/src/main/java/net/pterodactylus/sone/core/event/SoneInsertingEvent.java deleted file mode 100644 index a9ef6fb..0000000 --- a/src/main/java/net/pterodactylus/sone/core/event/SoneInsertingEvent.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Sone - SoneInsertingEvent.java - Copyright © 2013–2019 David 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 . - */ - -package net.pterodactylus.sone.core.event; - -import net.pterodactylus.sone.data.Sone; - -/** - * Event that signals that a {@link Sone} is now being inserted. - */ -public class SoneInsertingEvent extends SoneEvent { - - /** - * Creates a new “Sone is being inserted” event. - * - * @param sone - * The Sone that is being inserted - */ - public SoneInsertingEvent(Sone sone) { - super(sone); - } - -} diff --git a/src/main/java/net/pterodactylus/sone/core/event/SoneLockedEvent.java b/src/main/java/net/pterodactylus/sone/core/event/SoneLockedEvent.java deleted file mode 100644 index cdc9e3a..0000000 --- a/src/main/java/net/pterodactylus/sone/core/event/SoneLockedEvent.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Sone - SoneLockedEvent.java - Copyright © 2013–2019 David 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 . - */ - -package net.pterodactylus.sone.core.event; - -import net.pterodactylus.sone.data.Sone; - -/** - * Event that signals that a {@link Sone} was locked. Only - * {@link Sone#isLocal() local Sones} can be locked. - */ -public class SoneLockedEvent extends SoneEvent { - - /** - * Creates a new “Sone locked” event. - * - * @param sone - * The Sone that was locked - */ - public SoneLockedEvent(Sone sone) { - super(sone); - } - -} diff --git a/src/main/java/net/pterodactylus/sone/core/event/SoneRemovedEvent.java b/src/main/java/net/pterodactylus/sone/core/event/SoneRemovedEvent.java deleted file mode 100644 index e46272d..0000000 --- a/src/main/java/net/pterodactylus/sone/core/event/SoneRemovedEvent.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Sone - SoneRemovedEvent.java - Copyright © 2013–2019 David 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 . - */ - -package net.pterodactylus.sone.core.event; - -import net.pterodactylus.sone.data.Sone; - -/** - * Event that signals that a {@link Sone} was removed. - */ -public class SoneRemovedEvent extends SoneEvent { - - /** - * Creates a new “Sone removed” event. - * - * @param sone - * The Sone that was removed - */ - public SoneRemovedEvent(Sone sone) { - super(sone); - } - -} diff --git a/src/main/java/net/pterodactylus/sone/core/event/SoneUnlockedEvent.java b/src/main/java/net/pterodactylus/sone/core/event/SoneUnlockedEvent.java deleted file mode 100644 index 106d32d..0000000 --- a/src/main/java/net/pterodactylus/sone/core/event/SoneUnlockedEvent.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Sone - SoneUnlockedEvent.java - Copyright © 2013–2019 David 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 . - */ - -package net.pterodactylus.sone.core.event; - -import net.pterodactylus.sone.data.Sone; - -/** - * Event that signals that a {@link Sone} was unlocked. Only - * {@link Sone#isLocal() local Sones} can be locked. - */ -public class SoneUnlockedEvent extends SoneEvent { - - /** - * Creates a new “Sone unlocked” event. - * - * @param sone - * The Sone that was unlocked - */ - public SoneUnlockedEvent(Sone sone) { - super(sone); - } - -} diff --git a/src/main/java/net/pterodactylus/sone/core/event/UpdateFoundEvent.java b/src/main/java/net/pterodactylus/sone/core/event/UpdateFoundEvent.java deleted file mode 100644 index 2884090..0000000 --- a/src/main/java/net/pterodactylus/sone/core/event/UpdateFoundEvent.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Sone - UpdateFoundEvent.java - Copyright © 2013–2019 David 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 . - */ - -package net.pterodactylus.sone.core.event; - -import net.pterodactylus.util.version.Version; - -/** - * Event that signals that an update for Sone was found. - */ -public class UpdateFoundEvent { - - private final Version version; - private final long releaseTime; - private final long latestEdition; - private final boolean disruptive; - - public UpdateFoundEvent(Version version, long releaseTime, long latestEdition, boolean disruptive) { - this.version = version; - this.releaseTime = releaseTime; - this.latestEdition = latestEdition; - this.disruptive = disruptive; - } - - public Version version() { - return version; - } - - public long releaseTime() { - return releaseTime; - } - - public long latestEdition() { - return latestEdition; - } - - public boolean disruptive() { - return disruptive; - } - -} diff --git a/src/main/java/net/pterodactylus/sone/data/Album.java b/src/main/java/net/pterodactylus/sone/data/Album.java index c4af294..576c73a 100644 --- a/src/main/java/net/pterodactylus/sone/data/Album.java +++ b/src/main/java/net/pterodactylus/sone/data/Album.java @@ -1,5 +1,5 @@ /* - * Sone - Album.java - Copyright © 2011–2019 David Roden + * Sone - Album.java - Copyright © 2011–2020 David 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 diff --git a/src/main/java/net/pterodactylus/sone/data/Client.java b/src/main/java/net/pterodactylus/sone/data/Client.java index f23decf..7642957 100644 --- a/src/main/java/net/pterodactylus/sone/data/Client.java +++ b/src/main/java/net/pterodactylus/sone/data/Client.java @@ -1,5 +1,5 @@ /* - * Sone - Client.java - Copyright © 2010–2019 David Roden + * Sone - Client.java - Copyright © 2010–2020 David 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 @@ -81,4 +81,9 @@ public class Client { return Objects.hashCode(name, version); } + @Override + public String toString() { + return name + " " + version; + } + } diff --git a/src/main/java/net/pterodactylus/sone/data/Image.java b/src/main/java/net/pterodactylus/sone/data/Image.java index 93d0702..8414f3a 100644 --- a/src/main/java/net/pterodactylus/sone/data/Image.java +++ b/src/main/java/net/pterodactylus/sone/data/Image.java @@ -1,5 +1,5 @@ /* - * Sone - Image.java - Copyright © 2011–2019 David Roden + * Sone - Image.java - Copyright © 2011–2020 David 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 diff --git a/src/main/java/net/pterodactylus/sone/data/Post.java b/src/main/java/net/pterodactylus/sone/data/Post.java index b6648e2..a4a794e 100644 --- a/src/main/java/net/pterodactylus/sone/data/Post.java +++ b/src/main/java/net/pterodactylus/sone/data/Post.java @@ -1,5 +1,5 @@ /* - * Sone - Post.java - Copyright © 2010–2019 David Roden + * Sone - Post.java - Copyright © 2010–2020 David 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 diff --git a/src/main/java/net/pterodactylus/sone/data/PostReply.java b/src/main/java/net/pterodactylus/sone/data/PostReply.java index dc4a903..f5ffea3 100644 --- a/src/main/java/net/pterodactylus/sone/data/PostReply.java +++ b/src/main/java/net/pterodactylus/sone/data/PostReply.java @@ -1,5 +1,5 @@ /* - * Sone - PostReply.java - Copyright © 2010–2019 David Roden + * Sone - PostReply.java - Copyright © 2010–2020 David 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 diff --git a/src/main/java/net/pterodactylus/sone/data/Profile.java b/src/main/java/net/pterodactylus/sone/data/Profile.java index 8ea09f7..b87ef4f 100644 --- a/src/main/java/net/pterodactylus/sone/data/Profile.java +++ b/src/main/java/net/pterodactylus/sone/data/Profile.java @@ -1,5 +1,5 @@ /* - * Sone - Profile.java - Copyright © 2010–2019 David Roden + * Sone - Profile.java - Copyright © 2010–2020 David 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 diff --git a/src/main/java/net/pterodactylus/sone/data/Reply.java b/src/main/java/net/pterodactylus/sone/data/Reply.java index c596605..120575e 100644 --- a/src/main/java/net/pterodactylus/sone/data/Reply.java +++ b/src/main/java/net/pterodactylus/sone/data/Reply.java @@ -1,5 +1,5 @@ /* - * Sone - Reply.java - Copyright © 2010–2019 David Roden + * Sone - Reply.java - Copyright © 2010–2020 David 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 diff --git a/src/main/java/net/pterodactylus/sone/data/Sone.java b/src/main/java/net/pterodactylus/sone/data/Sone.java index 273b73e..b43d025 100644 --- a/src/main/java/net/pterodactylus/sone/data/Sone.java +++ b/src/main/java/net/pterodactylus/sone/data/Sone.java @@ -1,5 +1,5 @@ /* - * Sone - Sone.java - Copyright © 2010–2019 David Roden + * Sone - Sone.java - Copyright © 2010–2020 David 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 diff --git a/src/main/java/net/pterodactylus/sone/data/TemporaryImage.java b/src/main/java/net/pterodactylus/sone/data/TemporaryImage.java index 378a348..b76c3cd 100644 --- a/src/main/java/net/pterodactylus/sone/data/TemporaryImage.java +++ b/src/main/java/net/pterodactylus/sone/data/TemporaryImage.java @@ -1,5 +1,5 @@ /* - * Sone - TemporaryImage.java - Copyright © 2011–2019 David Roden + * Sone - TemporaryImage.java - Copyright © 2011–2020 David 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 diff --git a/src/main/java/net/pterodactylus/sone/data/impl/AbstractAlbumBuilder.java b/src/main/java/net/pterodactylus/sone/data/impl/AbstractAlbumBuilder.java index 460be4e..04b0eac 100644 --- a/src/main/java/net/pterodactylus/sone/data/impl/AbstractAlbumBuilder.java +++ b/src/main/java/net/pterodactylus/sone/data/impl/AbstractAlbumBuilder.java @@ -1,5 +1,5 @@ /* - * Sone - AbstractAlbumBuilder.java - Copyright © 2013–2019 David Roden + * Sone - AbstractAlbumBuilder.java - Copyright © 2013–2020 David 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 diff --git a/src/main/java/net/pterodactylus/sone/data/impl/AbstractImageBuilder.java b/src/main/java/net/pterodactylus/sone/data/impl/AbstractImageBuilder.java index f545ae9..56ceb6d 100644 --- a/src/main/java/net/pterodactylus/sone/data/impl/AbstractImageBuilder.java +++ b/src/main/java/net/pterodactylus/sone/data/impl/AbstractImageBuilder.java @@ -1,5 +1,5 @@ /* - * Sone - AbstractImageBuilder.java - Copyright © 2013–2019 David Roden + * Sone - AbstractImageBuilder.java - Copyright © 2013–2020 David 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 diff --git a/src/main/java/net/pterodactylus/sone/data/impl/AbstractPostBuilder.java b/src/main/java/net/pterodactylus/sone/data/impl/AbstractPostBuilder.java index cabe8d5..ca927b2 100644 --- a/src/main/java/net/pterodactylus/sone/data/impl/AbstractPostBuilder.java +++ b/src/main/java/net/pterodactylus/sone/data/impl/AbstractPostBuilder.java @@ -1,5 +1,5 @@ /* - * Sone - AbstractPostBuilder.java - Copyright © 2013–2019 David Roden + * Sone - AbstractPostBuilder.java - Copyright © 2013–2020 David 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 diff --git a/src/main/java/net/pterodactylus/sone/data/impl/AbstractPostReplyBuilder.java b/src/main/java/net/pterodactylus/sone/data/impl/AbstractPostReplyBuilder.java index 0fe124e..8dffdcd 100644 --- a/src/main/java/net/pterodactylus/sone/data/impl/AbstractPostReplyBuilder.java +++ b/src/main/java/net/pterodactylus/sone/data/impl/AbstractPostReplyBuilder.java @@ -1,5 +1,5 @@ /* - * Sone - AbstractPostReplyBuilder.java - Copyright © 2013–2019 David Roden + * Sone - AbstractPostReplyBuilder.java - Copyright © 2013–2020 David 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 diff --git a/src/main/java/net/pterodactylus/sone/data/impl/AbstractReplyBuilder.java b/src/main/java/net/pterodactylus/sone/data/impl/AbstractReplyBuilder.java index 052ca2d..7e95f38 100644 --- a/src/main/java/net/pterodactylus/sone/data/impl/AbstractReplyBuilder.java +++ b/src/main/java/net/pterodactylus/sone/data/impl/AbstractReplyBuilder.java @@ -1,5 +1,5 @@ /* - * Sone - AbstractReplyBuilder.java - Copyright © 2013–2019 David Roden + * Sone - AbstractReplyBuilder.java - Copyright © 2013–2020 David 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 diff --git a/src/main/java/net/pterodactylus/sone/data/impl/AlbumBuilderImpl.java b/src/main/java/net/pterodactylus/sone/data/impl/AlbumBuilderImpl.java index 601daa8..696d08d 100644 --- a/src/main/java/net/pterodactylus/sone/data/impl/AlbumBuilderImpl.java +++ b/src/main/java/net/pterodactylus/sone/data/impl/AlbumBuilderImpl.java @@ -1,5 +1,5 @@ /* - * Sone - AlbumBuilderImpl.java - Copyright © 2013–2019 David Roden + * Sone - AlbumBuilderImpl.java - Copyright © 2013–2020 David 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 diff --git a/src/main/java/net/pterodactylus/sone/data/impl/AlbumImpl.java b/src/main/java/net/pterodactylus/sone/data/impl/AlbumImpl.java index 3ec362f..04ab8e9 100644 --- a/src/main/java/net/pterodactylus/sone/data/impl/AlbumImpl.java +++ b/src/main/java/net/pterodactylus/sone/data/impl/AlbumImpl.java @@ -1,5 +1,5 @@ /* - * Sone - AlbumImpl.java - Copyright © 2011–2019 David Roden + * Sone - AlbumImpl.java - Copyright © 2011–2020 David 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 @@ -17,28 +17,17 @@ package net.pterodactylus.sone.data.impl; -import static com.google.common.base.Optional.absent; -import static com.google.common.base.Optional.fromNullable; -import static com.google.common.base.Preconditions.checkArgument; -import static com.google.common.base.Preconditions.checkNotNull; -import static java.nio.charset.StandardCharsets.UTF_8; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.UUID; - -import net.pterodactylus.sone.data.Album; -import net.pterodactylus.sone.data.Image; -import net.pterodactylus.sone.data.Sone; - -import com.google.common.base.Function; -import com.google.common.base.Optional; -import com.google.common.base.Predicates; -import com.google.common.collect.Collections2; -import com.google.common.hash.Hasher; +import java.util.*; +import javax.annotation.*; + +import com.google.common.base.*; +import com.google.common.collect.*; import com.google.common.hash.Hashing; +import com.google.common.hash.*; +import net.pterodactylus.sone.data.*; + +import static com.google.common.base.Preconditions.*; +import static java.nio.charset.StandardCharsets.*; /** * Container for images that can also contain nested {@link AlbumImpl}s. @@ -258,32 +247,33 @@ public class AlbumImpl implements Album { public Modifier modify() throws IllegalStateException { // TODO: reenable check for local Sones return new Modifier() { - private Optional title = absent(); - - private Optional description = absent(); + @Nullable + private String title; + @Nullable + private String description; @Override public Modifier setTitle(String title) { - this.title = fromNullable(title); + this.title = title; return this; } @Override public Modifier setDescription(String description) { - this.description = fromNullable(description); + this.description = description; return this; } @Override public Album update() throws IllegalStateException { - if (title.isPresent() && title.get().trim().isEmpty()) { + if (title != null && title.trim().isEmpty()) { throw new AlbumTitleMustNotBeEmpty(); } - if (title.isPresent()) { - AlbumImpl.this.title = title.get(); + if (title != null) { + AlbumImpl.this.title = title; } - if (description.isPresent()) { - AlbumImpl.this.description = description.get(); + if (description != null) { + AlbumImpl.this.description = description; } return AlbumImpl.this; } diff --git a/src/main/java/net/pterodactylus/sone/data/impl/ImageBuilderImpl.java b/src/main/java/net/pterodactylus/sone/data/impl/ImageBuilderImpl.java index b62cc40..e74b71a 100644 --- a/src/main/java/net/pterodactylus/sone/data/impl/ImageBuilderImpl.java +++ b/src/main/java/net/pterodactylus/sone/data/impl/ImageBuilderImpl.java @@ -1,5 +1,5 @@ /* - * Sone - ImageBuilderImpl.java - Copyright © 2013–2019 David Roden + * Sone - ImageBuilderImpl.java - Copyright © 2013–2020 David 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 diff --git a/src/main/java/net/pterodactylus/sone/data/impl/ImageImpl.java b/src/main/java/net/pterodactylus/sone/data/impl/ImageImpl.java index a54a8de..0dd84fe 100644 --- a/src/main/java/net/pterodactylus/sone/data/impl/ImageImpl.java +++ b/src/main/java/net/pterodactylus/sone/data/impl/ImageImpl.java @@ -1,5 +1,5 @@ /* - * Sone - ImageImpl.java - Copyright © 2011–2019 David Roden + * Sone - ImageImpl.java - Copyright © 2011–2020 David 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 @@ -16,22 +16,14 @@ */ package net.pterodactylus.sone.data.impl; -import static com.google.common.base.Optional.absent; -import static com.google.common.base.Optional.fromNullable; -import static com.google.common.base.Optional.of; -import static com.google.common.base.Preconditions.checkNotNull; -import static com.google.common.base.Preconditions.checkState; -import static java.nio.charset.StandardCharsets.UTF_8; +import java.util.*; +import javax.annotation.*; -import java.util.UUID; +import com.google.common.hash.*; +import net.pterodactylus.sone.data.*; -import net.pterodactylus.sone.data.Album; -import net.pterodactylus.sone.data.Image; -import net.pterodactylus.sone.data.Sone; - -import com.google.common.base.Optional; -import com.google.common.hash.Hasher; -import com.google.common.hash.Hashing; +import static com.google.common.base.Preconditions.*; +import static java.nio.charset.StandardCharsets.*; /** * Container for image metadata. @@ -146,93 +138,94 @@ public class ImageImpl implements Image { public Modifier modify() throws IllegalStateException { // TODO: reenable check for local images return new Modifier() { - private Optional sone = absent(); - - private Optional creationTime = absent(); - - private Optional key = absent(); - - private Optional title = absent(); - - private Optional description = absent(); - - private Optional width = absent(); - - private Optional height = absent(); + @Nullable + private Sone sone; + @Nullable + private Long creationTime; + @Nullable + private String key; + @Nullable + private String title; + @Nullable + private String description; + @Nullable + private Integer width; + @Nullable + private Integer height; @Override public Modifier setSone(Sone sone) { - this.sone = fromNullable(sone); + this.sone = sone; return this; } @Override public Modifier setCreationTime(long creationTime) { - this.creationTime = of(creationTime); + this.creationTime = creationTime; return this; } @Override public Modifier setKey(String key) { - this.key = fromNullable(key); + this.key = key; return this; } @Override public Modifier setTitle(String title) { - this.title = fromNullable(title); + this.title = title; return this; } @Override public Modifier setDescription(String description) { - this.description = fromNullable(description); + this.description = description; return this; } @Override public Modifier setWidth(int width) { - this.width = of(width); + this.width = width; return this; } @Override public Modifier setHeight(int height) { - this.height = of(height); + this.height = height; return this; } @Override public Image update() throws IllegalStateException { - checkState(!sone.isPresent() || (ImageImpl.this.sone == null) || sone.get().equals(ImageImpl.this.sone), "can not change Sone once set"); - checkState(!creationTime.isPresent() || ((ImageImpl.this.creationTime == 0) || (ImageImpl.this.creationTime == creationTime.get())), "can not change creation time once set"); - checkState(!key.isPresent() || (ImageImpl.this.key == null) || key.get().equals(ImageImpl.this.key), "can not change key once set"); - if (title.isPresent() && title.get().trim().isEmpty()) { + checkState(sone == null || (ImageImpl.this.sone == null) || sone.equals(ImageImpl.this.sone), "can not change Sone once set"); + checkState(creationTime == null || ((ImageImpl.this.creationTime == 0) || (ImageImpl.this.creationTime == creationTime)), "can not change creation time once set"); + checkState(key == null || (ImageImpl.this.key == null) || key.equals(ImageImpl.this.key), "can not change key once set"); + if (title != null && title.trim().isEmpty()) { throw new ImageTitleMustNotBeEmpty(); } - checkState(!width.isPresent() || (ImageImpl.this.width == 0) || width.get().equals(ImageImpl.this.width), "can not change width once set"); - checkState(!height.isPresent() || (ImageImpl.this.height == 0) || height.get().equals(ImageImpl.this.height), "can not change height once set"); + checkState(width == null || (ImageImpl.this.width == 0) || width.equals(ImageImpl.this.width), "can not change width once set"); + checkState(height == null || (ImageImpl.this.height == 0) || height.equals(ImageImpl.this.height), "can not change height once set"); - if (sone.isPresent()) { - ImageImpl.this.sone = sone.get(); + if (sone != null) { + ImageImpl.this.sone = sone; } - if (creationTime.isPresent()) { - ImageImpl.this.creationTime = creationTime.get(); + if (creationTime != null) { + ImageImpl.this.creationTime = creationTime; } - if (key.isPresent()) { - ImageImpl.this.key = key.get(); + if (key != null) { + ImageImpl.this.key = key; } - if (title.isPresent()) { - ImageImpl.this.title = title.get(); + if (title != null) { + ImageImpl.this.title = title; } - if (description.isPresent()) { - ImageImpl.this.description = description.get(); + if (description != null) { + ImageImpl.this.description = description; } - if (width.isPresent()) { - ImageImpl.this.width = width.get(); + if (width != null) { + ImageImpl.this.width = width; } - if (height.isPresent()) { - ImageImpl.this.height = height.get(); + if (height != null) { + ImageImpl.this.height = height; } return ImageImpl.this; diff --git a/src/main/java/net/pterodactylus/sone/data/impl/SoneImpl.java b/src/main/java/net/pterodactylus/sone/data/impl/SoneImpl.java index 3366448..1069550 100644 --- a/src/main/java/net/pterodactylus/sone/data/impl/SoneImpl.java +++ b/src/main/java/net/pterodactylus/sone/data/impl/SoneImpl.java @@ -1,5 +1,5 @@ /* - * Sone - SoneImpl.java - Copyright © 2010–2019 David Roden + * Sone - SoneImpl.java - Copyright © 2010–2020 David 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 diff --git a/src/main/java/net/pterodactylus/sone/database/DatabaseException.java b/src/main/java/net/pterodactylus/sone/database/DatabaseException.java index 9763ad6..6a24756 100644 --- a/src/main/java/net/pterodactylus/sone/database/DatabaseException.java +++ b/src/main/java/net/pterodactylus/sone/database/DatabaseException.java @@ -1,5 +1,5 @@ /* - * Sone - DatabaseException.java - Copyright © 2013–2019 David Roden + * Sone - DatabaseException.java - Copyright © 2013–2020 David 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 diff --git a/src/main/java/net/pterodactylus/sone/database/memory/MemoryDatabase.kt b/src/main/java/net/pterodactylus/sone/database/memory/MemoryDatabase.kt index 6fce7e0..8722873 100644 --- a/src/main/java/net/pterodactylus/sone/database/memory/MemoryDatabase.kt +++ b/src/main/java/net/pterodactylus/sone/database/memory/MemoryDatabase.kt @@ -1,5 +1,5 @@ /* - * Sone - MemoryDatabase.kt - Copyright © 2013–2019 David Roden + * Sone - MemoryDatabase.kt - Copyright © 2013–2020 David 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 @@ -21,7 +21,7 @@ import com.google.common.base.Preconditions.checkNotNull import com.google.common.collect.HashMultimap import com.google.common.collect.Multimap import com.google.common.collect.TreeMultimap -import com.google.common.util.concurrent.AbstractService +import com.google.common.util.concurrent.* import com.google.inject.Inject import com.google.inject.Singleton import net.pterodactylus.sone.data.Album @@ -41,7 +41,7 @@ import net.pterodactylus.sone.database.ImageBuilder import net.pterodactylus.sone.database.PostBuilder import net.pterodactylus.sone.database.PostDatabase import net.pterodactylus.sone.database.PostReplyBuilder -import net.pterodactylus.sone.utils.unit +import net.pterodactylus.sone.utils.* import net.pterodactylus.util.config.Configuration import net.pterodactylus.util.config.ConfigurationException import java.util.concurrent.locks.ReentrantReadWriteLock @@ -70,6 +70,9 @@ class MemoryDatabase @Inject constructor(private val configuration: Configuratio private val soneImages: Multimap = HashMultimap.create() private val memoryBookmarkDatabase = MemoryBookmarkDatabase(this, configurationLoader) private val memoryFriendDatabase = MemoryFriendDatabase(configurationLoader) + private val saveRateLimiter: RateLimiter = RateLimiter.create(1.0) + private val saveKnownPostsRateLimiter: RateLimiter = RateLimiter.create(1.0) + private val saveKnownPostRepliesRateLimiter: RateLimiter = RateLimiter.create(1.0) override val soneLoader get() = this::getSone @@ -82,8 +85,10 @@ class MemoryDatabase @Inject constructor(private val configuration: Configuratio override val bookmarkedPosts get() = memoryBookmarkDatabase.bookmarkedPosts override fun save() { - saveKnownPosts() - saveKnownPostReplies() + if (saveRateLimiter.tryAcquire()) { + saveKnownPosts() + saveKnownPostReplies() + } } override fun doStart() { @@ -311,15 +316,17 @@ class MemoryDatabase @Inject constructor(private val configuration: Configuratio } private fun saveKnownPosts() = - try { - readLock.withLock { - knownPosts.forEachIndexed { index, knownPostId -> - configuration.getStringValue("KnownPosts/$index/ID").value = knownPostId + saveKnownPostsRateLimiter.tryAcquire().ifTrue { + try { + readLock.withLock { + knownPosts.forEachIndexed { index, knownPostId -> + configuration.getStringValue("KnownPosts/$index/ID").value = knownPostId + } + configuration.getStringValue("KnownPosts/${knownPosts.size}/ID").value = null } - configuration.getStringValue("KnownPosts/${knownPosts.size}/ID").value = null + } catch (ce1: ConfigurationException) { + throw DatabaseException("Could not save database.", ce1) } - } catch (ce1: ConfigurationException) { - throw DatabaseException("Could not save database.", ce1) } private fun loadKnownPostReplies(): Unit = @@ -331,15 +338,17 @@ class MemoryDatabase @Inject constructor(private val configuration: Configuratio } private fun saveKnownPostReplies() = - try { - readLock.withLock { - knownPostReplies.forEachIndexed { index, knownPostReply -> - configuration.getStringValue("KnownReplies/$index/ID").value = knownPostReply + saveKnownPostRepliesRateLimiter.tryAcquire().ifTrue { + try { + readLock.withLock { + knownPostReplies.forEachIndexed { index, knownPostReply -> + configuration.getStringValue("KnownReplies/$index/ID").value = knownPostReply + } + configuration.getStringValue("KnownReplies/${knownPostReplies.size}/ID").value = null } - configuration.getStringValue("KnownReplies/${knownPostReplies.size}/ID").value = null + } catch (ce1: ConfigurationException) { + throw DatabaseException("Could not save database.", ce1) } - } catch (ce1: ConfigurationException) { - throw DatabaseException("Could not save database.", ce1) } } diff --git a/src/main/java/net/pterodactylus/sone/database/memory/MemoryPost.java b/src/main/java/net/pterodactylus/sone/database/memory/MemoryPost.java index 30fc2b0..451b6b5 100644 --- a/src/main/java/net/pterodactylus/sone/database/memory/MemoryPost.java +++ b/src/main/java/net/pterodactylus/sone/database/memory/MemoryPost.java @@ -1,5 +1,5 @@ /* - * Sone - MemoryPost.java - Copyright © 2010–2019 David Roden + * Sone - MemoryPost.java - Copyright © 2010–2020 David 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 diff --git a/src/main/java/net/pterodactylus/sone/database/memory/MemoryPostBuilder.java b/src/main/java/net/pterodactylus/sone/database/memory/MemoryPostBuilder.java index dea3449..b9e66fe 100644 --- a/src/main/java/net/pterodactylus/sone/database/memory/MemoryPostBuilder.java +++ b/src/main/java/net/pterodactylus/sone/database/memory/MemoryPostBuilder.java @@ -1,5 +1,5 @@ /* - * Sone - MemoryPostBuilder.java - Copyright © 2013–2019 David Roden + * Sone - MemoryPostBuilder.java - Copyright © 2013–2020 David 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 @@ -17,43 +17,30 @@ package net.pterodactylus.sone.database.memory; -import java.util.UUID; +import java.util.*; +import javax.annotation.*; -import net.pterodactylus.sone.data.Post; -import net.pterodactylus.sone.data.impl.AbstractPostBuilder; -import net.pterodactylus.sone.database.PostBuilder; -import net.pterodactylus.sone.database.SoneProvider; +import net.pterodactylus.sone.data.*; +import net.pterodactylus.sone.data.impl.*; +import net.pterodactylus.sone.database.*; /** * {@link PostBuilder} implementation that creates a {@link MemoryPost}. */ class MemoryPostBuilder extends AbstractPostBuilder { - /** The database. */ private final MemoryDatabase database; - /** - * Creates a new memory post builder. - * - * @param memoryDatabase - * The database - * @param soneProvider - * The Sone provider - */ public MemoryPostBuilder(MemoryDatabase memoryDatabase, SoneProvider soneProvider) { super(soneProvider); database = memoryDatabase; } - /** - * {@inheritDocs} - */ + @Nonnull @Override public Post build() throws IllegalStateException { validate(); - Post post = new MemoryPost(database, soneProvider, randomId ? UUID.randomUUID().toString() : id, senderId, recipientId, currentTime ? System.currentTimeMillis() : time, text); - post.setKnown(database.isPostKnown(post)); - return post; + return new MemoryPost(database, soneProvider, randomId ? UUID.randomUUID().toString() : id, senderId, recipientId, currentTime ? System.currentTimeMillis() : time, text); } } diff --git a/src/main/java/net/pterodactylus/sone/database/memory/MemoryPostReply.java b/src/main/java/net/pterodactylus/sone/database/memory/MemoryPostReply.java index 40f0775..e4a8f30 100644 --- a/src/main/java/net/pterodactylus/sone/database/memory/MemoryPostReply.java +++ b/src/main/java/net/pterodactylus/sone/database/memory/MemoryPostReply.java @@ -1,5 +1,5 @@ /* - * Sone - MemoryPostReply.java - Copyright © 2013–2019 David Roden + * Sone - MemoryPostReply.java - Copyright © 2013–2020 David 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 diff --git a/src/main/java/net/pterodactylus/sone/database/memory/MemoryPostReplyBuilder.java b/src/main/java/net/pterodactylus/sone/database/memory/MemoryPostReplyBuilder.java index 3bcfe33..b6e2e24 100644 --- a/src/main/java/net/pterodactylus/sone/database/memory/MemoryPostReplyBuilder.java +++ b/src/main/java/net/pterodactylus/sone/database/memory/MemoryPostReplyBuilder.java @@ -1,5 +1,5 @@ /* - * Sone - MemoryPostReplyBuilder.java - Copyright © 2013–2019 David Roden + * Sone - MemoryPostReplyBuilder.java - Copyright © 2013–2020 David 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 @@ -17,12 +17,12 @@ package net.pterodactylus.sone.database.memory; -import java.util.UUID; +import java.util.*; +import javax.annotation.*; -import net.pterodactylus.sone.data.PostReply; -import net.pterodactylus.sone.data.impl.AbstractPostReplyBuilder; -import net.pterodactylus.sone.database.PostReplyBuilder; -import net.pterodactylus.sone.database.SoneProvider; +import net.pterodactylus.sone.data.*; +import net.pterodactylus.sone.data.impl.*; +import net.pterodactylus.sone.database.*; /** * {@link PostReplyBuilder} implementation that creates {@link MemoryPostReply} @@ -30,35 +30,20 @@ import net.pterodactylus.sone.database.SoneProvider; */ class MemoryPostReplyBuilder extends AbstractPostReplyBuilder { - /** The database. */ private final MemoryDatabase database; - - /** The Sone provider. */ private final SoneProvider soneProvider; - /** - * Creates a new {@link MemoryPostReply} builder. - * - * @param database - * The database - * @param soneProvider - * The Sone provider - */ public MemoryPostReplyBuilder(MemoryDatabase database, SoneProvider soneProvider) { this.database = database; this.soneProvider = soneProvider; } - /** - * {@inheritDocs} - */ + @Nonnull @Override public PostReply build() throws IllegalStateException { validate(); - PostReply postReply = new MemoryPostReply(database, soneProvider, randomId ? UUID.randomUUID().toString() : id, senderId, currentTime ? System.currentTimeMillis() : time, text, postId); - postReply.setKnown(database.isPostReplyKnown(postReply)); - return postReply; + return new MemoryPostReply(database, soneProvider, randomId ? UUID.randomUUID().toString() : id, senderId, currentTime ? System.currentTimeMillis() : time, text, postId); } } diff --git a/src/main/java/net/pterodactylus/sone/fcp/CreatePostCommand.java b/src/main/java/net/pterodactylus/sone/fcp/CreatePostCommand.java index 2c8456d..39a6a1e 100644 --- a/src/main/java/net/pterodactylus/sone/fcp/CreatePostCommand.java +++ b/src/main/java/net/pterodactylus/sone/fcp/CreatePostCommand.java @@ -1,5 +1,5 @@ /* - * Sone - CreatePostCommand.java - Copyright © 2011–2019 David Roden + * Sone - CreatePostCommand.java - Copyright © 2011–2020 David 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 @@ -29,7 +29,7 @@ import freenet.support.SimpleFieldSet; /** * FCP command that creates a new {@link Post}. * - * @see Core#createPost(Sone, Optional, String) + * @see Core#createPost(Sone, Sone, String) */ public class CreatePostCommand extends AbstractSoneCommand { @@ -57,7 +57,7 @@ public class CreatePostCommand extends AbstractSoneCommand { if (sone.equals(recipient)) { return new ErrorResponse("Sone and Recipient must not be the same."); } - Post post = getCore().createPost(sone, Optional.fromNullable(recipient), text); + Post post = getCore().createPost(sone, recipient, text); return new Response("PostCreated", new SimpleFieldSetBuilder().put("Post", post.getId()).get()); } diff --git a/src/main/java/net/pterodactylus/sone/fcp/CreateReplyCommand.java b/src/main/java/net/pterodactylus/sone/fcp/CreateReplyCommand.java index 9f17940..de0f309 100644 --- a/src/main/java/net/pterodactylus/sone/fcp/CreateReplyCommand.java +++ b/src/main/java/net/pterodactylus/sone/fcp/CreateReplyCommand.java @@ -1,5 +1,5 @@ /* - * Sone - CreateReplyCommand.java - Copyright © 2011–2019 David Roden + * Sone - CreateReplyCommand.java - Copyright © 2011–2020 David 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 diff --git a/src/main/java/net/pterodactylus/sone/fcp/DeletePostCommand.java b/src/main/java/net/pterodactylus/sone/fcp/DeletePostCommand.java index c93029f..5d599e1 100644 --- a/src/main/java/net/pterodactylus/sone/fcp/DeletePostCommand.java +++ b/src/main/java/net/pterodactylus/sone/fcp/DeletePostCommand.java @@ -1,5 +1,5 @@ /* - * Sone - DeletePostCommand.java - Copyright © 2011–2019 David Roden + * Sone - DeletePostCommand.java - Copyright © 2011–2020 David 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 diff --git a/src/main/java/net/pterodactylus/sone/fcp/DeleteReplyCommand.java b/src/main/java/net/pterodactylus/sone/fcp/DeleteReplyCommand.java index 4531f93..59f744a 100644 --- a/src/main/java/net/pterodactylus/sone/fcp/DeleteReplyCommand.java +++ b/src/main/java/net/pterodactylus/sone/fcp/DeleteReplyCommand.java @@ -1,5 +1,5 @@ /* - * Sone - DeleteReplyCommand.java - Copyright © 2011–2019 David Roden + * Sone - DeleteReplyCommand.java - Copyright © 2011–2020 David 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 diff --git a/src/main/java/net/pterodactylus/sone/fcp/FcpInterface.java b/src/main/java/net/pterodactylus/sone/fcp/FcpInterface.java index 0b321f4..3bb8777 100644 --- a/src/main/java/net/pterodactylus/sone/fcp/FcpInterface.java +++ b/src/main/java/net/pterodactylus/sone/fcp/FcpInterface.java @@ -1,5 +1,5 @@ /* - * Sone - FcpInterface.java - Copyright © 2011–2019 David Roden + * Sone - FcpInterface.java - Copyright © 2011–2020 David 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 diff --git a/src/main/java/net/pterodactylus/sone/fcp/GetLocalSonesCommand.java b/src/main/java/net/pterodactylus/sone/fcp/GetLocalSonesCommand.java index 1427fe7..3ef919e 100644 --- a/src/main/java/net/pterodactylus/sone/fcp/GetLocalSonesCommand.java +++ b/src/main/java/net/pterodactylus/sone/fcp/GetLocalSonesCommand.java @@ -1,5 +1,5 @@ /* - * Sone - GetLocalSonesCommand.java - Copyright © 2011–2019 David Roden + * Sone - GetLocalSonesCommand.java - Copyright © 2011–2020 David 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 diff --git a/src/main/java/net/pterodactylus/sone/fcp/GetPostCommand.java b/src/main/java/net/pterodactylus/sone/fcp/GetPostCommand.java index d1dc643..8033bcb 100644 --- a/src/main/java/net/pterodactylus/sone/fcp/GetPostCommand.java +++ b/src/main/java/net/pterodactylus/sone/fcp/GetPostCommand.java @@ -1,5 +1,5 @@ /* - * Sone - GetPostCommand.java - Copyright © 2011–2019 David Roden + * Sone - GetPostCommand.java - Copyright © 2011–2020 David 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 diff --git a/src/main/java/net/pterodactylus/sone/fcp/GetPostFeedCommand.java b/src/main/java/net/pterodactylus/sone/fcp/GetPostFeedCommand.java index bf80dc2..821c198 100644 --- a/src/main/java/net/pterodactylus/sone/fcp/GetPostFeedCommand.java +++ b/src/main/java/net/pterodactylus/sone/fcp/GetPostFeedCommand.java @@ -1,5 +1,5 @@ /* - * Sone - GetPostFeedCommand.java - Copyright © 2011–2019 David Roden + * Sone - GetPostFeedCommand.java - Copyright © 2011–2020 David 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 diff --git a/src/main/java/net/pterodactylus/sone/fcp/GetPostsCommand.java b/src/main/java/net/pterodactylus/sone/fcp/GetPostsCommand.java index 9cc131b..2f250a3 100644 --- a/src/main/java/net/pterodactylus/sone/fcp/GetPostsCommand.java +++ b/src/main/java/net/pterodactylus/sone/fcp/GetPostsCommand.java @@ -1,5 +1,5 @@ /* - * Sone - GetPostsCommand.java - Copyright © 2011–2019 David Roden + * Sone - GetPostsCommand.java - Copyright © 2011–2020 David 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 diff --git a/src/main/java/net/pterodactylus/sone/fcp/GetSoneCommand.java b/src/main/java/net/pterodactylus/sone/fcp/GetSoneCommand.java index a4b936b..64d24fe 100644 --- a/src/main/java/net/pterodactylus/sone/fcp/GetSoneCommand.java +++ b/src/main/java/net/pterodactylus/sone/fcp/GetSoneCommand.java @@ -1,5 +1,5 @@ /* - * Sone - GetSoneCommand.java - Copyright © 2011–2019 David Roden + * Sone - GetSoneCommand.java - Copyright © 2011–2020 David 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 diff --git a/src/main/java/net/pterodactylus/sone/fcp/GetSonesCommand.java b/src/main/java/net/pterodactylus/sone/fcp/GetSonesCommand.java index ec43143..83167cd 100644 --- a/src/main/java/net/pterodactylus/sone/fcp/GetSonesCommand.java +++ b/src/main/java/net/pterodactylus/sone/fcp/GetSonesCommand.java @@ -1,5 +1,5 @@ /* - * Sone - GetSonesCommand.java - Copyright © 2011–2019 David Roden + * Sone - GetSonesCommand.java - Copyright © 2011–2020 David 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 diff --git a/src/main/java/net/pterodactylus/sone/fcp/LikePostCommand.java b/src/main/java/net/pterodactylus/sone/fcp/LikePostCommand.java index edd5a43..058045a 100644 --- a/src/main/java/net/pterodactylus/sone/fcp/LikePostCommand.java +++ b/src/main/java/net/pterodactylus/sone/fcp/LikePostCommand.java @@ -1,5 +1,5 @@ /* - * Sone - LikePostCommand.java - Copyright © 2011–2019 David Roden + * Sone - LikePostCommand.java - Copyright © 2011–2020 David 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 diff --git a/src/main/java/net/pterodactylus/sone/fcp/LikeReplyCommand.java b/src/main/java/net/pterodactylus/sone/fcp/LikeReplyCommand.java index 800e43c..acdfb98 100644 --- a/src/main/java/net/pterodactylus/sone/fcp/LikeReplyCommand.java +++ b/src/main/java/net/pterodactylus/sone/fcp/LikeReplyCommand.java @@ -1,5 +1,5 @@ /* - * Sone - LikeReplyCommand.java - Copyright © 2011–2019 David Roden + * Sone - LikeReplyCommand.java - Copyright © 2011–2020 David 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 diff --git a/src/main/java/net/pterodactylus/sone/fcp/LockSoneCommand.java b/src/main/java/net/pterodactylus/sone/fcp/LockSoneCommand.java index 691bc4b..fbceb9a 100644 --- a/src/main/java/net/pterodactylus/sone/fcp/LockSoneCommand.java +++ b/src/main/java/net/pterodactylus/sone/fcp/LockSoneCommand.java @@ -1,5 +1,5 @@ /* - * Sone - LockSoneCommand.java - Copyright © 2013–2019 David Roden + * Sone - LockSoneCommand.java - Copyright © 2013–2020 David 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 diff --git a/src/main/java/net/pterodactylus/sone/fcp/UnlockSoneCommand.java b/src/main/java/net/pterodactylus/sone/fcp/UnlockSoneCommand.java index ca5a59e..d489603 100644 --- a/src/main/java/net/pterodactylus/sone/fcp/UnlockSoneCommand.java +++ b/src/main/java/net/pterodactylus/sone/fcp/UnlockSoneCommand.java @@ -1,5 +1,5 @@ /* - * Sone - UnlockSoneCommand.java - Copyright © 2013–2019 David Roden + * Sone - UnlockSoneCommand.java - Copyright © 2013–2020 David 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 diff --git a/src/main/java/net/pterodactylus/sone/fcp/VersionCommand.java b/src/main/java/net/pterodactylus/sone/fcp/VersionCommand.java index 7acea2d..a7d1cd7 100644 --- a/src/main/java/net/pterodactylus/sone/fcp/VersionCommand.java +++ b/src/main/java/net/pterodactylus/sone/fcp/VersionCommand.java @@ -1,5 +1,5 @@ /* - * Sone - VersionCommand.java - Copyright © 2011–2019 David Roden + * Sone - VersionCommand.java - Copyright © 2011–2020 David 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 diff --git a/src/main/java/net/pterodactylus/sone/freenet/Key.java b/src/main/java/net/pterodactylus/sone/freenet/Key.java deleted file mode 100644 index 6811642..0000000 --- a/src/main/java/net/pterodactylus/sone/freenet/Key.java +++ /dev/null @@ -1,65 +0,0 @@ -package net.pterodactylus.sone.freenet; - -import static freenet.support.Base64.encode; -import static java.lang.String.format; - -import freenet.keys.FreenetURI; - -import com.google.common.annotations.VisibleForTesting; - -/** - * Encapsulates the parts of a {@link FreenetURI} that do not change while - * being converted from SSK to USK and/or back. - */ -public class Key { - - private final byte[] routingKey; - private final byte[] cryptoKey; - private final byte[] extra; - - private Key(byte[] routingKey, byte[] cryptoKey, byte[] extra) { - this.routingKey = routingKey; - this.cryptoKey = cryptoKey; - this.extra = extra; - } - - @VisibleForTesting - public String getRoutingKey() { - return encode(routingKey); - } - - @VisibleForTesting - public String getCryptoKey() { - return encode(cryptoKey); - } - - @VisibleForTesting - public String getExtra() { - return encode(extra); - } - - public FreenetURI toUsk(String docName, long edition, String... paths) { - return new FreenetURI("USK", docName, paths, routingKey, cryptoKey, - extra, edition); - } - - public FreenetURI toSsk(String docName, String... paths) { - return new FreenetURI("SSK", docName, paths, routingKey, cryptoKey, - extra); - } - - public FreenetURI toSsk(String docName, long edition, String... paths) { - return new FreenetURI("SSK", format("%s-%d", docName, edition), paths, - routingKey, cryptoKey, extra, edition); - } - - public static Key from(FreenetURI freenetURI) { - return new Key(freenetURI.getRoutingKey(), freenetURI.getCryptoKey(), - freenetURI.getExtra()); - } - - public static String routingKey(FreenetURI freenetURI) { - return from(freenetURI).getRoutingKey(); - } - -} diff --git a/src/main/java/net/pterodactylus/sone/freenet/L10nFilter.java b/src/main/java/net/pterodactylus/sone/freenet/L10nFilter.java deleted file mode 100644 index d667811..0000000 --- a/src/main/java/net/pterodactylus/sone/freenet/L10nFilter.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Sone - L10nFilter.java - Copyright © 2010–2019 David 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 . - */ - -package net.pterodactylus.sone.freenet; - -import java.text.MessageFormat; -import java.util.ArrayList; -import java.util.List; -import java.util.Locale; -import java.util.Map; - -import javax.annotation.Nonnull; - -import net.pterodactylus.util.template.Filter; -import net.pterodactylus.util.template.TemplateContext; - -import freenet.l10n.BaseL10n; - -/** - * {@link Filter} implementation replaces {@link String} values with their - * translated equivalents. - */ -public class L10nFilter implements Filter { - - private final BaseL10n l10n; - - public L10nFilter(BaseL10n l10n) { - this.l10n = l10n; - } - - /** - * {@inheritDoc} - */ - @Override - public String format(TemplateContext templateContext, Object data, Map parameters) { - List parameterValues = getParameters(data, parameters); - String text = getText(data); - if (parameterValues.isEmpty()) { - return l10n.getString(text); - } - return new MessageFormat(l10n.getString(text), new Locale(l10n.getSelectedLanguage().shortCode)).format(parameterValues.toArray()); - } - - @Nonnull - private String getText(Object data) { - return (data instanceof L10nText) ? ((L10nText) data).getText() : String.valueOf(data); - } - - @Nonnull - private List getParameters(Object data, Map parameters) { - if (data instanceof L10nText) { - return ((L10nText) data).getParameters(); - } - List parameterValues = new ArrayList<>(); - int parameterIndex = 0; - while (parameters.containsKey(String.valueOf(parameterIndex))) { - Object value = parameters.get(String.valueOf(parameterIndex)); - parameterValues.add(value); - ++parameterIndex; - } - return parameterValues; - } - -} diff --git a/src/main/java/net/pterodactylus/sone/freenet/PluginStoreConfigurationBackend.java b/src/main/java/net/pterodactylus/sone/freenet/PluginStoreConfigurationBackend.java index cce544f..a8a2697 100644 --- a/src/main/java/net/pterodactylus/sone/freenet/PluginStoreConfigurationBackend.java +++ b/src/main/java/net/pterodactylus/sone/freenet/PluginStoreConfigurationBackend.java @@ -1,5 +1,5 @@ /* - * Sone - PluginStoreConfigurationBackend.java - Copyright © 2010–2019 David Roden + * Sone - PluginStoreConfigurationBackend.java - Copyright © 2010–2020 David 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 diff --git a/src/main/java/net/pterodactylus/sone/freenet/SimpleFieldSetBuilder.java b/src/main/java/net/pterodactylus/sone/freenet/SimpleFieldSetBuilder.java index 48eb4bf..a974a0c 100644 --- a/src/main/java/net/pterodactylus/sone/freenet/SimpleFieldSetBuilder.java +++ b/src/main/java/net/pterodactylus/sone/freenet/SimpleFieldSetBuilder.java @@ -1,5 +1,5 @@ /* - * Sone - SimpleFieldSetBuilder.java - Copyright © 2011–2019 David Roden + * Sone - SimpleFieldSetBuilder.java - Copyright © 2011–2020 David 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 diff --git a/src/main/java/net/pterodactylus/sone/freenet/fcp/AbstractCommand.java b/src/main/java/net/pterodactylus/sone/freenet/fcp/AbstractCommand.java index 3ea4804..890e3ff 100644 --- a/src/main/java/net/pterodactylus/sone/freenet/fcp/AbstractCommand.java +++ b/src/main/java/net/pterodactylus/sone/freenet/fcp/AbstractCommand.java @@ -1,5 +1,5 @@ /* - * Sone - AbstractCommand.java - Copyright © 2011–2019 David Roden + * Sone - AbstractCommand.java - Copyright © 2011–2020 David 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 diff --git a/src/main/java/net/pterodactylus/sone/freenet/fcp/Command.java b/src/main/java/net/pterodactylus/sone/freenet/fcp/Command.java index 63fc8eb..f3f32cf 100644 --- a/src/main/java/net/pterodactylus/sone/freenet/fcp/Command.java +++ b/src/main/java/net/pterodactylus/sone/freenet/fcp/Command.java @@ -1,5 +1,5 @@ /* - * Sone - Command.java - Copyright © 2011–2019 David Roden + * Sone - Command.java - Copyright © 2011–2020 David 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 diff --git a/src/main/java/net/pterodactylus/sone/freenet/fcp/FcpException.java b/src/main/java/net/pterodactylus/sone/freenet/fcp/FcpException.java index 0e156a1..db70018 100644 --- a/src/main/java/net/pterodactylus/sone/freenet/fcp/FcpException.java +++ b/src/main/java/net/pterodactylus/sone/freenet/fcp/FcpException.java @@ -1,5 +1,5 @@ /* - * Sone - FcpException.java - Copyright © 2011–2019 David Roden + * Sone - FcpException.java - Copyright © 2011–2020 David 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 diff --git a/src/main/java/net/pterodactylus/sone/freenet/plugin/PluginConnector.java b/src/main/java/net/pterodactylus/sone/freenet/plugin/PluginConnector.java deleted file mode 100644 index fbaabb3..0000000 --- a/src/main/java/net/pterodactylus/sone/freenet/plugin/PluginConnector.java +++ /dev/null @@ -1,133 +0,0 @@ -/* - * Sone - PluginConnector.java - Copyright © 2010–2019 David 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 . - */ - -package net.pterodactylus.sone.freenet.plugin; - -import net.pterodactylus.sone.freenet.plugin.event.ReceivedReplyEvent; - -import com.google.common.eventbus.EventBus; -import com.google.inject.Inject; -import com.google.inject.Singleton; - -import freenet.pluginmanager.FredPluginTalker; -import freenet.pluginmanager.PluginNotFoundException; -import freenet.pluginmanager.PluginRespirator; -import freenet.pluginmanager.PluginTalker; -import freenet.support.SimpleFieldSet; -import freenet.support.api.Bucket; - -/** - * Interface for talking to other plugins. Other plugins are identified by their - * name and a unique connection identifier. - */ -@Singleton -public class PluginConnector implements FredPluginTalker { - - /** The event bus. */ - private final EventBus eventBus; - - /** The plugin respirator. */ - private final PluginRespirator pluginRespirator; - - /** - * Creates a new plugin connector. - * - * @param eventBus - * The event bus - * @param pluginRespirator - * The plugin respirator - */ - @Inject - public PluginConnector(EventBus eventBus, PluginRespirator pluginRespirator) { - this.eventBus = eventBus; - this.pluginRespirator = pluginRespirator; - } - - // - // ACTIONS - // - - /** - * Sends a request to the given plugin. - * - * @param pluginName - * The name of the plugin - * @param identifier - * The identifier of the connection - * @param fields - * The fields of the message - * @throws PluginException - * if the plugin can not be found - */ - public void sendRequest(String pluginName, String identifier, SimpleFieldSet fields) throws PluginException { - sendRequest(pluginName, identifier, fields, null); - } - - /** - * Sends a request to the given plugin. - * - * @param pluginName - * The name of the plugin - * @param identifier - * The identifier of the connection - * @param fields - * The fields of the message - * @param data - * The payload of the message (may be null) - * @throws PluginException - * if the plugin can not be found - */ - public void sendRequest(String pluginName, String identifier, SimpleFieldSet fields, Bucket data) throws PluginException { - getPluginTalker(pluginName, identifier).send(fields, data); - } - - // - // PRIVATE METHODS - // - - /** - * Returns the plugin talker for the given plugin connection. - * - * @param pluginName - * The name of the plugin - * @param identifier - * The identifier of the connection - * @return The plugin talker - * @throws PluginException - * if the plugin can not be found - */ - private PluginTalker getPluginTalker(String pluginName, String identifier) throws PluginException { - try { - return pluginRespirator.getPluginTalker(this, pluginName, identifier); - } catch (PluginNotFoundException pnfe1) { - throw new PluginException(pnfe1); - } - } - - // - // INTERFACE FredPluginTalker - // - - /** - * {@inheritDoc} - */ - @Override - public void onReply(String pluginName, String identifier, SimpleFieldSet params, Bucket data) { - eventBus.post(new ReceivedReplyEvent(this, pluginName, identifier, params, data)); - } - -} diff --git a/src/main/java/net/pterodactylus/sone/freenet/plugin/PluginException.java b/src/main/java/net/pterodactylus/sone/freenet/plugin/PluginException.java deleted file mode 100644 index e263a60..0000000 --- a/src/main/java/net/pterodactylus/sone/freenet/plugin/PluginException.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Sone - PluginException.java - Copyright © 2010–2019 David 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 . - */ - -package net.pterodactylus.sone.freenet.plugin; - -import net.pterodactylus.sone.freenet.wot.WebOfTrustException; - -/** - * Exception that signals an error when communicating with a plugin. - */ -public class PluginException extends WebOfTrustException { - - /** - * Creates a new plugin exception. - */ - public PluginException() { - super(); - } - - /** - * Creates a new plugin exception. - * - * @param message - * The message of the exception - */ - public PluginException(String message) { - super(message); - } - - /** - * Creates a new plugin exception. - * - * @param cause - * The cause of the exception - */ - public PluginException(Throwable cause) { - super(cause); - } - - /** - * Creates a new plugin exception. - * - * @param message - * The message of the exception - * @param cause - * The cause of the exception - */ - public PluginException(String message, Throwable cause) { - super(message, cause); - } - -} diff --git a/src/main/java/net/pterodactylus/sone/freenet/plugin/event/ReceivedReplyEvent.java b/src/main/java/net/pterodactylus/sone/freenet/plugin/event/ReceivedReplyEvent.java deleted file mode 100644 index ce2ba7f..0000000 --- a/src/main/java/net/pterodactylus/sone/freenet/plugin/event/ReceivedReplyEvent.java +++ /dev/null @@ -1,115 +0,0 @@ -/* - * Sone - ReceivedReplyEvent.java - Copyright © 2013–2019 David 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 . - */ - -package net.pterodactylus.sone.freenet.plugin.event; - -import net.pterodactylus.sone.freenet.plugin.PluginConnector; -import freenet.support.SimpleFieldSet; -import freenet.support.api.Bucket; - -/** - * Event that signals that a plugin reply was received. - */ -public class ReceivedReplyEvent { - - /** The connector that received the reply. */ - private final PluginConnector pluginConnector; - - /** The name of the plugin that sent the reply. */ - private final String pluginName; - - /** The identifier of the initial request. */ - private final String identifier; - - /** The fields containing the reply. */ - private final SimpleFieldSet fieldSet; - - /** The optional reply data. */ - private final Bucket data; - - /** - * Creates a new “reply received” event. - * - * @param pluginConnector - * The connector that received the event - * @param pluginName - * The name of the plugin that sent the reply - * @param identifier - * The identifier of the initial request - * @param fieldSet - * The fields containing the reply - * @param data - * The optional data of the reply - */ - public ReceivedReplyEvent(PluginConnector pluginConnector, String pluginName, String identifier, SimpleFieldSet fieldSet, Bucket data) { - this.pluginConnector = pluginConnector; - this.pluginName = pluginName; - this.identifier = identifier; - this.fieldSet = fieldSet; - this.data = data; - } - - // - // ACCESSORS - // - - /** - * Returns the plugin connector that received the reply. - * - * @return The plugin connector that received the reply - */ - public PluginConnector pluginConnector() { - return pluginConnector; - } - - /** - * Returns the name of the plugin that sent the reply. - * - * @return The name of the plugin that sent the reply - */ - public String pluginName() { - return pluginName; - } - - /** - * Returns the identifier of the initial request. - * - * @return The identifier of the initial request - */ - public String identifier() { - return identifier; - } - - /** - * Returns the fields containing the reply. - * - * @return The fields containing the reply - */ - public SimpleFieldSet fieldSet() { - return fieldSet; - } - - /** - * Returns the optional data of the reply. - * - * @return The optional data of the reply (may be {@code null}) - */ - public Bucket data() { - return data; - } - -} diff --git a/src/main/java/net/pterodactylus/sone/freenet/wot/Context.java b/src/main/java/net/pterodactylus/sone/freenet/wot/Context.java deleted file mode 100644 index b386bdb..0000000 --- a/src/main/java/net/pterodactylus/sone/freenet/wot/Context.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Sone - Context.java - Copyright © 2014–2019 David 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 . - */ - -package net.pterodactylus.sone.freenet.wot; - -import javax.annotation.Nullable; - -import com.google.common.base.Function; - -/** - * Custom container for the Web of Trust context. This allows easier - * configuration of dependency injection. - */ -public class Context { - - public static final Function extractContext = new Function() { - @Nullable - @Override - public String apply(@Nullable Context context) { - return (context == null) ? null : context.getContext(); - } - }; - - private final String context; - - public Context(String context) { - this.context = context; - } - - public String getContext() { - return context; - } - -} diff --git a/src/main/java/net/pterodactylus/sone/freenet/wot/DefaultIdentity.java b/src/main/java/net/pterodactylus/sone/freenet/wot/DefaultIdentity.java deleted file mode 100644 index 0d92d61..0000000 --- a/src/main/java/net/pterodactylus/sone/freenet/wot/DefaultIdentity.java +++ /dev/null @@ -1,181 +0,0 @@ -/* - * Sone - DefaultIdentity.java - Copyright © 2010–2019 David 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 . - */ - -package net.pterodactylus.sone.freenet.wot; - -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; - -/** - * A Web of Trust identity. - */ -public class DefaultIdentity implements Identity { - - /** The ID of the identity. */ - private final String id; - - /** The nickname of the identity. */ - private final String nickname; - - /** The request URI of the identity. */ - private final String requestUri; - - /** The contexts of the identity. */ - private final Set contexts = Collections.synchronizedSet(new HashSet()); - - /** The properties of the identity. */ - private final Map properties = Collections.synchronizedMap(new HashMap()); - - /** Cached trust. */ - private final Map trustCache = Collections.synchronizedMap(new HashMap()); - - /** - * Creates a new identity. - * - * @param id - * The ID of the identity - * @param nickname - * The nickname of the identity - * @param requestUri - * The request URI of the identity - */ - public DefaultIdentity(String id, String nickname, String requestUri) { - this.id = id; - this.nickname = nickname; - this.requestUri = requestUri; - } - - // - // ACCESSORS - // - - @Override - public String getId() { - return id; - } - - @Override - public String getNickname() { - return nickname; - } - - @Override - public String getRequestUri() { - return requestUri; - } - - @Override - public Set getContexts() { - return Collections.unmodifiableSet(contexts); - } - - @Override - public boolean hasContext(String context) { - return contexts.contains(context); - } - - @Override - public void setContexts(Collection contexts) { - this.contexts.clear(); - this.contexts.addAll(contexts); - } - - @Override - public Identity addContext(String context) { - contexts.add(context); - return this; - } - - @Override - public Identity removeContext(String context) { - contexts.remove(context); - return this; - } - - @Override - public Map getProperties() { - return Collections.unmodifiableMap(properties); - } - - @Override - public void setProperties(Map properties) { - this.properties.clear(); - this.properties.putAll(properties); - } - - @Override - public String getProperty(String name) { - return properties.get(name); - } - - @Override - public Identity setProperty(String name, String value) { - properties.put(name, value); - return this; - } - - @Override - public Identity removeProperty(String name) { - properties.remove(name); - return this; - } - - @Override - public Trust getTrust(OwnIdentity ownIdentity) { - return trustCache.get(ownIdentity); - } - - @Override - public Identity setTrust(OwnIdentity ownIdentity, Trust trust) { - trustCache.put(ownIdentity, trust); - return this; - } - - @Override - public Identity removeTrust(OwnIdentity ownIdentity) { - trustCache.remove(ownIdentity); - return this; - } - - // - // OBJECT METHODS - // - - @Override - public int hashCode() { - return getId().hashCode(); - } - - @Override - public boolean equals(Object object) { - if (!(object instanceof Identity)) { - return false; - } - Identity identity = (Identity) object; - return identity.getId().equals(getId()); - } - - @Override - public String toString() { - return getClass().getSimpleName() + "[id=" + id + ",nickname=" + nickname + ",contexts=" + contexts + ",properties=" + properties + "]"; - } - -} diff --git a/src/main/java/net/pterodactylus/sone/freenet/wot/DefaultOwnIdentity.java b/src/main/java/net/pterodactylus/sone/freenet/wot/DefaultOwnIdentity.java deleted file mode 100644 index e2e3a74..0000000 --- a/src/main/java/net/pterodactylus/sone/freenet/wot/DefaultOwnIdentity.java +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Sone - DefaultOwnIdentity.java - Copyright © 2010–2019 David 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 . - */ - -package net.pterodactylus.sone.freenet.wot; - -import static com.google.common.base.Preconditions.checkNotNull; - -/** - * An own identity is an identity that the owner of the node has full control - * over. - */ -public class DefaultOwnIdentity extends DefaultIdentity implements OwnIdentity { - - /** The insert URI of the identity. */ - private final String insertUri; - - /** - * Creates a new own identity. - * - * @param id - * The ID of the identity - * @param nickname - * The nickname of the identity - * @param requestUri - * The request URI of the identity - * @param insertUri - * The insert URI of the identity - */ - public DefaultOwnIdentity(String id, String nickname, String requestUri, String insertUri) { - super(id, nickname, requestUri); - this.insertUri = checkNotNull(insertUri); - } - - // - // ACCESSORS - // - - @Override - public String getInsertUri() { - return insertUri; - } - - @Override - public OwnIdentity addContext(String context) { - return (OwnIdentity) super.addContext(context); - } - - @Override - public OwnIdentity removeContext(String context) { - return (OwnIdentity) super.removeContext(context); - } - - @Override - public OwnIdentity setProperty(String name, String value) { - return (OwnIdentity) super.setProperty(name, value); - } - - @Override - public OwnIdentity removeProperty(String name) { - return (OwnIdentity) super.removeProperty(name); - } - - // - // OBJECT METHODS - // - - @Override - public int hashCode() { - return super.hashCode(); - } - - @Override - public boolean equals(Object object) { - return super.equals(object); - } - -} diff --git a/src/main/java/net/pterodactylus/sone/freenet/wot/Identity.java b/src/main/java/net/pterodactylus/sone/freenet/wot/Identity.java index a99aac0..b8d4e6d 100644 --- a/src/main/java/net/pterodactylus/sone/freenet/wot/Identity.java +++ b/src/main/java/net/pterodactylus/sone/freenet/wot/Identity.java @@ -1,5 +1,5 @@ /* - * Sone - Identity.java - Copyright © 2010–2019 David Roden + * Sone - Identity.java - Copyright © 2010–2020 David 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 @@ -31,20 +31,6 @@ import com.google.common.base.Function; */ public interface Identity { - public static final Function> TO_CONTEXTS = new Function>() { - @Override - public Set apply(Identity identity) { - return (identity == null) ? Collections.emptySet() : identity.getContexts(); - } - }; - - public static final Function> TO_PROPERTIES = new Function>() { - @Override - public Map apply(Identity input) { - return (input == null) ? Collections.emptyMap() : input.getProperties(); - } - }; - /** * Returns the ID of the identity. * @@ -97,7 +83,7 @@ public interface Identity { * @param contexts * All contexts of the identity */ - public void setContexts(Collection contexts); + public void setContexts(Set contexts); /** * Removes the given context from this identity. diff --git a/src/main/java/net/pterodactylus/sone/freenet/wot/IdentityChangeDetector.java b/src/main/java/net/pterodactylus/sone/freenet/wot/IdentityChangeDetector.java deleted file mode 100644 index 8b28011..0000000 --- a/src/main/java/net/pterodactylus/sone/freenet/wot/IdentityChangeDetector.java +++ /dev/null @@ -1,198 +0,0 @@ -/* - * Sone - IdentityChangeDetector.java - Copyright © 2013–2019 David 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 . - */ - -package net.pterodactylus.sone.freenet.wot; - -import static com.google.common.base.Optional.absent; -import static com.google.common.base.Optional.fromNullable; -import static com.google.common.base.Predicates.not; -import static com.google.common.collect.FluentIterable.from; -import static net.pterodactylus.sone.freenet.wot.Identity.TO_CONTEXTS; -import static net.pterodactylus.sone.freenet.wot.Identity.TO_PROPERTIES; - -import java.util.Collection; -import java.util.Map; -import java.util.Map.Entry; - -import com.google.common.base.Optional; -import com.google.common.base.Predicate; -import com.google.common.collect.FluentIterable; -import com.google.common.collect.ImmutableMap; - -/** - * Detects changes between two lists of {@link Identity}s. The detector can find - * added and removed identities, and for identities that exist in both list - * their contexts and properties are checked for added, removed, or (in case of - * properties) changed values. - */ -public class IdentityChangeDetector { - - private final Map oldIdentities; - private Optional onNewIdentity = absent(); - private Optional onRemovedIdentity = absent(); - private Optional onChangedIdentity = absent(); - private Optional onUnchangedIdentity = absent(); - - public IdentityChangeDetector(Collection oldIdentities) { - this.oldIdentities = convertToMap(oldIdentities); - } - - public void onNewIdentity(IdentityProcessor onNewIdentity) { - this.onNewIdentity = fromNullable(onNewIdentity); - } - - public void onRemovedIdentity(IdentityProcessor onRemovedIdentity) { - this.onRemovedIdentity = fromNullable(onRemovedIdentity); - } - - public void onChangedIdentity(IdentityProcessor onChangedIdentity) { - this.onChangedIdentity = fromNullable(onChangedIdentity); - } - - public void onUnchangedIdentity(IdentityProcessor onUnchangedIdentity) { - this.onUnchangedIdentity = fromNullable(onUnchangedIdentity); - } - - public void detectChanges(final Collection newIdentities) { - notifyForRemovedIdentities(from(oldIdentities.values()).filter(notContainedIn(newIdentities))); - notifyForNewIdentities(from(newIdentities).filter(notContainedIn(oldIdentities.values()))); - notifyForChangedIdentities(from(newIdentities).filter(containedIn(oldIdentities)).filter(hasChanged(oldIdentities))); - notifyForUnchangedIdentities(from(newIdentities).filter(containedIn(oldIdentities)).filter(not(hasChanged(oldIdentities)))); - } - - private void notifyForRemovedIdentities(Iterable identities) { - notify(onRemovedIdentity, identities); - } - - private void notifyForNewIdentities(FluentIterable newIdentities) { - notify(onNewIdentity, newIdentities); - } - - private void notifyForChangedIdentities(FluentIterable identities) { - notify(onChangedIdentity, identities); - } - - private void notifyForUnchangedIdentities(FluentIterable identities) { - notify(onUnchangedIdentity, identities); - } - - private void notify(Optional identityProcessor, Iterable identities) { - if (!identityProcessor.isPresent()) { - return; - } - for (Identity identity : identities) { - identityProcessor.get().processIdentity(identity); - } - } - - private static Predicate hasChanged(final Map oldIdentities) { - return new Predicate() { - @Override - public boolean apply(Identity identity) { - return (identity != null) && identityHasChanged(oldIdentities.get(identity.getId()), identity); - } - }; - } - - private static boolean identityHasChanged(Identity oldIdentity, Identity newIdentity) { - return identityHasNewContexts(oldIdentity, newIdentity) - || identityHasRemovedContexts(oldIdentity, newIdentity) - || identityHasNewProperties(oldIdentity, newIdentity) - || identityHasRemovedProperties(oldIdentity, newIdentity) - || identityHasChangedProperties(oldIdentity, newIdentity); - } - - private static boolean identityHasNewContexts(Identity oldIdentity, Identity newIdentity) { - return from(TO_CONTEXTS.apply(newIdentity)).anyMatch(notAContextOf(oldIdentity)); - } - - private static boolean identityHasRemovedContexts(Identity oldIdentity, Identity newIdentity) { - return from(TO_CONTEXTS.apply(oldIdentity)).anyMatch(notAContextOf(newIdentity)); - } - - private static boolean identityHasNewProperties(Identity oldIdentity, Identity newIdentity) { - return from(TO_PROPERTIES.apply(newIdentity).entrySet()).anyMatch(notAPropertyOf(oldIdentity)); - } - - private static boolean identityHasRemovedProperties(Identity oldIdentity, Identity newIdentity) { - return from(TO_PROPERTIES.apply(oldIdentity).entrySet()).anyMatch(notAPropertyOf(newIdentity)); - } - - private static boolean identityHasChangedProperties(Identity oldIdentity, Identity newIdentity) { - return from(TO_PROPERTIES.apply(oldIdentity).entrySet()).anyMatch(hasADifferentValueThanIn(newIdentity)); - } - - private static Predicate containedIn(final Map identities) { - return new Predicate() { - @Override - public boolean apply(Identity identity) { - return (identity != null) && identities.containsKey(identity.getId()); - } - }; - } - - private static Predicate notAContextOf(final Identity identity) { - return new Predicate() { - @Override - public boolean apply(String context) { - return (identity != null) && !identity.getContexts().contains(context); - } - }; - } - - private static Predicate notContainedIn(final Collection newIdentities) { - return new Predicate() { - @Override - public boolean apply(Identity identity) { - return (identity != null) && !newIdentities.contains(identity); - } - }; - } - - private static Predicate> notAPropertyOf(final Identity identity) { - return new Predicate>() { - @Override - public boolean apply(Entry property) { - return (property != null) && !identity.getProperties().containsKey(property.getKey()); - } - }; - } - - private static Predicate> hasADifferentValueThanIn(final Identity newIdentity) { - return new Predicate>() { - @Override - public boolean apply(Entry property) { - return (property != null) && !newIdentity.getProperty(property.getKey()).equals(property.getValue()); - } - }; - } - - private static Map convertToMap(Collection identities) { - ImmutableMap.Builder mapBuilder = ImmutableMap.builder(); - for (Identity identity : identities) { - mapBuilder.put(identity.getId(), identity); - } - return mapBuilder.build(); - } - - public interface IdentityProcessor { - - void processIdentity(Identity identity); - - } - -} diff --git a/src/main/java/net/pterodactylus/sone/freenet/wot/IdentityChangeDetector.kt b/src/main/java/net/pterodactylus/sone/freenet/wot/IdentityChangeDetector.kt new file mode 100644 index 0000000..ffcafb3 --- /dev/null +++ b/src/main/java/net/pterodactylus/sone/freenet/wot/IdentityChangeDetector.kt @@ -0,0 +1,68 @@ +/* + * Sone - IdentityChangeDetector.kt - Copyright © 2013–2020 David 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 . + */ + +package net.pterodactylus.sone.freenet.wot + +/** + * Detects changes between two lists of [Identity]s. The detector can find + * added and removed identities, and for identities that exist in both list + * their contexts and properties are checked for added, removed, or (in case of + * properties) changed values. + */ +class IdentityChangeDetector(oldIdentities: Collection) { + + private val oldIdentities: Map = oldIdentities.associateBy { it.id } + var onNewIdentity: IdentityProcessor? = null + var onRemovedIdentity: IdentityProcessor? = null + var onChangedIdentity: IdentityProcessor? = null + var onUnchangedIdentity: IdentityProcessor? = null + + fun detectChanges(newIdentities: Collection) { + onRemovedIdentity.notify(oldIdentities.values.filter { it !in newIdentities }) + onNewIdentity.notify(newIdentities.filter { it !in oldIdentities.values }) + onChangedIdentity.notify(newIdentities.filter { it.id in oldIdentities }.filter { identityHasChanged(oldIdentities[it.id]!!, it) }) + onUnchangedIdentity.notify(newIdentities.filter { it.id in oldIdentities }.filterNot { identityHasChanged(oldIdentities[it.id]!!, it) }) + } + + private fun identityHasChanged(oldIdentity: Identity, newIdentity: Identity?) = + identityHasNewContexts(oldIdentity, newIdentity!!) + || identityHasRemovedContexts(oldIdentity, newIdentity) + || identityHasNewProperties(oldIdentity, newIdentity) + || identityHasRemovedProperties(oldIdentity, newIdentity) + || identityHasChangedProperties(oldIdentity, newIdentity) + + private fun identityHasNewContexts(oldIdentity: Identity, newIdentity: Identity) = + newIdentity.contexts.any { it !in oldIdentity.contexts } + + private fun identityHasRemovedContexts(oldIdentity: Identity, newIdentity: Identity) = + oldIdentity.contexts.any { it !in newIdentity.contexts } + + private fun identityHasNewProperties(oldIdentity: Identity, newIdentity: Identity) = + newIdentity.properties.keys.any { it !in oldIdentity.properties } + + private fun identityHasRemovedProperties(oldIdentity: Identity, newIdentity: Identity) = + oldIdentity.properties.keys.any { it !in newIdentity.properties } + + private fun identityHasChangedProperties(oldIdentity: Identity, newIdentity: Identity) = + oldIdentity.properties.entries.any { newIdentity.properties[it.key] != it.value } + +} + +typealias IdentityProcessor = (Identity) -> Unit + +private fun IdentityProcessor?.notify(identities: Iterable) = + this?.let { identities.forEach(this::invoke) } diff --git a/src/main/java/net/pterodactylus/sone/freenet/wot/IdentityChangeEventSender.java b/src/main/java/net/pterodactylus/sone/freenet/wot/IdentityChangeEventSender.java deleted file mode 100644 index fd57c38..0000000 --- a/src/main/java/net/pterodactylus/sone/freenet/wot/IdentityChangeEventSender.java +++ /dev/null @@ -1,132 +0,0 @@ -/* - * Sone - IdentityChangeEventSender.java - Copyright © 2013–2019 David 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 . - */ - -package net.pterodactylus.sone.freenet.wot; - -import java.util.Collection; -import java.util.Map; - -import net.pterodactylus.sone.freenet.wot.IdentityChangeDetector.IdentityProcessor; -import net.pterodactylus.sone.freenet.wot.event.IdentityAddedEvent; -import net.pterodactylus.sone.freenet.wot.event.IdentityRemovedEvent; -import net.pterodactylus.sone.freenet.wot.event.IdentityUpdatedEvent; -import net.pterodactylus.sone.freenet.wot.event.OwnIdentityAddedEvent; -import net.pterodactylus.sone.freenet.wot.event.OwnIdentityRemovedEvent; - -import com.google.common.eventbus.EventBus; - -/** - * Detects changes in {@link Identity}s trusted my multiple {@link - * OwnIdentity}s. - * - * @see IdentityChangeDetector - */ -public class IdentityChangeEventSender { - - private final EventBus eventBus; - private final Map> oldIdentities; - - public IdentityChangeEventSender(EventBus eventBus, Map> oldIdentities) { - this.eventBus = eventBus; - this.oldIdentities = oldIdentities; - } - - public void detectChanges(Map> identities) { - IdentityChangeDetector identityChangeDetector = new IdentityChangeDetector(oldIdentities.keySet()); - identityChangeDetector.onNewIdentity(addNewOwnIdentityAndItsTrustedIdentities(identities)); - identityChangeDetector.onRemovedIdentity(removeOwnIdentityAndItsTrustedIdentities(oldIdentities)); - identityChangeDetector.onUnchangedIdentity(detectChangesInTrustedIdentities(identities, oldIdentities)); - identityChangeDetector.detectChanges(identities.keySet()); - } - - private IdentityProcessor addNewOwnIdentityAndItsTrustedIdentities(final Map> newIdentities) { - return new IdentityProcessor() { - @Override - public void processIdentity(Identity identity) { - eventBus.post(new OwnIdentityAddedEvent((OwnIdentity) identity)); - for (Identity newIdentity : newIdentities.get((OwnIdentity) identity)) { - eventBus.post(new IdentityAddedEvent((OwnIdentity) identity, newIdentity)); - } - } - }; - } - - private IdentityProcessor removeOwnIdentityAndItsTrustedIdentities(final Map> oldIdentities) { - return new IdentityProcessor() { - @Override - public void processIdentity(Identity identity) { - eventBus.post(new OwnIdentityRemovedEvent((OwnIdentity) identity)); - for (Identity removedIdentity : oldIdentities.get((OwnIdentity) identity)) { - eventBus.post(new IdentityRemovedEvent((OwnIdentity) identity, removedIdentity)); - } - } - }; - } - - private IdentityProcessor detectChangesInTrustedIdentities(Map> newIdentities, Map> oldIdentities) { - return new DefaultIdentityProcessor(oldIdentities, newIdentities); - } - - private class DefaultIdentityProcessor implements IdentityProcessor { - - private final Map> oldIdentities; - private final Map> newIdentities; - - public DefaultIdentityProcessor(Map> oldIdentities, Map> newIdentities) { - this.oldIdentities = oldIdentities; - this.newIdentities = newIdentities; - } - - @Override - public void processIdentity(Identity ownIdentity) { - IdentityChangeDetector identityChangeDetector = new IdentityChangeDetector(oldIdentities.get((OwnIdentity) ownIdentity)); - identityChangeDetector.onNewIdentity(notifyForAddedIdentities((OwnIdentity) ownIdentity)); - identityChangeDetector.onRemovedIdentity(notifyForRemovedIdentities((OwnIdentity) ownIdentity)); - identityChangeDetector.onChangedIdentity(notifyForChangedIdentities((OwnIdentity) ownIdentity)); - identityChangeDetector.detectChanges(newIdentities.get((OwnIdentity) ownIdentity)); - } - - private IdentityProcessor notifyForChangedIdentities(final OwnIdentity ownIdentity) { - return new IdentityProcessor() { - @Override - public void processIdentity(Identity identity) { - eventBus.post(new IdentityUpdatedEvent(ownIdentity, identity)); - } - }; - } - - private IdentityProcessor notifyForRemovedIdentities(final OwnIdentity ownIdentity) { - return new IdentityProcessor() { - @Override - public void processIdentity(Identity identity) { - eventBus.post(new IdentityRemovedEvent(ownIdentity, identity)); - } - }; - } - - private IdentityProcessor notifyForAddedIdentities(final OwnIdentity ownIdentity) { - return new IdentityProcessor() { - @Override - public void processIdentity(Identity identity) { - eventBus.post(new IdentityAddedEvent(ownIdentity, identity)); - } - }; - } - - } - -} diff --git a/src/main/java/net/pterodactylus/sone/freenet/wot/IdentityLoader.java b/src/main/java/net/pterodactylus/sone/freenet/wot/IdentityLoader.java deleted file mode 100644 index f16df1c..0000000 --- a/src/main/java/net/pterodactylus/sone/freenet/wot/IdentityLoader.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Sone - IdentityLoader.java - Copyright © 2013–2019 David 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 . - */ - -package net.pterodactylus.sone.freenet.wot; - -import static java.util.Collections.emptySet; -import static net.pterodactylus.sone.freenet.wot.Context.extractContext; - -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; -import java.util.Set; - -import net.pterodactylus.sone.freenet.plugin.PluginException; - -import com.google.common.base.Optional; -import com.google.inject.Inject; - -/** - * Loads {@link OwnIdentity}s and the {@link Identity}s they trust. - */ -public class IdentityLoader { - - private final WebOfTrustConnector webOfTrustConnector; - private final Optional context; - - public IdentityLoader(WebOfTrustConnector webOfTrustConnector) { - this(webOfTrustConnector, Optional.absent()); - } - - @Inject - public IdentityLoader(WebOfTrustConnector webOfTrustConnector, Optional context) { - this.webOfTrustConnector = webOfTrustConnector; - this.context = context; - } - - public Map> loadIdentities() throws WebOfTrustException { - Collection currentOwnIdentities = webOfTrustConnector.loadAllOwnIdentities(); - return loadTrustedIdentitiesForOwnIdentities(currentOwnIdentities); - } - - private Map> loadTrustedIdentitiesForOwnIdentities(Collection ownIdentities) throws PluginException { - Map> currentIdentities = new HashMap<>(); - - for (OwnIdentity ownIdentity : ownIdentities) { - if (identityDoesNotHaveTheCorrectContext(ownIdentity)) { - currentIdentities.put(ownIdentity, Collections.emptySet()); - continue; - } - - Set trustedIdentities = webOfTrustConnector.loadTrustedIdentities(ownIdentity, context.transform(extractContext)); - currentIdentities.put(ownIdentity, trustedIdentities); - } - - return currentIdentities; - } - - private boolean identityDoesNotHaveTheCorrectContext(OwnIdentity ownIdentity) { - return context.isPresent() && !ownIdentity.hasContext(context.transform(extractContext).get()); - } - -} diff --git a/src/main/java/net/pterodactylus/sone/freenet/wot/IdentityManager.java b/src/main/java/net/pterodactylus/sone/freenet/wot/IdentityManager.java deleted file mode 100644 index c0f6f1b..0000000 --- a/src/main/java/net/pterodactylus/sone/freenet/wot/IdentityManager.java +++ /dev/null @@ -1,20 +0,0 @@ -package net.pterodactylus.sone.freenet.wot; - -import java.util.Set; - -import net.pterodactylus.util.service.Service; - -import com.google.common.eventbus.EventBus; -import com.google.inject.ImplementedBy; - -/** - * Connects to a {@link WebOfTrustConnector} and sends identity events to an - * {@link EventBus}. - */ -@ImplementedBy(IdentityManagerImpl.class) -public interface IdentityManager extends Service { - - boolean isConnected(); - Set getAllOwnIdentities(); - -} diff --git a/src/main/java/net/pterodactylus/sone/freenet/wot/IdentityManagerImpl.java b/src/main/java/net/pterodactylus/sone/freenet/wot/IdentityManagerImpl.java deleted file mode 100644 index 6f46465..0000000 --- a/src/main/java/net/pterodactylus/sone/freenet/wot/IdentityManagerImpl.java +++ /dev/null @@ -1,147 +0,0 @@ -/* - * Sone - IdentityManagerImpl.java - Copyright © 2010–2019 David 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 . - */ - -package net.pterodactylus.sone.freenet.wot; - -import static java.util.logging.Logger.getLogger; - -import java.util.Collection; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; -import java.util.logging.Level; -import java.util.logging.Logger; - -import net.pterodactylus.sone.freenet.plugin.PluginException; -import net.pterodactylus.util.service.AbstractService; - -import com.google.common.collect.Sets; -import com.google.common.eventbus.EventBus; -import com.google.inject.Inject; -import com.google.inject.Singleton; - -/** - * The identity manager takes care of loading and storing identities, their - * contexts, and properties. It does so in a way that does not expose errors via - * exceptions but it only logs them and tries to return sensible defaults. - *

- * It is also responsible for polling identities from the Web of Trust plugin - * and sending events to the {@link EventBus} when {@link Identity}s and - * {@link OwnIdentity}s are discovered or disappearing. - */ -@Singleton -public class IdentityManagerImpl extends AbstractService implements IdentityManager { - - /** The logger. */ - private static final Logger logger = getLogger(IdentityManagerImpl.class.getName()); - - /** The event bus. */ - private final EventBus eventBus; - - private final IdentityLoader identityLoader; - - /** The Web of Trust connector. */ - private final WebOfTrustConnector webOfTrustConnector; - - /** The currently known own identities. */ - private final Set currentOwnIdentities = Sets.newHashSet(); - - /** - * Creates a new identity manager. - * - * @param eventBus - * The event bus - * @param webOfTrustConnector - * The Web of Trust connector - */ - @Inject - public IdentityManagerImpl(EventBus eventBus, WebOfTrustConnector webOfTrustConnector, IdentityLoader identityLoader) { - super("Sone Identity Manager", false); - this.eventBus = eventBus; - this.webOfTrustConnector = webOfTrustConnector; - this.identityLoader = identityLoader; - } - - // - // ACCESSORS - // - - /** - * Returns whether the Web of Trust plugin could be reached during the last - * try. - * - * @return {@code true} if the Web of Trust plugin is connected, - * {@code false} otherwise - */ - @Override - public boolean isConnected() { - try { - webOfTrustConnector.ping(); - return true; - } catch (PluginException pe1) { - /* not connected, ignore. */ - return false; - } - } - - /** - * Returns all own identities. - * - * @return All own identities - */ - @Override - public Set getAllOwnIdentities() { - synchronized (currentOwnIdentities) { - return new HashSet<>(currentOwnIdentities); - } - } - - // - // SERVICE METHODS - // - - /** - * {@inheritDoc} - */ - @Override - protected void serviceRun() { - Map> oldIdentities = new HashMap<>(); - - while (!shouldStop()) { - try { - Map> currentIdentities = identityLoader.loadIdentities(); - - IdentityChangeEventSender identityChangeEventSender = new IdentityChangeEventSender(eventBus, oldIdentities); - identityChangeEventSender.detectChanges(currentIdentities); - - oldIdentities = currentIdentities; - - synchronized (currentOwnIdentities) { - currentOwnIdentities.clear(); - currentOwnIdentities.addAll(currentIdentities.keySet()); - } - } catch (WebOfTrustException wote1) { - logger.log(Level.WARNING, "WoT has disappeared!", wote1); - } - - /* wait a minute before checking again. */ - sleep(60 * 1000); - } - } - -} diff --git a/src/main/java/net/pterodactylus/sone/freenet/wot/OwnIdentity.java b/src/main/java/net/pterodactylus/sone/freenet/wot/OwnIdentity.java index 500f2c7..25b77b9 100644 --- a/src/main/java/net/pterodactylus/sone/freenet/wot/OwnIdentity.java +++ b/src/main/java/net/pterodactylus/sone/freenet/wot/OwnIdentity.java @@ -1,5 +1,5 @@ /* - * Sone - OwnIdentity.java - Copyright © 2010–2019 David Roden + * Sone - OwnIdentity.java - Copyright © 2010–2020 David 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 diff --git a/src/main/java/net/pterodactylus/sone/freenet/wot/Trust.java b/src/main/java/net/pterodactylus/sone/freenet/wot/Trust.java deleted file mode 100644 index 2da00f6..0000000 --- a/src/main/java/net/pterodactylus/sone/freenet/wot/Trust.java +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Sone - Trust.java - Copyright © 2010–2019 David 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 . - */ - -package net.pterodactylus.sone.freenet.wot; - -import static com.google.common.base.Objects.equal; - -import com.google.common.base.Objects; - -/** - * Container class for trust in the web of trust. - */ -public class Trust { - - /** Explicitely assigned trust. */ - private final Integer explicit; - - /** Implicitely calculated trust. */ - private final Integer implicit; - - /** The distance from the owner of the trust tree. */ - private final Integer distance; - - /** - * Creates a new trust container. - * - * @param explicit - * The explicit trust - * @param implicit - * The implicit trust - * @param distance - * The distance - */ - public Trust(Integer explicit, Integer implicit, Integer distance) { - this.explicit = explicit; - this.implicit = implicit; - this.distance = distance; - } - - /** - * Returns the trust explicitely assigned to an identity. - * - * @return The explicitely assigned trust, or {@code null} if the identity - * is not in the own identity’s trust tree - */ - public Integer getExplicit() { - return explicit; - } - - /** - * Returns the implicitely assigned trust, or the calculated trust. - * - * @return The calculated trust, or {@code null} if the identity is not in - * the own identity’s trust tree - */ - public Integer getImplicit() { - return implicit; - } - - /** - * Returns the distance of the trusted identity from the trusting identity. - * - * @return The distance from the own identity, or {@code null} if the - * identity is not in the own identity’s trust tree - */ - public Integer getDistance() { - return distance; - } - - @Override - public boolean equals(Object object) { - if (!(object instanceof Trust)) { - return false; - } - Trust trust = (Trust) object; - return equal(getExplicit(), trust.getExplicit()) && equal(getImplicit(), trust.getImplicit()) && equal(getDistance(), trust.getDistance()); - } - - @Override - public int hashCode() { - return Objects.hashCode(explicit, implicit, distance); - } - - /** {@inheritDoc} */ - @Override - public String toString() { - return getClass().getName() + "[explicit=" + explicit + ",implicit=" + implicit + ",distance=" + distance + "]"; - } - -} diff --git a/src/main/java/net/pterodactylus/sone/freenet/wot/WebOfTrustConnector.java b/src/main/java/net/pterodactylus/sone/freenet/wot/WebOfTrustConnector.java deleted file mode 100644 index c1dbef9..0000000 --- a/src/main/java/net/pterodactylus/sone/freenet/wot/WebOfTrustConnector.java +++ /dev/null @@ -1,623 +0,0 @@ -/* - * Sone - WebOfTrustConnector.java - Copyright © 2010–2019 David 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 . - */ - -package net.pterodactylus.sone.freenet.wot; - -import static java.util.logging.Logger.getLogger; -import static net.pterodactylus.sone.utils.NumberParsers.parseInt; - -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.atomic.AtomicLong; -import java.util.logging.Level; -import java.util.logging.Logger; - -import net.pterodactylus.sone.freenet.plugin.PluginConnector; -import net.pterodactylus.sone.freenet.plugin.PluginException; -import net.pterodactylus.sone.freenet.plugin.event.ReceivedReplyEvent; - -import com.google.common.base.Optional; -import com.google.common.collect.MapMaker; -import com.google.common.eventbus.Subscribe; -import com.google.inject.Inject; -import com.google.inject.Singleton; - -import freenet.support.SimpleFieldSet; -import freenet.support.api.Bucket; - -/** - * Connector for the Web of Trust plugin. - */ -@Singleton -public class WebOfTrustConnector { - - /** The logger. */ - private static final Logger logger = getLogger(WebOfTrustConnector.class.getName()); - - /** The name of the WoT plugin. */ - private static final String WOT_PLUGIN_NAME = "plugins.WebOfTrust.WebOfTrust"; - - /** Counter for connection identifiers. */ - private final AtomicLong counter = new AtomicLong(); - - /** The plugin connector. */ - private final PluginConnector pluginConnector; - - /** Map for replies. */ - private final Map replies = new MapMaker().makeMap(); - - /** - * Creates a new Web of Trust connector that uses the given plugin - * connector. - * - * @param pluginConnector - * The plugin connector - */ - @Inject - public WebOfTrustConnector(PluginConnector pluginConnector) { - this.pluginConnector = pluginConnector; - } - - // - // ACTIONS - // - - /** - * Stops the web of trust connector. - */ - public void stop() { - /* does nothing. */ - } - - /** - * Loads all own identities from the Web of Trust plugin. - * - * @return All own identity - * @throws WebOfTrustException - * if the own identities can not be loaded - */ - public Set loadAllOwnIdentities() throws WebOfTrustException { - Reply reply = performRequest(SimpleFieldSetConstructor.create().put("Message", "GetOwnIdentities").get()); - SimpleFieldSet fields = reply.getFields(); - int ownIdentityCounter = -1; - Set ownIdentities = new HashSet<>(); - while (true) { - String id = fields.get("Identity" + ++ownIdentityCounter); - if (id == null) { - break; - } - String requestUri = fields.get("RequestURI" + ownIdentityCounter); - String insertUri = fields.get("InsertURI" + ownIdentityCounter); - String nickname = fields.get("Nickname" + ownIdentityCounter); - DefaultOwnIdentity ownIdentity = new DefaultOwnIdentity(id, nickname, requestUri, insertUri); - ownIdentity.setContexts(parseContexts("Contexts" + ownIdentityCounter + ".", fields)); - ownIdentity.setProperties(parseProperties("Properties" + ownIdentityCounter + ".", fields)); - ownIdentities.add(ownIdentity); - } - return ownIdentities; - } - - /** - * Loads all identities that the given identities trusts with a score of - * more than 0. - * - * @param ownIdentity - * The own identity - * @return All trusted identities - * @throws PluginException - * if an error occured talking to the Web of Trust plugin - */ - public Set loadTrustedIdentities(OwnIdentity ownIdentity) throws PluginException { - return loadTrustedIdentities(ownIdentity, Optional.absent()); - } - - /** - * Loads all identities that the given identities trusts with a score of - * more than 0 and the (optional) given context. - * - * @param ownIdentity - * The own identity - * @param context - * The context to filter, or {@code null} - * @return All trusted identities - * @throws PluginException - * if an error occured talking to the Web of Trust plugin - */ - public Set loadTrustedIdentities(OwnIdentity ownIdentity, Optional context) throws PluginException { - Reply reply = performRequest(SimpleFieldSetConstructor.create().put("Message", "GetIdentitiesByScore").put("Truster", ownIdentity.getId()).put("Selection", "+").put("Context", context.or("")).put("WantTrustValues", "true").get()); - SimpleFieldSet fields = reply.getFields(); - Set identities = new HashSet<>(); - int identityCounter = -1; - while (true) { - String id = fields.get("Identity" + ++identityCounter); - if (id == null) { - break; - } - String nickname = fields.get("Nickname" + identityCounter); - String requestUri = fields.get("RequestURI" + identityCounter); - DefaultIdentity identity = new DefaultIdentity(id, nickname, requestUri); - identity.setContexts(parseContexts("Contexts" + identityCounter + ".", fields)); - identity.setProperties(parseProperties("Properties" + identityCounter + ".", fields)); - Integer trust = parseInt(fields.get("Trust" + identityCounter), null); - int score = parseInt(fields.get("Score" + identityCounter), 0); - int rank = parseInt(fields.get("Rank" + identityCounter), 0); - identity.setTrust(ownIdentity, new Trust(trust, score, rank)); - identities.add(identity); - } - return identities; - } - - /** - * Adds the given context to the given identity. - * - * @param ownIdentity - * The identity to add the context to - * @param context - * The context to add - * @throws PluginException - * if an error occured talking to the Web of Trust plugin - */ - public void addContext(OwnIdentity ownIdentity, String context) throws PluginException { - performRequest(SimpleFieldSetConstructor.create().put("Message", "AddContext").put("Identity", ownIdentity.getId()).put("Context", context).get()); - } - - /** - * Removes the given context from the given identity. - * - * @param ownIdentity - * The identity to remove the context from - * @param context - * The context to remove - * @throws PluginException - * if an error occured talking to the Web of Trust plugin - */ - public void removeContext(OwnIdentity ownIdentity, String context) throws PluginException { - performRequest(SimpleFieldSetConstructor.create().put("Message", "RemoveContext").put("Identity", ownIdentity.getId()).put("Context", context).get()); - } - - /** - * Returns the value of the property with the given name. - * - * @param identity - * The identity whose properties to check - * @param name - * The name of the property to return - * @return The value of the property, or {@code null} if there is no value - * @throws PluginException - * if an error occured talking to the Web of Trust plugin - */ - public String getProperty(Identity identity, String name) throws PluginException { - Reply reply = performRequest(SimpleFieldSetConstructor.create().put("Message", "GetProperty").put("Identity", identity.getId()).put("Property", name).get()); - return reply.getFields().get("Property"); - } - - /** - * Sets the property with the given name to the given value. - * - * @param ownIdentity - * The identity to set the property on - * @param name - * The name of the property to set - * @param value - * The value to set - * @throws PluginException - * if an error occured talking to the Web of Trust plugin - */ - public void setProperty(OwnIdentity ownIdentity, String name, String value) throws PluginException { - performRequest(SimpleFieldSetConstructor.create().put("Message", "SetProperty").put("Identity", ownIdentity.getId()).put("Property", name).put("Value", value).get()); - } - - /** - * Removes the property with the given name. - * - * @param ownIdentity - * The identity to remove the property from - * @param name - * The name of the property to remove - * @throws PluginException - * if an error occured talking to the Web of Trust plugin - */ - public void removeProperty(OwnIdentity ownIdentity, String name) throws PluginException { - performRequest(SimpleFieldSetConstructor.create().put("Message", "RemoveProperty").put("Identity", ownIdentity.getId()).put("Property", name).get()); - } - - /** - * Returns the trust for the given identity assigned to it by the given own - * identity. - * - * @param ownIdentity - * The own identity - * @param identity - * The identity to get the trust for - * @return The trust for the given identity - * @throws PluginException - * if an error occured talking to the Web of Trust plugin - */ - public Trust getTrust(OwnIdentity ownIdentity, Identity identity) throws PluginException { - Reply getTrustReply = performRequest(SimpleFieldSetConstructor.create().put("Message", "GetIdentity").put("Truster", ownIdentity.getId()).put("Identity", identity.getId()).get()); - String trust = getTrustReply.getFields().get("Trust"); - String score = getTrustReply.getFields().get("Score"); - String rank = getTrustReply.getFields().get("Rank"); - Integer explicit = null; - Integer implicit = null; - Integer distance = null; - try { - explicit = Integer.valueOf(trust); - } catch (NumberFormatException nfe1) { - /* ignore. */ - } - try { - implicit = Integer.valueOf(score); - distance = Integer.valueOf(rank); - } catch (NumberFormatException nfe1) { - /* ignore. */ - } - return new Trust(explicit, implicit, distance); - } - - /** - * Sets the trust for the given identity. - * - * @param ownIdentity - * The trusting identity - * @param identity - * The trusted identity - * @param trust - * The amount of trust (-100 thru 100) - * @param comment - * The comment or explanation of the trust value - * @throws PluginException - * if an error occured talking to the Web of Trust plugin - */ - public void setTrust(OwnIdentity ownIdentity, Identity identity, int trust, String comment) throws PluginException { - performRequest(SimpleFieldSetConstructor.create().put("Message", "SetTrust").put("Truster", ownIdentity.getId()).put("Trustee", identity.getId()).put("Value", String.valueOf(trust)).put("Comment", comment).get()); - } - - /** - * Removes any trust assignment of the given own identity for the given - * identity. - * - * @param ownIdentity - * The own identity - * @param identity - * The identity to remove all trust for - * @throws WebOfTrustException - * if an error occurs - */ - public void removeTrust(OwnIdentity ownIdentity, Identity identity) throws WebOfTrustException { - performRequest(SimpleFieldSetConstructor.create().put("Message", "RemoveTrust").put("Truster", ownIdentity.getId()).put("Trustee", identity.getId()).get()); - } - - /** - * Pings the Web of Trust plugin. If the plugin can not be reached, a - * {@link PluginException} is thrown. - * - * @throws PluginException - * if the plugin is not loaded - */ - public void ping() throws PluginException { - performRequest(SimpleFieldSetConstructor.create().put("Message", "Ping").get()); - } - - // - // PRIVATE ACTIONS - // - - /** - * Parses the contexts from the given fields. - * - * @param prefix - * The prefix to use to access the contexts - * @param fields - * The fields to parse the contexts from - * @return The parsed contexts - */ - private static Set parseContexts(String prefix, SimpleFieldSet fields) { - Set contexts = new HashSet<>(); - int contextCounter = -1; - while (true) { - String context = fields.get(prefix + "Context" + ++contextCounter); - if (context == null) { - break; - } - contexts.add(context); - } - return contexts; - } - - /** - * Parses the properties from the given fields. - * - * @param prefix - * The prefix to use to access the properties - * @param fields - * The fields to parse the properties from - * @return The parsed properties - */ - private static Map parseProperties(String prefix, SimpleFieldSet fields) { - Map properties = new HashMap<>(); - int propertiesCounter = -1; - while (true) { - String propertyName = fields.get(prefix + "Property" + ++propertiesCounter + ".Name"); - if (propertyName == null) { - break; - } - String propertyValue = fields.get(prefix + "Property" + propertiesCounter + ".Value"); - properties.put(propertyName, propertyValue); - } - return properties; - } - - /** - * Sends a request containing the given fields and waits for the target - * message. - * - * @param fields - * The fields of the message - * @return The reply message - * @throws PluginException - * if the request could not be sent - */ - private Reply performRequest(SimpleFieldSet fields) throws PluginException { - return performRequest(fields, null); - } - - /** - * Sends a request containing the given fields and waits for the target - * message. - * - * @param fields - * The fields of the message - * @param data - * The payload of the message - * @return The reply message - * @throws PluginException - * if the request could not be sent - */ - private Reply performRequest(SimpleFieldSet fields, Bucket data) throws PluginException { - String identifier = "FCP-Command-" + System.currentTimeMillis() + "-" + counter.getAndIncrement(); - Reply reply = new Reply(); - PluginIdentifier pluginIdentifier = new PluginIdentifier(WOT_PLUGIN_NAME, identifier); - replies.put(pluginIdentifier, reply); - - logger.log(Level.FINE, String.format("Sending FCP Request: %s", fields.get("Message"))); - synchronized (reply) { - try { - pluginConnector.sendRequest(WOT_PLUGIN_NAME, identifier, fields, data); - while (reply.getFields() == null) { - try { - reply.wait(); - } catch (InterruptedException ie1) { - logger.log(Level.WARNING, String.format("Got interrupted while waiting for reply on %s.", fields.get("Message")), ie1); - } - } - } finally { - replies.remove(pluginIdentifier); - } - } - logger.log(Level.FINEST, String.format("Received FCP Response for %s: %s", fields.get("Message"), (reply.getFields() != null) ? reply.getFields().get("Message") : null)); - if ((reply.getFields() == null) || "Error".equals(reply.getFields().get("Message"))) { - throw new PluginException("Could not perform request for " + fields.get("Message")); - } - return reply; - } - - /** - * Notifies the connector that a plugin reply was received. - * - * @param receivedReplyEvent - * The event - */ - @Subscribe - public void receivedReply(ReceivedReplyEvent receivedReplyEvent) { - PluginIdentifier pluginIdentifier = new PluginIdentifier(receivedReplyEvent.pluginName(), receivedReplyEvent.identifier()); - Reply reply = replies.remove(pluginIdentifier); - if (reply == null) { - return; - } - logger.log(Level.FINEST, String.format("Received Reply from Plugin: %s", receivedReplyEvent.fieldSet().get("Message"))); - synchronized (reply) { - reply.setFields(receivedReplyEvent.fieldSet()); - reply.setData(receivedReplyEvent.data()); - reply.notify(); - } - } - - /** - * Container for the data of the reply from a plugin. - */ - private static class Reply { - - /** The fields of the reply. */ - private SimpleFieldSet fields; - - /** The payload of the reply. */ - private Bucket data; - - /** Empty constructor. */ - public Reply() { - /* do nothing. */ - } - - /** - * Returns the fields of the reply. - * - * @return The fields of the reply - */ - public SimpleFieldSet getFields() { - return fields; - } - - /** - * Sets the fields of the reply. - * - * @param fields - * The fields of the reply - */ - public void setFields(SimpleFieldSet fields) { - this.fields = fields; - } - - /** - * Returns the payload of the reply. - * - * @return The payload of the reply (may be {@code null}) - */ - @SuppressWarnings("unused") - public Bucket getData() { - return data; - } - - /** - * Sets the payload of the reply. - * - * @param data - * The payload of the reply (may be {@code null}) - */ - public void setData(Bucket data) { - this.data = data; - } - - } - - /** - * Helper method to create {@link SimpleFieldSet}s with terser code. - */ - private static class SimpleFieldSetConstructor { - - /** The field set being created. */ - private SimpleFieldSet simpleFieldSet; - - /** - * Creates a new simple field set constructor. - * - * @param shortLived - * {@code true} if the resulting simple field set should be - * short-lived, {@code false} otherwise - */ - private SimpleFieldSetConstructor(boolean shortLived) { - simpleFieldSet = new SimpleFieldSet(shortLived); - } - - // - // ACCESSORS - // - - /** - * Returns the created simple field set. - * - * @return The created simple field set - */ - public SimpleFieldSet get() { - return simpleFieldSet; - } - - /** - * Sets the field with the given name to the given value. - * - * @param name - * The name of the fleld - * @param value - * The value of the field - * @return This constructor (for method chaining) - */ - public SimpleFieldSetConstructor put(String name, String value) { - simpleFieldSet.putOverwrite(name, value); - return this; - } - - // - // ACTIONS - // - - /** - * Creates a new simple field set constructor. - * - * @return The created simple field set constructor - */ - public static SimpleFieldSetConstructor create() { - return create(true); - } - - /** - * Creates a new simple field set constructor. - * - * @param shortLived - * {@code true} if the resulting simple field set should be - * short-lived, {@code false} otherwise - * @return The created simple field set constructor - */ - public static SimpleFieldSetConstructor create(boolean shortLived) { - return new SimpleFieldSetConstructor(shortLived); - } - - } - - /** - * Container for identifying plugins. Plugins are identified by their plugin - * name and their unique identifier. - */ - private static class PluginIdentifier { - - /** The plugin name. */ - private final String pluginName; - - /** The plugin identifier. */ - private final String identifier; - - /** - * Creates a new plugin identifier. - * - * @param pluginName - * The name of the plugin - * @param identifier - * The identifier of the plugin - */ - public PluginIdentifier(String pluginName, String identifier) { - this.pluginName = pluginName; - this.identifier = identifier; - } - - // - // OBJECT METHODS - // - - /** - * {@inheritDoc} - */ - @Override - public int hashCode() { - return pluginName.hashCode() ^ identifier.hashCode(); - } - - /** - * {@inheritDoc} - */ - @Override - public boolean equals(Object object) { - if (!(object instanceof PluginIdentifier)) { - return false; - } - PluginIdentifier pluginIdentifier = (PluginIdentifier) object; - return pluginName.equals(pluginIdentifier.pluginName) && identifier.equals(pluginIdentifier.identifier); - } - - } - -} diff --git a/src/main/java/net/pterodactylus/sone/freenet/wot/WebOfTrustException.java b/src/main/java/net/pterodactylus/sone/freenet/wot/WebOfTrustException.java deleted file mode 100644 index 954fe04..0000000 --- a/src/main/java/net/pterodactylus/sone/freenet/wot/WebOfTrustException.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Sone - WebOfTrustException.java - Copyright © 2010–2019 David 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 . - */ - -package net.pterodactylus.sone.freenet.wot; - -/** - * Exception that signals an error processing web of trust identities, mostly - * when communicating with the web of trust plugin. - */ -public class WebOfTrustException extends Exception { - - /** - * Creates a new web of trust exception. - */ - public WebOfTrustException() { - super(); - } - - /** - * Creates a new web of trust exception. - * - * @param message - * The message of the exception - */ - public WebOfTrustException(String message) { - super(message); - } - - /** - * Creates a new web of trust exception. - * - * @param cause - * The cause of the exception - */ - public WebOfTrustException(Throwable cause) { - super(cause); - } - - /** - * Creates a new web of trust exception. - * - * @param message - * The message of the exception - * @param cause - * The cause of the exception - */ - public WebOfTrustException(String message, Throwable cause) { - super(message, cause); - } - -} diff --git a/src/main/java/net/pterodactylus/sone/freenet/wot/event/IdentityAddedEvent.java b/src/main/java/net/pterodactylus/sone/freenet/wot/event/IdentityAddedEvent.java deleted file mode 100644 index 2d7ab32..0000000 --- a/src/main/java/net/pterodactylus/sone/freenet/wot/event/IdentityAddedEvent.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Sone - IdentityAddedEvent.java - Copyright © 2013–2019 David 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 . - */ - -package net.pterodactylus.sone.freenet.wot.event; - -import net.pterodactylus.sone.freenet.wot.Identity; -import net.pterodactylus.sone.freenet.wot.OwnIdentity; - -/** - * Event that signals that an {@link Identity} was added. - */ -public class IdentityAddedEvent extends IdentityEvent { - - /** - * Creates a new “identity added” event. - * - * @param ownIdentity - * The own identity that added the identity - * @param identity - * The identity that was added - */ - public IdentityAddedEvent(OwnIdentity ownIdentity, Identity identity) { - super(ownIdentity, identity); - } - -} diff --git a/src/main/java/net/pterodactylus/sone/freenet/wot/event/IdentityEvent.java b/src/main/java/net/pterodactylus/sone/freenet/wot/event/IdentityEvent.java deleted file mode 100644 index 8a23ae1..0000000 --- a/src/main/java/net/pterodactylus/sone/freenet/wot/event/IdentityEvent.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Sone - IdentityEvent.java - Copyright © 2013–2019 David 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 . - */ - -package net.pterodactylus.sone.freenet.wot.event; - -import net.pterodactylus.sone.freenet.wot.Identity; -import net.pterodactylus.sone.freenet.wot.OwnIdentity; - -/** - * Base class for {@link Identity} events. - */ -public abstract class IdentityEvent { - - /** The own identity this event relates to. */ - private final OwnIdentity ownIdentity; - - /** The identity this event is about. */ - private final Identity identity; - - /** - * Creates a new identity-based event. - * - * @param ownIdentity - * The own identity that relates to the identity - * @param identity - * The identity this event is about - */ - protected IdentityEvent(OwnIdentity ownIdentity, Identity identity) { - this.ownIdentity = ownIdentity; - this.identity = identity; - } - - // - // ACCESSORS - // - - /** - * Returns the own identity this event relates to. - * - * @return The own identity this event relates to - */ - public OwnIdentity ownIdentity() { - return ownIdentity; - } - - /** - * Returns the identity this event is about. - * - * @return The identity this event is about - */ - public Identity identity() { - return identity; - } - - @Override - public int hashCode() { - return ownIdentity().hashCode() ^ identity().hashCode(); - } - - @Override - public boolean equals(Object object) { - if ((object == null) || !object.getClass().equals(getClass())) { - return false; - } - IdentityEvent identityEvent = (IdentityEvent) object; - return ownIdentity().equals(identityEvent.ownIdentity()) && identity().equals(identityEvent.identity()); - } - -} diff --git a/src/main/java/net/pterodactylus/sone/freenet/wot/event/IdentityRemovedEvent.java b/src/main/java/net/pterodactylus/sone/freenet/wot/event/IdentityRemovedEvent.java deleted file mode 100644 index 655015e..0000000 --- a/src/main/java/net/pterodactylus/sone/freenet/wot/event/IdentityRemovedEvent.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Sone - IdentityRemovedEvent.java - Copyright © 2013–2019 David 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 . - */ - -package net.pterodactylus.sone.freenet.wot.event; - -import net.pterodactylus.sone.freenet.wot.Identity; -import net.pterodactylus.sone.freenet.wot.OwnIdentity; - -/** - * Event that signals that an {@link Identity} was removed. - */ -public class IdentityRemovedEvent extends IdentityEvent { - - /** - * Creates a new “identity removed” event. - * - * @param ownIdentity - * The own identity that removed the identity - * @param identity - * The identity that was removed - */ - public IdentityRemovedEvent(OwnIdentity ownIdentity, Identity identity) { - super(ownIdentity, identity); - } - -} diff --git a/src/main/java/net/pterodactylus/sone/freenet/wot/event/IdentityUpdatedEvent.java b/src/main/java/net/pterodactylus/sone/freenet/wot/event/IdentityUpdatedEvent.java deleted file mode 100644 index a71ad5b..0000000 --- a/src/main/java/net/pterodactylus/sone/freenet/wot/event/IdentityUpdatedEvent.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Sone - IdentityUpdatedEvent.java - Copyright © 2013–2019 David 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 . - */ - -package net.pterodactylus.sone.freenet.wot.event; - -import net.pterodactylus.sone.freenet.wot.Identity; -import net.pterodactylus.sone.freenet.wot.OwnIdentity; - -/** - * Event that signals that an {@link Identity} was updated. - */ -public class IdentityUpdatedEvent extends IdentityEvent { - - /** - * Creates a new “identity updated” event. - * - * @param ownIdentity - * The own identity that tracks the identity - * @param identity - * The identity that was updated - */ - public IdentityUpdatedEvent(OwnIdentity ownIdentity, Identity identity) { - super(ownIdentity, identity); - } - -} diff --git a/src/main/java/net/pterodactylus/sone/freenet/wot/event/OwnIdentityAddedEvent.java b/src/main/java/net/pterodactylus/sone/freenet/wot/event/OwnIdentityAddedEvent.java deleted file mode 100644 index fc34848..0000000 --- a/src/main/java/net/pterodactylus/sone/freenet/wot/event/OwnIdentityAddedEvent.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Sone - OwnIdentityAddedEvent.java - Copyright © 2013–2019 David 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 . - */ - -package net.pterodactylus.sone.freenet.wot.event; - -import net.pterodactylus.sone.freenet.wot.OwnIdentity; - -/** - * Event that signals that an {@link OwnIdentity} was added. - */ -public class OwnIdentityAddedEvent extends OwnIdentityEvent { - - /** - * Creates new “own identity added” event. - * - * @param ownIdentity - * The own identity that was added - */ - public OwnIdentityAddedEvent(OwnIdentity ownIdentity) { - super(ownIdentity); - } - -} diff --git a/src/main/java/net/pterodactylus/sone/freenet/wot/event/OwnIdentityEvent.java b/src/main/java/net/pterodactylus/sone/freenet/wot/event/OwnIdentityEvent.java deleted file mode 100644 index 9e88d20..0000000 --- a/src/main/java/net/pterodactylus/sone/freenet/wot/event/OwnIdentityEvent.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Sone - OwnIdentityEvent.java - Copyright © 2013–2019 David 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 . - */ - -package net.pterodactylus.sone.freenet.wot.event; - -import net.pterodactylus.sone.freenet.wot.OwnIdentity; - -/** - * Base class for {@link OwnIdentity} events. - */ -public abstract class OwnIdentityEvent { - - /** The own identity this event is about. */ - private final OwnIdentity ownIdentity; - - /** - * Creates a new own identity-based event. - * - * @param ownIdentity - * The own identity this event is about - */ - protected OwnIdentityEvent(OwnIdentity ownIdentity) { - this.ownIdentity = ownIdentity; - } - - // - // ACCESSORS - // - - /** - * Returns the own identity this event is about. - * - * @return The own identity this event is about - */ - public OwnIdentity ownIdentity() { - return ownIdentity; - } - - @Override - public int hashCode() { - return ownIdentity().hashCode(); - } - - @Override - public boolean equals(Object object) { - if ((object == null) || !object.getClass().equals(getClass())) { - return false; - } - OwnIdentityEvent ownIdentityEvent = (OwnIdentityEvent) object; - return ownIdentity().equals(ownIdentityEvent.ownIdentity()); - } - -} diff --git a/src/main/java/net/pterodactylus/sone/freenet/wot/event/OwnIdentityRemovedEvent.java b/src/main/java/net/pterodactylus/sone/freenet/wot/event/OwnIdentityRemovedEvent.java deleted file mode 100644 index 73761b0..0000000 --- a/src/main/java/net/pterodactylus/sone/freenet/wot/event/OwnIdentityRemovedEvent.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Sone - OwnIdentityRemovedEvent.java - Copyright © 2013–2019 David 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 . - */ - -package net.pterodactylus.sone.freenet.wot.event; - -import net.pterodactylus.sone.freenet.wot.OwnIdentity; - -/** - * Event that signals that an {@link OwnIdentity} was removed. - */ -public class OwnIdentityRemovedEvent extends OwnIdentityEvent { - - /** - * Creates a new “own identity removed” event. - * - * @param ownIdentity - * The own identity that was removed - */ - public OwnIdentityRemovedEvent(OwnIdentity ownIdentity) { - super(ownIdentity); - } - -} diff --git a/src/main/java/net/pterodactylus/sone/main/DebugLoaders.java b/src/main/java/net/pterodactylus/sone/main/DebugLoaders.java index c42b056..2e18347 100644 --- a/src/main/java/net/pterodactylus/sone/main/DebugLoaders.java +++ b/src/main/java/net/pterodactylus/sone/main/DebugLoaders.java @@ -1,6 +1,7 @@ package net.pterodactylus.sone.main; import java.io.File; +import javax.annotation.Nonnull; import net.pterodactylus.sone.template.FilesystemTemplate; import net.pterodactylus.sone.web.pages.ReloadingPage; @@ -21,16 +22,19 @@ public class DebugLoaders implements Loaders { this.filesystemPath = filesystemPath; } + @Nonnull @Override - public Template loadTemplate(String path) { + public Template loadTemplate(@Nonnull String path) { return new FilesystemTemplate(new File(filesystemPath, path).getAbsolutePath()); } + @Nonnull @Override - public Page loadStaticPage(String basePath, String prefix, String mimeType) { + public Page loadStaticPage(@Nonnull String basePath, @Nonnull String prefix, @Nonnull String mimeType) { return new ReloadingPage<>(basePath, new File(filesystemPath, prefix).getAbsolutePath(), mimeType); } + @Nonnull @Override public TemplateProvider getTemplateProvider() { return new FilesystemTemplateProvider(new File(filesystemPath, "/templates/").getAbsolutePath()); diff --git a/src/main/java/net/pterodactylus/sone/main/DefaultLoaders.java b/src/main/java/net/pterodactylus/sone/main/DefaultLoaders.java index 72d8d19..2a2bd8c 100644 --- a/src/main/java/net/pterodactylus/sone/main/DefaultLoaders.java +++ b/src/main/java/net/pterodactylus/sone/main/DefaultLoaders.java @@ -1,11 +1,10 @@ package net.pterodactylus.sone.main; -import static net.pterodactylus.util.template.TemplateParser.parse; - import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; import java.io.UnsupportedEncodingException; +import javax.annotation.Nonnull; import net.pterodactylus.sone.web.WebInterface; import net.pterodactylus.util.io.Closer; @@ -16,13 +15,16 @@ import net.pterodactylus.util.web.Page; import net.pterodactylus.util.web.Request; import net.pterodactylus.util.web.StaticPage; +import static net.pterodactylus.util.template.TemplateParser.parse; + /** * Default {@link Loaders} implementation that loads resources from the classpath. */ public class DefaultLoaders implements Loaders { + @Nonnull @Override - public Template loadTemplate(String path) { + public Template loadTemplate(@Nonnull String path) { InputStream templateInputStream = null; Reader reader = null; try { @@ -37,12 +39,14 @@ public class DefaultLoaders implements Loaders { } } + @Nonnull @Override - public Page loadStaticPage(String pathPrefix, String basePath, String mimeType) { + public Page loadStaticPage(@Nonnull String pathPrefix, @Nonnull String basePath, @Nonnull String mimeType) { return new StaticPage(pathPrefix, basePath, mimeType) { }; } + @Nonnull @Override public TemplateProvider getTemplateProvider() { return new ClassPathTemplateProvider(WebInterface.class, "/templates/"); diff --git a/src/main/java/net/pterodactylus/sone/main/Loaders.java b/src/main/java/net/pterodactylus/sone/main/Loaders.java index 8ee5132..b07118b 100644 --- a/src/main/java/net/pterodactylus/sone/main/Loaders.java +++ b/src/main/java/net/pterodactylus/sone/main/Loaders.java @@ -1,5 +1,7 @@ package net.pterodactylus.sone.main; +import javax.annotation.Nonnull; + import net.pterodactylus.util.template.Template; import net.pterodactylus.util.template.TemplateProvider; import net.pterodactylus.util.web.Page; @@ -13,8 +15,8 @@ import com.google.inject.ImplementedBy; @ImplementedBy(DefaultLoaders.class) public interface Loaders { - Template loadTemplate(String path); - Page loadStaticPage(String basePath, String prefix, String mimeType); - TemplateProvider getTemplateProvider(); + @Nonnull Template loadTemplate(@Nonnull String path); + @Nonnull Page loadStaticPage(@Nonnull String basePath, @Nonnull String prefix, @Nonnull String mimeType); + @Nonnull TemplateProvider getTemplateProvider(); } diff --git a/src/main/java/net/pterodactylus/sone/main/SonePlugin.java b/src/main/java/net/pterodactylus/sone/main/SonePlugin.java index ceb336b..25c6dac 100644 --- a/src/main/java/net/pterodactylus/sone/main/SonePlugin.java +++ b/src/main/java/net/pterodactylus/sone/main/SonePlugin.java @@ -1,5 +1,5 @@ /* - * Sone - SonePlugin.java - Copyright © 2010–2019 David Roden + * Sone - SonePlugin.java - Copyright © 2010–2020 David 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 @@ -17,62 +17,34 @@ package net.pterodactylus.sone.main; -import static com.google.common.base.Optional.of; -import static java.util.logging.Logger.getLogger; +import static java.util.logging.Logger.*; -import java.io.File; -import java.util.logging.Handler; -import java.util.logging.Level; -import java.util.logging.LogRecord; import java.util.logging.Logger; - -import javax.inject.Singleton; - -import net.pterodactylus.sone.core.Core; -import net.pterodactylus.sone.database.Database; -import net.pterodactylus.sone.database.PostProvider; -import net.pterodactylus.sone.database.SoneProvider; -import net.pterodactylus.sone.database.memory.MemoryDatabase; -import net.pterodactylus.sone.fcp.FcpInterface; -import net.pterodactylus.sone.freenet.PluginStoreConfigurationBackend; -import net.pterodactylus.sone.freenet.wot.Context; -import net.pterodactylus.sone.freenet.wot.WebOfTrustConnector; -import net.pterodactylus.sone.web.WebInterface; -import net.pterodactylus.sone.web.WebInterfaceModule; -import net.pterodactylus.util.config.Configuration; -import net.pterodactylus.util.config.ConfigurationException; -import net.pterodactylus.util.config.MapConfigurationBackend; -import net.pterodactylus.util.version.Version; - -import com.google.common.base.Optional; -import com.google.common.cache.CacheBuilder; -import com.google.common.cache.CacheLoader; -import com.google.common.cache.LoadingCache; -import com.google.common.eventbus.EventBus; -import com.google.inject.AbstractModule; -import com.google.inject.Guice; -import com.google.inject.Injector; +import java.util.logging.*; + +import javax.annotation.Nonnull; + +import net.pterodactylus.sone.core.*; +import net.pterodactylus.sone.core.event.*; +import net.pterodactylus.sone.fcp.*; +import net.pterodactylus.sone.freenet.wot.*; +import net.pterodactylus.sone.web.*; +import net.pterodactylus.sone.web.notification.NotificationHandler; +import net.pterodactylus.sone.web.notification.NotificationHandlerModule; + +import freenet.l10n.BaseL10n.*; +import freenet.l10n.*; +import freenet.pluginmanager.*; +import freenet.support.*; +import freenet.support.api.*; + +import com.google.common.annotations.*; +import com.google.common.eventbus.*; +import com.google.common.cache.*; +import com.google.inject.*; import com.google.inject.Module; -import com.google.inject.TypeLiteral; -import com.google.inject.matcher.Matchers; -import com.google.inject.spi.InjectionListener; -import com.google.inject.spi.TypeEncounter; -import com.google.inject.spi.TypeListener; - -import freenet.client.async.PersistenceDisabledException; -import freenet.l10n.BaseL10n; -import freenet.l10n.BaseL10n.LANGUAGE; -import freenet.l10n.PluginL10n; -import freenet.pluginmanager.FredPlugin; -import freenet.pluginmanager.FredPluginBaseL10n; -import freenet.pluginmanager.FredPluginFCP; -import freenet.pluginmanager.FredPluginL10n; -import freenet.pluginmanager.FredPluginThreadless; -import freenet.pluginmanager.FredPluginVersioned; -import freenet.pluginmanager.PluginReplySender; -import freenet.pluginmanager.PluginRespirator; -import freenet.support.SimpleFieldSet; -import freenet.support.api.Bucket; +import com.google.inject.name.*; +import kotlin.jvm.functions.*; /** * This class interfaces with Freenet. It is the class that is loaded by the @@ -85,12 +57,13 @@ public class SonePlugin implements FredPlugin, FredPluginFCP, FredPluginL10n, Fr static { /* initialize logging. */ soneLogger.setUseParentHandlers(false); + soneLogger.setLevel(Level.ALL); soneLogger.addHandler(new Handler() { private final LoadingCache> classCache = CacheBuilder.newBuilder() .build(new CacheLoader>() { @Override - public Class load(String key) throws Exception { - return Class.forName(key); + public Class load(@Nonnull String key) throws Exception { + return SonePlugin.class.getClassLoader().loadClass(key); } }); @@ -122,19 +95,24 @@ public class SonePlugin implements FredPlugin, FredPluginFCP, FredPluginL10n, Fr } /** The current year at time of release. */ - private static final int YEAR = 2019; + private static final int YEAR = 2020; private static final String SONE_HOMEPAGE = "USK@nwa8lHa271k2QvJ8aa0Ov7IHAV-DFOCFgmDt3X6BpCI,DuQSUZiI~agF8c-6tjsFFGuZ8eICrzWCILB60nT8KKo,AQACAAE/sone/"; - private static final int LATEST_EDITION = 79; + private static final int LATEST_EDITION = 80; /** The logger. */ private static final Logger logger = getLogger(SonePlugin.class.getName()); + private final Function1 injectorCreator; + /** The plugin respirator. */ private PluginRespirator pluginRespirator; /** The core. */ private Core core; + /** The event bus. */ + private EventBus eventBus; + /** The web interface. */ private WebInterface webInterface; @@ -147,6 +125,15 @@ public class SonePlugin implements FredPlugin, FredPluginFCP, FredPluginL10n, Fr /** The web of trust connector. */ private WebOfTrustConnector webOfTrustConnector; + public SonePlugin() { + this(Guice::createInjector); + } + + @VisibleForTesting + public SonePlugin(Function1 injectorCreator) { + this.injectorCreator = injectorCreator; + } + // // ACCESSORS // @@ -206,92 +193,7 @@ public class SonePlugin implements FredPlugin, FredPluginFCP, FredPluginL10n, Fr public void runPlugin(PluginRespirator pluginRespirator) { this.pluginRespirator = pluginRespirator; - /* create a configuration. */ - Configuration oldConfiguration; - Configuration newConfiguration = null; - boolean firstStart = !new File("sone.properties").exists(); - boolean newConfig = false; - try { - oldConfiguration = new Configuration(new MapConfigurationBackend(new File("sone.properties"), false)); - newConfiguration = oldConfiguration; - } catch (ConfigurationException ce1) { - newConfig = true; - logger.log(Level.INFO, "Could not load configuration file, trying plugin store…", ce1); - try { - newConfiguration = new Configuration(new MapConfigurationBackend(new File("sone.properties"), true)); - logger.log(Level.INFO, "Created new configuration file."); - } catch (ConfigurationException ce2) { - logger.log(Level.SEVERE, "Could not create configuration file, using Plugin Store!", ce2); - } - try { - oldConfiguration = new Configuration(new PluginStoreConfigurationBackend(pluginRespirator)); - logger.log(Level.INFO, "Plugin store loaded."); - } catch (PersistenceDisabledException pde1) { - logger.log(Level.SEVERE, "Could not load any configuration, using empty configuration!"); - oldConfiguration = new Configuration(new MapConfigurationBackend()); - } - } - - final Configuration startConfiguration; - if ((newConfiguration != null) && (oldConfiguration != newConfiguration)) { - logger.log(Level.INFO, "Setting configuration to file-based configuration."); - startConfiguration = newConfiguration; - } else { - startConfiguration = oldConfiguration; - } - final EventBus eventBus = new EventBus(); - - /* Freenet injector configuration. */ - FreenetModule freenetModule = new FreenetModule(pluginRespirator); - - /* Sone injector configuration. */ - AbstractModule soneModule = new AbstractModule() { - - @Override - protected void configure() { - bind(EventBus.class).toInstance(eventBus); - bind(Configuration.class).toInstance(startConfiguration); - Context context = new Context("Sone"); - bind(Context.class).toInstance(context); - bind(getOptionalContextTypeLiteral()).toInstance(of(context)); - bind(SonePlugin.class).toInstance(SonePlugin.this); - bind(Version.class).toInstance(Version.parse(getVersion().substring(1))); - bind(PluginVersion.class).toInstance(new PluginVersion(getVersion())); - bind(PluginYear.class).toInstance(new PluginYear(getYear())); - bind(PluginHomepage.class).toInstance(new PluginHomepage(getHomepage())); - bind(Database.class).to(MemoryDatabase.class).in(Singleton.class); - bind(BaseL10n.class).toInstance(l10n.getBase()); - bind(SoneProvider.class).to(Core.class).in(Singleton.class); - bind(PostProvider.class).to(Core.class).in(Singleton.class); - if (startConfiguration.getBooleanValue("Developer.LoadFromFilesystem").getValue(false)) { - String path = startConfiguration.getStringValue("Developer.FilesystemPath").getValue(null); - if (path != null) { - bind(Loaders.class).toInstance(new DebugLoaders(path)); - } - } - bindListener(Matchers.any(), new TypeListener() { - - @Override - public void hear(TypeLiteral typeLiteral, TypeEncounter typeEncounter) { - typeEncounter.register(new InjectionListener() { - - @Override - public void afterInjection(I injectee) { - eventBus.register(injectee); - } - }); - } - }); - } - - private TypeLiteral> getOptionalContextTypeLiteral() { - return new TypeLiteral>() { - }; - } - - }; - Module webInterfaceModule = new WebInterfaceModule(); - Injector injector = Guice.createInjector(freenetModule, soneModule, webInterfaceModule); + Injector injector = createInjector(); core = injector.getInstance(Core.class); /* create web of trust connector. */ @@ -303,11 +205,47 @@ public class SonePlugin implements FredPlugin, FredPluginFCP, FredPluginL10n, Fr /* create the web interface. */ webInterface = injector.getInstance(WebInterface.class); + /* we need to request this to install all notification handlers. */ + injector.getInstance(NotificationHandler.class); + + /* and this is required to shutdown all tickers. */ + injector.getInstance(TickerShutdown.class); + /* start core! */ core.start(); + + /* start the web interface! */ webInterface.start(); - webInterface.setFirstStart(firstStart); - webInterface.setNewConfig(newConfig); + + /* send some events on startup */ + eventBus = injector.getInstance(EventBus.class); + + /* first start? */ + if (injector.getInstance(Key.get(Boolean.class, Names.named("FirstStart")))) { + eventBus.post(new FirstStart()); + } else { + /* new config? */ + if (injector.getInstance(Key.get(Boolean.class, Names.named("NewConfig")))) { + eventBus.post(new ConfigNotRead()); + } + } + + eventBus.post(new Startup()); + } + + @VisibleForTesting + protected Injector createInjector() { + FreenetModule freenetModule = new FreenetModule(pluginRespirator); + AbstractModule soneModule = new SoneModule(this, new EventBus()); + Module webInterfaceModule = new WebInterfaceModule(); + Module notificationHandlerModule = new NotificationHandlerModule(); + + return createInjector(freenetModule, soneModule, webInterfaceModule, notificationHandlerModule); + } + + @VisibleForTesting + protected Injector createInjector(Module... modules) { + return injectorCreator.invoke(modules); } /** @@ -315,6 +253,9 @@ public class SonePlugin implements FredPlugin, FredPluginFCP, FredPluginL10n, Fr */ @Override public void terminate() { + /* send shutdown event. */ + eventBus.post(new Shutdown()); + try { /* stop the web interface. */ webInterface.stop(); diff --git a/src/main/java/net/pterodactylus/sone/notify/ListNotification.java b/src/main/java/net/pterodactylus/sone/notify/ListNotification.java deleted file mode 100644 index 6a7b086..0000000 --- a/src/main/java/net/pterodactylus/sone/notify/ListNotification.java +++ /dev/null @@ -1,199 +0,0 @@ -/* - * Sone - ListNotification.java - Copyright © 2010–2019 David 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 . - */ - -package net.pterodactylus.sone.notify; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.concurrent.CopyOnWriteArrayList; - -import net.pterodactylus.util.notify.TemplateNotification; -import net.pterodactylus.util.template.Template; - -/** - * Notification that maintains a list of new elements. - * - * @param - * The type of the items - */ -public class ListNotification extends TemplateNotification { - - /** The key under which to store the elements in the template. */ - private final String key; - - /** The list of new elements. */ - private final List elements = new CopyOnWriteArrayList<>(); - - /** - * Creates a new list notification. - * - * @param id - * The ID of the notification - * @param key - * The key under which to store the elements in the template - * @param template - * The template to render - */ - public ListNotification(String id, String key, Template template) { - this(id, key, template, true); - } - - /** - * Creates a new list notification. - * - * @param id - * The ID of the notification - * @param key - * The key under which to store the elements in the template - * @param template - * The template to render - * @param dismissable - * {@code true} if this notification should be dismissable by the - * user, {@code false} otherwise - */ - public ListNotification(String id, String key, Template template, boolean dismissable) { - super(id, System.currentTimeMillis(), System.currentTimeMillis(), dismissable, template); - this.key = key; - template.getInitialContext().set(key, elements); - } - - /** - * Creates a new list notification that copies its ID and the template from - * the given list notification. - * - * @param listNotification - * The list notification to copy - */ - public ListNotification(ListNotification listNotification) { - super(listNotification.getId(), listNotification.getCreatedTime(), listNotification.getLastUpdatedTime(), listNotification.isDismissable(), new Template()); - this.key = listNotification.key; - getTemplate().add(listNotification.getTemplate()); - getTemplate().getInitialContext().set(key, elements); - } - - // - // ACTIONS - // - - /** - * Returns the current list of elements. - * - * @return The current list of elements - */ - public List getElements() { - return new ArrayList<>(elements); - } - - /** - * Sets the elements to show in this notification. This method will not call - * {@link #touch()}. - * - * @param elements - * The elements to show - */ - public void setElements(Collection elements) { - this.elements.clear(); - this.elements.addAll(elements); - touch(); - } - - /** - * Returns whether there are any new elements. - * - * @return {@code true} if there are no new elements, {@code false} if there - * are new elements - */ - public boolean isEmpty() { - return elements.isEmpty(); - } - - /** - * Adds a discovered element. - * - * @param element - * The new element - */ - public void add(T element) { - elements.add(element); - touch(); - } - - /** - * Removes the given element from the list of new elements. - * - * @param element - * The element to remove - */ - public void remove(T element) { - while (elements.remove(element)) { - /* do nothing, just remove all instances of the element. */ - } - if (elements.isEmpty()) { - dismiss(); - } - touch(); - } - - // - // ABSTRACTNOTIFICATION METHODS - // - - /** - * {@inheritDoc} - */ - @Override - public void dismiss() { - super.dismiss(); - elements.clear(); - } - - // - // OBJECT METHODS - // - - /** - * {@inheritDoc} - */ - @Override - public int hashCode() { - int hashCode = super.hashCode(); - for (T element : elements) { - hashCode ^= element.hashCode(); - } - return hashCode; - } - - /** - * {@inheritDoc} - */ - @Override - public boolean equals(Object object) { - if (!(object instanceof ListNotification)) { - return false; - } - ListNotification listNotification = (ListNotification) object; - if (!super.equals(listNotification)) { - return false; - } - if (!key.equals(listNotification.key)) { - return false; - } - return elements.equals(listNotification.elements); - } - -} diff --git a/src/main/java/net/pterodactylus/sone/notify/ListNotificationFilter.java b/src/main/java/net/pterodactylus/sone/notify/ListNotificationFilter.java index 50a1087..739907c 100644 --- a/src/main/java/net/pterodactylus/sone/notify/ListNotificationFilter.java +++ b/src/main/java/net/pterodactylus/sone/notify/ListNotificationFilter.java @@ -1,5 +1,5 @@ /* - * Sone - ListNotificationFilter.java - Copyright © 2010–2019 David Roden + * Sone - ListNotificationFilter.java - Copyright © 2010–2020 David 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 diff --git a/src/main/java/net/pterodactylus/sone/template/AlbumAccessor.java b/src/main/java/net/pterodactylus/sone/template/AlbumAccessor.java index 19b600e..87378cc 100644 --- a/src/main/java/net/pterodactylus/sone/template/AlbumAccessor.java +++ b/src/main/java/net/pterodactylus/sone/template/AlbumAccessor.java @@ -1,5 +1,5 @@ /* - * Sone - AlbumAccessor.java - Copyright © 2011–2019 David Roden + * Sone - AlbumAccessor.java - Copyright © 2011–2020 David 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 diff --git a/src/main/java/net/pterodactylus/sone/template/CollectionAccessor.java b/src/main/java/net/pterodactylus/sone/template/CollectionAccessor.java index 8ce97ad..f170701 100644 --- a/src/main/java/net/pterodactylus/sone/template/CollectionAccessor.java +++ b/src/main/java/net/pterodactylus/sone/template/CollectionAccessor.java @@ -1,5 +1,5 @@ /* - * Sone - CollectionAccessor.java - Copyright © 2010–2019 David Roden + * Sone - CollectionAccessor.java - Copyright © 2010–2020 David 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 diff --git a/src/main/java/net/pterodactylus/sone/template/CssClassNameFilter.java b/src/main/java/net/pterodactylus/sone/template/CssClassNameFilter.java index 1ff4f04..6439c44 100644 --- a/src/main/java/net/pterodactylus/sone/template/CssClassNameFilter.java +++ b/src/main/java/net/pterodactylus/sone/template/CssClassNameFilter.java @@ -1,5 +1,5 @@ /* - * Sone - CssClassNameFilter.java - Copyright © 2010–2019 David Roden + * Sone - CssClassNameFilter.java - Copyright © 2010–2020 David 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 diff --git a/src/main/java/net/pterodactylus/sone/template/GetPagePlugin.java b/src/main/java/net/pterodactylus/sone/template/GetPagePlugin.java index 172b4b2..2910ec8 100644 --- a/src/main/java/net/pterodactylus/sone/template/GetPagePlugin.java +++ b/src/main/java/net/pterodactylus/sone/template/GetPagePlugin.java @@ -1,5 +1,5 @@ /* - * Sone - GetPagePlugin.java - Copyright © 2010–2019 David Roden + * Sone - GetPagePlugin.java - Copyright © 2010–2020 David 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 diff --git a/src/main/java/net/pterodactylus/sone/template/HttpRequestAccessor.java b/src/main/java/net/pterodactylus/sone/template/HttpRequestAccessor.java index 0e6fda0..a43bd70 100644 --- a/src/main/java/net/pterodactylus/sone/template/HttpRequestAccessor.java +++ b/src/main/java/net/pterodactylus/sone/template/HttpRequestAccessor.java @@ -1,5 +1,5 @@ /* - * Sone - HttpRequestAccessor.java - Copyright © 2011–2019 David Roden + * Sone - HttpRequestAccessor.java - Copyright © 2011–2020 David 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 diff --git a/src/main/java/net/pterodactylus/sone/template/IdentityAccessor.java b/src/main/java/net/pterodactylus/sone/template/IdentityAccessor.java index ed139de..5b229dd 100644 --- a/src/main/java/net/pterodactylus/sone/template/IdentityAccessor.java +++ b/src/main/java/net/pterodactylus/sone/template/IdentityAccessor.java @@ -1,5 +1,5 @@ /* - * Sone - IdentityAccessor.java - Copyright © 2010–2019 David Roden + * Sone - IdentityAccessor.java - Copyright © 2010–2020 David 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 diff --git a/src/main/java/net/pterodactylus/sone/template/ImageAccessor.java b/src/main/java/net/pterodactylus/sone/template/ImageAccessor.java index 78c3aeb..6feb94e 100644 --- a/src/main/java/net/pterodactylus/sone/template/ImageAccessor.java +++ b/src/main/java/net/pterodactylus/sone/template/ImageAccessor.java @@ -1,5 +1,5 @@ /* - * Sone - ImageAccessor.java - Copyright © 2011–2019 David Roden + * Sone - ImageAccessor.java - Copyright © 2011–2020 David 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 diff --git a/src/main/java/net/pterodactylus/sone/template/ImageLinkFilter.java b/src/main/java/net/pterodactylus/sone/template/ImageLinkFilter.java index fc4b803..85a7562 100644 --- a/src/main/java/net/pterodactylus/sone/template/ImageLinkFilter.java +++ b/src/main/java/net/pterodactylus/sone/template/ImageLinkFilter.java @@ -1,5 +1,5 @@ /* - * Sone - ImageLinkFilter.java - Copyright © 2011–2019 David Roden + * Sone - ImageLinkFilter.java - Copyright © 2011–2020 David 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 diff --git a/src/main/java/net/pterodactylus/sone/template/JavascriptFilter.java b/src/main/java/net/pterodactylus/sone/template/JavascriptFilter.java index 8dd2b04..a5f734d 100644 --- a/src/main/java/net/pterodactylus/sone/template/JavascriptFilter.java +++ b/src/main/java/net/pterodactylus/sone/template/JavascriptFilter.java @@ -1,5 +1,5 @@ /* - * Sone - JavascriptFilter.java - Copyright © 2011–2019 David Roden + * Sone - JavascriptFilter.java - Copyright © 2011–2020 David 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 diff --git a/src/main/java/net/pterodactylus/sone/template/PostAccessor.java b/src/main/java/net/pterodactylus/sone/template/PostAccessor.java deleted file mode 100644 index 4025c7b..0000000 --- a/src/main/java/net/pterodactylus/sone/template/PostAccessor.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Sone - PostAccessor.java - Copyright © 2010–2019 David 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 . - */ - -package net.pterodactylus.sone.template; - -import net.pterodactylus.sone.core.Core; -import net.pterodactylus.sone.data.Post; -import net.pterodactylus.sone.data.Reply; -import net.pterodactylus.sone.data.Sone; -import net.pterodactylus.util.template.ReflectionAccessor; -import net.pterodactylus.util.template.TemplateContext; - -import com.google.common.collect.Collections2; - -/** - * Accessor for {@link Post} objects that adds additional properties: - *

- *
replies
- *
All replies to this post, sorted by time, oldest first
- *
- */ -public class PostAccessor extends ReflectionAccessor { - - /** The core to get the replies from. */ - private final Core core; - - /** - * Creates a new post accessor. - * - * @param core - * The core to get the replies from - */ - public PostAccessor(Core core) { - this.core = core; - } - - /** - * {@inheritDoc} - */ - @Override - public Object get(TemplateContext templateContext, Object object, String member) { - Post post = (Post) object; - if ("replies".equals(member)) { - return Collections2.filter(core.getReplies(post.getId()), Reply.FUTURE_REPLY_FILTER); - } else if (member.equals("likes")) { - return core.getLikes(post); - } else if (member.equals("liked")) { - Sone currentSone = (Sone) templateContext.get("currentSone"); - return (currentSone != null) && (currentSone.isLikedPostId(post.getId())); - } else if (member.equals("new")) { - return !post.isKnown(); - } else if (member.equals("bookmarked")) { - return core.isBookmarked(post); - } - return super.get(templateContext, object, member); - } - -} diff --git a/src/main/java/net/pterodactylus/sone/template/ProfileAccessor.java b/src/main/java/net/pterodactylus/sone/template/ProfileAccessor.java index a152038..7dc1eb5 100644 --- a/src/main/java/net/pterodactylus/sone/template/ProfileAccessor.java +++ b/src/main/java/net/pterodactylus/sone/template/ProfileAccessor.java @@ -1,5 +1,5 @@ /* - * Sone - ProfileAccessor.java - Copyright © 2011–2019 David Roden + * Sone - ProfileAccessor.java - Copyright © 2011–2020 David 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 diff --git a/src/main/java/net/pterodactylus/sone/template/ReplyAccessor.java b/src/main/java/net/pterodactylus/sone/template/ReplyAccessor.java index c6dea31..fe1ccd3 100644 --- a/src/main/java/net/pterodactylus/sone/template/ReplyAccessor.java +++ b/src/main/java/net/pterodactylus/sone/template/ReplyAccessor.java @@ -1,5 +1,5 @@ /* - * Sone - ReplyAccessor.java - Copyright © 2010–2019 David Roden + * Sone - ReplyAccessor.java - Copyright © 2010–2020 David 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 diff --git a/src/main/java/net/pterodactylus/sone/template/ReplyGroupFilter.java b/src/main/java/net/pterodactylus/sone/template/ReplyGroupFilter.java index c093e6f..9623b62 100644 --- a/src/main/java/net/pterodactylus/sone/template/ReplyGroupFilter.java +++ b/src/main/java/net/pterodactylus/sone/template/ReplyGroupFilter.java @@ -1,5 +1,5 @@ /* - * Sone - ReplyGroupFilter.java - Copyright © 2010–2019 David Roden + * Sone - ReplyGroupFilter.java - Copyright © 2010–2020 David 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 diff --git a/src/main/java/net/pterodactylus/sone/template/RequestChangeFilter.java b/src/main/java/net/pterodactylus/sone/template/RequestChangeFilter.java index d004dfc..16cb062 100644 --- a/src/main/java/net/pterodactylus/sone/template/RequestChangeFilter.java +++ b/src/main/java/net/pterodactylus/sone/template/RequestChangeFilter.java @@ -1,5 +1,5 @@ /* - * Sone - RequestChangeFilter.java - Copyright © 2010–2019 David Roden + * Sone - RequestChangeFilter.java - Copyright © 2010–2020 David 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 diff --git a/src/main/java/net/pterodactylus/sone/template/SoneAccessor.java b/src/main/java/net/pterodactylus/sone/template/SoneAccessor.java index a8cbe57..4bc6a5f 100644 --- a/src/main/java/net/pterodactylus/sone/template/SoneAccessor.java +++ b/src/main/java/net/pterodactylus/sone/template/SoneAccessor.java @@ -1,5 +1,5 @@ /* - * Sone - SoneAccessor.java - Copyright © 2010–2019 David Roden + * Sone - SoneAccessor.java - Copyright © 2010–2020 David 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 diff --git a/src/main/java/net/pterodactylus/sone/template/SubstringFilter.java b/src/main/java/net/pterodactylus/sone/template/SubstringFilter.java index 355b6a9..2a2e526 100644 --- a/src/main/java/net/pterodactylus/sone/template/SubstringFilter.java +++ b/src/main/java/net/pterodactylus/sone/template/SubstringFilter.java @@ -1,5 +1,5 @@ /* - * Sone - SubstringFilter.java - Copyright © 2010–2019 David Roden + * Sone - SubstringFilter.java - Copyright © 2010–2020 David 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 diff --git a/src/main/java/net/pterodactylus/sone/template/TrustAccessor.java b/src/main/java/net/pterodactylus/sone/template/TrustAccessor.java index 26a128b..6ccc897 100644 --- a/src/main/java/net/pterodactylus/sone/template/TrustAccessor.java +++ b/src/main/java/net/pterodactylus/sone/template/TrustAccessor.java @@ -1,5 +1,5 @@ /* - * Sone - TrustAccessor.java - Copyright © 2010–2019 David Roden + * Sone - TrustAccessor.java - Copyright © 2010–2020 David 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 diff --git a/src/main/java/net/pterodactylus/sone/template/UniqueElementFilter.java b/src/main/java/net/pterodactylus/sone/template/UniqueElementFilter.java index 6fb584e..0e12448 100644 --- a/src/main/java/net/pterodactylus/sone/template/UniqueElementFilter.java +++ b/src/main/java/net/pterodactylus/sone/template/UniqueElementFilter.java @@ -1,5 +1,5 @@ /* - * Sone - UniqueElementFilter.java - Copyright © 2011–2019 David Roden + * Sone - UniqueElementFilter.java - Copyright © 2011–2020 David 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 diff --git a/src/main/java/net/pterodactylus/sone/template/UnknownDateFilter.java b/src/main/java/net/pterodactylus/sone/template/UnknownDateFilter.java index 0d4872b..2d6c05c 100644 --- a/src/main/java/net/pterodactylus/sone/template/UnknownDateFilter.java +++ b/src/main/java/net/pterodactylus/sone/template/UnknownDateFilter.java @@ -1,5 +1,5 @@ /* - * Sone - UnknownDateFilter.java - Copyright © 2011–2019 David Roden + * Sone - UnknownDateFilter.java - Copyright © 2011–2020 David 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 @@ -19,18 +19,18 @@ package net.pterodactylus.sone.template; import java.util.Map; +import net.pterodactylus.sone.freenet.Translation; import net.pterodactylus.util.template.Filter; import net.pterodactylus.util.template.TemplateContext; -import freenet.l10n.BaseL10n; /** * {@link Filter} implementation that replaces a {@link Long} with a value of - * {@code 0} by a {@link String} from an {@link BaseL10n l10n handler}. + * {@code 0} by a {@link String} from a {@link Translation translation}. */ public class UnknownDateFilter implements Filter { - /** The l10n handler. */ - private BaseL10n l10nHandler; + /** The translation. */ + private final Translation translation; /** The key for the text to show. */ private final String unknownKey; @@ -38,13 +38,11 @@ public class UnknownDateFilter implements Filter { /** * Creates a new unknown date filter. * - * @param l10nHandler - * The l10n handler - * @param unknownKey - * The key of the text to show + * @param translation The translation + * @param unknownKey The key of the text to show */ - public UnknownDateFilter(BaseL10n l10nHandler, String unknownKey) { - this.l10nHandler = l10nHandler; + public UnknownDateFilter(Translation translation, String unknownKey) { + this.translation = translation; this.unknownKey = unknownKey; } @@ -55,7 +53,7 @@ public class UnknownDateFilter implements Filter { public Object format(TemplateContext templateContext, Object data, Map parameters) { if (data instanceof Long) { if ((Long) data == 0) { - return l10nHandler.getString(unknownKey); + return translation.translate(unknownKey); } } return data; diff --git a/src/main/java/net/pterodactylus/sone/text/Parser.java b/src/main/java/net/pterodactylus/sone/text/Parser.java index 0daf30c..06eed37 100644 --- a/src/main/java/net/pterodactylus/sone/text/Parser.java +++ b/src/main/java/net/pterodactylus/sone/text/Parser.java @@ -1,5 +1,5 @@ /* - * Sone - Parser.java - Copyright © 2010–2019 David Roden + * Sone - Parser.java - Copyright © 2010–2020 David 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 diff --git a/src/main/java/net/pterodactylus/sone/text/ParserContext.java b/src/main/java/net/pterodactylus/sone/text/ParserContext.java index 056fcc1..468b565 100644 --- a/src/main/java/net/pterodactylus/sone/text/ParserContext.java +++ b/src/main/java/net/pterodactylus/sone/text/ParserContext.java @@ -1,5 +1,5 @@ /* - * Sone - ParserContext.java - Copyright © 2010–2019 David Roden + * Sone - ParserContext.java - Copyright © 2010–2020 David 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 diff --git a/src/main/java/net/pterodactylus/sone/text/PostPart.java b/src/main/java/net/pterodactylus/sone/text/PostPart.java index de957c5..fac2b81 100644 --- a/src/main/java/net/pterodactylus/sone/text/PostPart.java +++ b/src/main/java/net/pterodactylus/sone/text/PostPart.java @@ -1,5 +1,5 @@ /* - * Sone - PostPart.java - Copyright © 2011–2019 David Roden + * Sone - PostPart.java - Copyright © 2011–2020 David 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 diff --git a/src/main/java/net/pterodactylus/sone/text/SoneTextParserContext.java b/src/main/java/net/pterodactylus/sone/text/SoneTextParserContext.java index 13a7a26..d3a1557 100644 --- a/src/main/java/net/pterodactylus/sone/text/SoneTextParserContext.java +++ b/src/main/java/net/pterodactylus/sone/text/SoneTextParserContext.java @@ -1,5 +1,5 @@ /* - * Sone - SoneTextParserContext.java - Copyright © 2011–2019 David Roden + * Sone - SoneTextParserContext.java - Copyright © 2011–2020 David 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 diff --git a/src/main/java/net/pterodactylus/sone/text/TextFilter.java b/src/main/java/net/pterodactylus/sone/text/TextFilter.java index 0703f32..885843e 100644 --- a/src/main/java/net/pterodactylus/sone/text/TextFilter.java +++ b/src/main/java/net/pterodactylus/sone/text/TextFilter.java @@ -1,5 +1,5 @@ /* - * Sone - TextFilter.java - Copyright © 2011–2019 David Roden + * Sone - TextFilter.java - Copyright © 2011–2020 David 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 diff --git a/src/main/java/net/pterodactylus/sone/utils/IntegerRangePredicate.java b/src/main/java/net/pterodactylus/sone/utils/IntegerRangePredicate.java index 1fd7527..3a631cf 100644 --- a/src/main/java/net/pterodactylus/sone/utils/IntegerRangePredicate.java +++ b/src/main/java/net/pterodactylus/sone/utils/IntegerRangePredicate.java @@ -1,5 +1,5 @@ /* - * Sone - IntegerRangePredicate.java - Copyright © 2013–2019 David Roden + * Sone - IntegerRangePredicate.java - Copyright © 2013–2020 David 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 diff --git a/src/main/java/net/pterodactylus/sone/web/AllPages.kt b/src/main/java/net/pterodactylus/sone/web/AllPages.kt index 9eccccf..c538f0e 100644 --- a/src/main/java/net/pterodactylus/sone/web/AllPages.kt +++ b/src/main/java/net/pterodactylus/sone/web/AllPages.kt @@ -23,7 +23,6 @@ class AllPages { @Inject lateinit var deleteReplyPage: DeleteReplyPage @Inject lateinit var deleteSonePage: DeleteSonePage @Inject lateinit var dismissNotificationPage: DismissNotificationPage - @Inject lateinit var distrustPage: DistrustPage @Inject lateinit var editAlbumPage: EditAlbumPage @Inject lateinit var editImagePage: EditImagePage @Inject lateinit var editProfileFieldPage: EditProfileFieldPage @@ -42,12 +41,10 @@ class AllPages { @Inject lateinit var optionsPage: OptionsPage @Inject lateinit var rescuePage: RescuePage @Inject lateinit var searchPage: SearchPage - @Inject lateinit var trustPage: TrustPage @Inject lateinit var unbookmarkPage: UnbookmarkPage @Inject lateinit var unfollowSonePage: UnfollowSonePage @Inject lateinit var unlikePage: UnlikePage @Inject lateinit var unlockSonePage: UnlockSonePage - @Inject lateinit var untrustPage: UntrustPage @Inject lateinit var uploadImagePage: UploadImagePage @Inject lateinit var viewPostPage: ViewPostPage @Inject lateinit var viewSonePage: ViewSonePage diff --git a/src/main/java/net/pterodactylus/sone/web/WebInterface.java b/src/main/java/net/pterodactylus/sone/web/WebInterface.java index 3d131c8..5448054 100644 --- a/src/main/java/net/pterodactylus/sone/web/WebInterface.java +++ b/src/main/java/net/pterodactylus/sone/web/WebInterface.java @@ -1,5 +1,5 @@ /* - * Sone - WebInterface.java - Copyright © 2010–2019 David Roden + * Sone - WebInterface.java - Copyright © 2010–2020 David 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 @@ -19,51 +19,24 @@ package net.pterodactylus.sone.web; import static com.google.common.collect.FluentIterable.from; import static java.util.logging.Logger.getLogger; -import static net.pterodactylus.util.template.TemplateParser.parse; -import java.io.StringReader; import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; import java.util.Set; import java.util.TimeZone; import java.util.UUID; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; import java.util.logging.Logger; import javax.annotation.Nonnull; import javax.annotation.Nullable; +import javax.inject.Named; import net.pterodactylus.sone.core.Core; import net.pterodactylus.sone.core.ElementLoader; -import net.pterodactylus.sone.core.event.ImageInsertAbortedEvent; -import net.pterodactylus.sone.core.event.ImageInsertFailedEvent; -import net.pterodactylus.sone.core.event.ImageInsertFinishedEvent; -import net.pterodactylus.sone.core.event.ImageInsertStartedEvent; -import net.pterodactylus.sone.core.event.MarkPostKnownEvent; -import net.pterodactylus.sone.core.event.MarkPostReplyKnownEvent; -import net.pterodactylus.sone.core.event.MarkSoneKnownEvent; -import net.pterodactylus.sone.core.event.NewPostFoundEvent; -import net.pterodactylus.sone.core.event.NewPostReplyFoundEvent; -import net.pterodactylus.sone.core.event.NewSoneFoundEvent; -import net.pterodactylus.sone.core.event.PostRemovedEvent; -import net.pterodactylus.sone.core.event.PostReplyRemovedEvent; -import net.pterodactylus.sone.core.event.SoneInsertAbortedEvent; -import net.pterodactylus.sone.core.event.SoneInsertedEvent; -import net.pterodactylus.sone.core.event.SoneInsertingEvent; -import net.pterodactylus.sone.core.event.SoneLockedEvent; -import net.pterodactylus.sone.core.event.SoneRemovedEvent; -import net.pterodactylus.sone.core.event.SoneUnlockedEvent; -import net.pterodactylus.sone.core.event.UpdateFoundEvent; -import net.pterodactylus.sone.data.Image; +import net.pterodactylus.sone.core.event.*; import net.pterodactylus.sone.data.Post; import net.pterodactylus.sone.data.PostReply; import net.pterodactylus.sone.data.Sone; import net.pterodactylus.sone.freenet.L10nFilter; +import net.pterodactylus.sone.freenet.Translation; import net.pterodactylus.sone.main.Loaders; import net.pterodactylus.sone.main.PluginHomepage; import net.pterodactylus.sone.main.PluginVersion; @@ -77,9 +50,6 @@ import net.pterodactylus.sone.template.LinkedElementRenderFilter; import net.pterodactylus.sone.template.ParserFilter; import net.pterodactylus.sone.template.RenderFilter; import net.pterodactylus.sone.template.ShortenFilter; -import net.pterodactylus.sone.text.Part; -import net.pterodactylus.sone.text.SonePart; -import net.pterodactylus.sone.text.SoneTextParser; import net.pterodactylus.sone.text.TimeTextConverter; import net.pterodactylus.sone.web.ajax.BookmarkAjaxPage; import net.pterodactylus.sone.web.ajax.CreatePostAjaxPage; @@ -88,7 +58,6 @@ import net.pterodactylus.sone.web.ajax.DeletePostAjaxPage; import net.pterodactylus.sone.web.ajax.DeleteProfileFieldAjaxPage; import net.pterodactylus.sone.web.ajax.DeleteReplyAjaxPage; import net.pterodactylus.sone.web.ajax.DismissNotificationAjaxPage; -import net.pterodactylus.sone.web.ajax.DistrustAjaxPage; import net.pterodactylus.sone.web.ajax.EditAlbumAjaxPage; import net.pterodactylus.sone.web.ajax.EditImageAjaxPage; import net.pterodactylus.sone.web.ajax.EditProfileFieldAjaxPage; @@ -105,64 +74,15 @@ import net.pterodactylus.sone.web.ajax.LikeAjaxPage; import net.pterodactylus.sone.web.ajax.LockSoneAjaxPage; import net.pterodactylus.sone.web.ajax.MarkAsKnownAjaxPage; import net.pterodactylus.sone.web.ajax.MoveProfileFieldAjaxPage; -import net.pterodactylus.sone.web.ajax.TrustAjaxPage; import net.pterodactylus.sone.web.ajax.UnbookmarkAjaxPage; import net.pterodactylus.sone.web.ajax.UnfollowSoneAjaxPage; import net.pterodactylus.sone.web.ajax.UnlikeAjaxPage; import net.pterodactylus.sone.web.ajax.UnlockSoneAjaxPage; -import net.pterodactylus.sone.web.ajax.UntrustAjaxPage; import net.pterodactylus.sone.web.page.FreenetRequest; import net.pterodactylus.sone.web.page.TemplateRenderer; -import net.pterodactylus.sone.web.pages.AboutPage; -import net.pterodactylus.sone.web.pages.BookmarkPage; -import net.pterodactylus.sone.web.pages.BookmarksPage; -import net.pterodactylus.sone.web.pages.CreateAlbumPage; -import net.pterodactylus.sone.web.pages.CreatePostPage; -import net.pterodactylus.sone.web.pages.CreateReplyPage; -import net.pterodactylus.sone.web.pages.CreateSonePage; -import net.pterodactylus.sone.web.pages.DeleteAlbumPage; -import net.pterodactylus.sone.web.pages.DeleteImagePage; -import net.pterodactylus.sone.web.pages.DeletePostPage; -import net.pterodactylus.sone.web.pages.DeleteProfileFieldPage; -import net.pterodactylus.sone.web.pages.DeleteReplyPage; -import net.pterodactylus.sone.web.pages.DeleteSonePage; -import net.pterodactylus.sone.web.pages.DismissNotificationPage; -import net.pterodactylus.sone.web.pages.DistrustPage; -import net.pterodactylus.sone.web.pages.EditAlbumPage; -import net.pterodactylus.sone.web.pages.EditImagePage; -import net.pterodactylus.sone.web.pages.EditProfileFieldPage; -import net.pterodactylus.sone.web.pages.EditProfilePage; -import net.pterodactylus.sone.web.pages.EmptyAlbumTitlePage; -import net.pterodactylus.sone.web.pages.EmptyImageTitlePage; -import net.pterodactylus.sone.web.pages.FollowSonePage; -import net.pterodactylus.sone.web.pages.GetImagePage; -import net.pterodactylus.sone.web.pages.ImageBrowserPage; -import net.pterodactylus.sone.web.pages.IndexPage; -import net.pterodactylus.sone.web.pages.InvalidPage; -import net.pterodactylus.sone.web.pages.KnownSonesPage; -import net.pterodactylus.sone.web.pages.LikePage; -import net.pterodactylus.sone.web.pages.LockSonePage; -import net.pterodactylus.sone.web.pages.LoginPage; -import net.pterodactylus.sone.web.pages.LogoutPage; -import net.pterodactylus.sone.web.pages.MarkAsKnownPage; -import net.pterodactylus.sone.web.pages.NewPage; -import net.pterodactylus.sone.web.pages.NoPermissionPage; -import net.pterodactylus.sone.web.pages.OptionsPage; -import net.pterodactylus.sone.web.pages.RescuePage; -import net.pterodactylus.sone.web.pages.SearchPage; -import net.pterodactylus.sone.web.pages.SoneTemplatePage; -import net.pterodactylus.sone.web.pages.TrustPage; -import net.pterodactylus.sone.web.pages.UnbookmarkPage; -import net.pterodactylus.sone.web.pages.UnfollowSonePage; -import net.pterodactylus.sone.web.pages.UnlikePage; -import net.pterodactylus.sone.web.pages.UnlockSonePage; -import net.pterodactylus.sone.web.pages.UntrustPage; -import net.pterodactylus.sone.web.pages.UploadImagePage; -import net.pterodactylus.sone.web.pages.ViewPostPage; -import net.pterodactylus.sone.web.pages.ViewSonePage; +import net.pterodactylus.sone.web.pages.*; import net.pterodactylus.util.notify.Notification; import net.pterodactylus.util.notify.NotificationManager; -import net.pterodactylus.util.notify.TemplateNotification; import net.pterodactylus.util.template.Template; import net.pterodactylus.util.template.TemplateContextFactory; import net.pterodactylus.util.web.RedirectPage; @@ -171,10 +91,9 @@ import net.pterodactylus.util.web.TemplatePage; import freenet.clients.http.SessionManager; import freenet.clients.http.SessionManager.Session; import freenet.clients.http.ToadletContext; -import freenet.l10n.BaseL10n; +import com.codahale.metrics.*; import com.google.common.base.Optional; -import com.google.common.collect.Collections2; import com.google.common.collect.ImmutableSet; import com.google.common.eventbus.Subscribe; import com.google.inject.Inject; @@ -192,7 +111,7 @@ public class WebInterface implements SessionProvider { private final Loaders loaders; /** The notification manager. */ - private final NotificationManager notificationManager = new NotificationManager(); + private final NotificationManager notificationManager; /** The Sone plugin. */ private final SonePlugin sonePlugin; @@ -204,9 +123,6 @@ public class WebInterface implements SessionProvider { private final TemplateContextFactory templateContextFactory; private final TemplateRenderer templateRenderer; - /** The Sone text parser. */ - private final SoneTextParser soneTextParser; - /** The parser filter. */ private final ParserFilter parserFilter; private final ShortenFilter shortenFilter; @@ -222,9 +138,8 @@ public class WebInterface implements SessionProvider { private final L10nFilter l10nFilter; private final PageToadletRegistry pageToadletRegistry; - - /** The “new Sone” notification. */ - private final ListNotification newSoneNotification; + private final MetricRegistry metricRegistry; + private final Translation translation; /** The “new post” notification. */ private final ListNotification newPostNotification; @@ -238,33 +153,6 @@ public class WebInterface implements SessionProvider { /** The invisible “local reply” notification. */ private final ListNotification localReplyNotification; - /** The “you have been mentioned” notification. */ - private final ListNotification mentionNotification; - - /** Notifications for sone inserts. */ - private final Map soneInsertNotifications = new HashMap<>(); - - /** Sone locked notification ticker objects. */ - private final Map> lockedSonesTickerObjects = Collections.synchronizedMap(new HashMap>()); - - /** The “Sone locked” notification. */ - private final ListNotification lockedSonesNotification; - - /** The “new version” notification. */ - private final TemplateNotification newVersionNotification; - - /** The “inserting images” notification. */ - private final ListNotification insertingImagesNotification; - - /** The “inserted images” notification. */ - private final ListNotification insertedImagesNotification; - - /** The “image insert failed” notification. */ - private final ListNotification imageInsertFailedNotification; - - /** Scheduled executor for time-based notifications. */ - private final ScheduledExecutorService ticker = Executors.newScheduledThreadPool(1); - @Inject public WebInterface(SonePlugin sonePlugin, Loaders loaders, ListNotificationFilter listNotificationFilter, PostVisibilityFilter postVisibilityFilter, ReplyVisibilityFilter replyVisibilityFilter, @@ -273,7 +161,11 @@ public class WebInterface implements SessionProvider { ParserFilter parserFilter, ShortenFilter shortenFilter, RenderFilter renderFilter, LinkedElementRenderFilter linkedElementRenderFilter, - PageToadletRegistry pageToadletRegistry) { + PageToadletRegistry pageToadletRegistry, MetricRegistry metricRegistry, Translation translation, L10nFilter l10nFilter, + NotificationManager notificationManager, @Named("newRemotePost") ListNotification newPostNotification, + @Named("newRemotePostReply") ListNotification newReplyNotification, + @Named("localPost") ListNotification localPostNotification, + @Named("localReply") ListNotification localReplyNotification) { this.sonePlugin = sonePlugin; this.loaders = loaders; this.listNotificationFilter = listNotificationFilter; @@ -286,47 +178,19 @@ public class WebInterface implements SessionProvider { this.renderFilter = renderFilter; this.linkedElementRenderFilter = linkedElementRenderFilter; this.pageToadletRegistry = pageToadletRegistry; + this.metricRegistry = metricRegistry; + this.l10nFilter = l10nFilter; + this.translation = translation; + this.notificationManager = notificationManager; + this.newPostNotification = newPostNotification; + this.newReplyNotification = newReplyNotification; + this.localPostNotification = localPostNotification; + this.localReplyNotification = localReplyNotification; formPassword = sonePlugin.pluginRespirator().getToadletContainer().getFormPassword(); - soneTextParser = new SoneTextParser(getCore(), getCore()); - l10nFilter = new L10nFilter(getL10n()); this.templateContextFactory = templateContextFactory; templateContextFactory.addTemplateObject("webInterface", this); templateContextFactory.addTemplateObject("formPassword", formPassword); - - /* create notifications. */ - Template newSoneNotificationTemplate = loaders.loadTemplate("/templates/notify/newSoneNotification.html"); - newSoneNotification = new ListNotification<>("new-sone-notification", "sones", newSoneNotificationTemplate, false); - - Template newPostNotificationTemplate = loaders.loadTemplate("/templates/notify/newPostNotification.html"); - newPostNotification = new ListNotification<>("new-post-notification", "posts", newPostNotificationTemplate, false); - - Template localPostNotificationTemplate = loaders.loadTemplate("/templates/notify/newPostNotification.html"); - localPostNotification = new ListNotification<>("local-post-notification", "posts", localPostNotificationTemplate, false); - - Template newReplyNotificationTemplate = loaders.loadTemplate("/templates/notify/newReplyNotification.html"); - newReplyNotification = new ListNotification<>("new-reply-notification", "replies", newReplyNotificationTemplate, false); - - Template localReplyNotificationTemplate = loaders.loadTemplate("/templates/notify/newReplyNotification.html"); - localReplyNotification = new ListNotification<>("local-reply-notification", "replies", localReplyNotificationTemplate, false); - - 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); - - Template insertingImagesTemplate = loaders.loadTemplate("/templates/notify/inserting-images-notification.html"); - insertingImagesNotification = new ListNotification<>("inserting-images-notification", "images", insertingImagesTemplate); - - Template insertedImagesTemplate = loaders.loadTemplate("/templates/notify/inserted-images-notification.html"); - insertedImagesNotification = new ListNotification<>("inserted-images-notification", "images", insertedImagesTemplate); - - Template imageInsertFailedTemplate = loaders.loadTemplate("/templates/notify/image-insert-failed-notification.html"); - imageInsertFailedNotification = new ListNotification<>("image-insert-failed-notification", "images", imageInsertFailedTemplate); } // @@ -442,13 +306,8 @@ public class WebInterface implements SessionProvider { return listNotificationFilter.filterNotifications(notificationManager.getNotifications(), currentSone); } - /** - * Returns the l10n helper of the node. - * - * @return The node’s l10n helper - */ - public BaseL10n getL10n() { - return sonePlugin.l10n().getBase(); + public Translation getTranslation() { + return translation; } /** @@ -469,16 +328,6 @@ public class WebInterface implements SessionProvider { return formPassword; } - /** - * Returns the posts that have been announced as new in the - * {@link #newPostNotification}. - * - * @return The new posts - */ - public Set getNewPosts() { - return ImmutableSet. builder().addAll(newPostNotification.getElements()).addAll(localPostNotification.getElements()).build(); - } - @Nonnull public Collection getNewPosts(@Nullable Sone currentSone) { Set allNewPosts = ImmutableSet. builder() @@ -488,16 +337,6 @@ public class WebInterface implements SessionProvider { return from(allNewPosts).filter(postVisibilityFilter.isVisible(currentSone)).toSet(); } - /** - * Returns the replies that have been announced as new in the - * {@link #newReplyNotification}. - * - * @return The new replies - */ - public Set getNewReplies() { - return ImmutableSet. builder().addAll(newReplyNotification.getElements()).addAll(localReplyNotification.getElements()).build(); - } - @Nonnull public Collection getNewReplies(@Nullable Sone currentSone) { Set allNewReplies = ImmutableSet.builder() @@ -507,51 +346,6 @@ public class WebInterface implements SessionProvider { return from(allNewReplies).filter(replyVisibilityFilter.isVisible(currentSone)).toSet(); } - /** - * Sets whether the current start of the plugin is the first start. It is - * considered a first start if the configuration file does not exist. - * - * @param firstStart - * {@code true} if no configuration file existed when Sone was - * loaded, {@code false} otherwise - */ - public void setFirstStart(boolean firstStart) { - if (firstStart) { - Template firstStartNotificationTemplate = loaders.loadTemplate("/templates/notify/firstStartNotification.html"); - Notification firstStartNotification = new TemplateNotification("first-start-notification", firstStartNotificationTemplate); - notificationManager.addNotification(firstStartNotification); - } - } - - /** - * Sets whether Sone was started with a fresh configuration file. - * - * @param newConfig - * {@code true} if Sone was started with a fresh configuration, - * {@code false} if the existing configuration could be read - */ - public void setNewConfig(boolean newConfig) { - if (newConfig && !hasFirstStartNotification()) { - Template configNotReadNotificationTemplate = loaders.loadTemplate("/templates/notify/configNotReadNotification.html"); - Notification configNotReadNotification = new TemplateNotification("config-not-read-notification", configNotReadNotificationTemplate); - notificationManager.addNotification(configNotReadNotification); - } - } - - // - // PRIVATE ACCESSORS - // - - /** - * Returns whether the first start notification is currently displayed. - * - * @return {@code true} if the first-start notification is currently - * displayed, {@code false} otherwise - */ - private boolean hasFirstStartNotification() { - return notificationManager.getNotification("first-start-notification") != null; - } - // // ACTIONS // @@ -561,36 +355,6 @@ public class WebInterface implements SessionProvider { */ public void start() { registerToadlets(); - - /* notification templates. */ - Template startupNotificationTemplate = loaders.loadTemplate("/templates/notify/startupNotification.html"); - - final TemplateNotification startupNotification = new TemplateNotification("startup-notification", startupNotificationTemplate); - notificationManager.addNotification(startupNotification); - - ticker.schedule(new Runnable() { - - @Override - public void run() { - startupNotification.dismiss(); - } - }, 2, TimeUnit.MINUTES); - - Template wotMissingNotificationTemplate = loaders.loadTemplate("/templates/notify/wotMissingNotification.html"); - final TemplateNotification wotMissingNotification = new TemplateNotification("wot-missing-notification", wotMissingNotificationTemplate); - ticker.scheduleAtFixedRate(new Runnable() { - - @Override - @SuppressWarnings("synthetic-access") - public void run() { - if (getCore().getIdentityManager().isConnected()) { - wotMissingNotification.dismiss(); - } else { - notificationManager.addNotification(wotMissingNotification); - } - } - - }, 15, 15, TimeUnit.SECONDS); } /** @@ -598,7 +362,6 @@ public class WebInterface implements SessionProvider { */ public void stop() { pageToadletRegistry.unregisterToadlets(); - ticker.shutdownNow(); } // @@ -640,9 +403,6 @@ public class WebInterface implements SessionProvider { pageToadletRegistry.addPage(new UploadImagePage(this, loaders, templateRenderer)); pageToadletRegistry.addPage(new EditImagePage(this, loaders, templateRenderer)); pageToadletRegistry.addPage(new DeleteImagePage(this, loaders, templateRenderer)); - pageToadletRegistry.addPage(new TrustPage(this, loaders, templateRenderer)); - pageToadletRegistry.addPage(new DistrustPage(this, loaders, templateRenderer)); - pageToadletRegistry.addPage(new UntrustPage(this, loaders, templateRenderer)); pageToadletRegistry.addPage(new MarkAsKnownPage(this, loaders, templateRenderer)); pageToadletRegistry.addPage(new BookmarkPage(this, loaders, templateRenderer)); pageToadletRegistry.addPage(new UnbookmarkPage(this, loaders, templateRenderer)); @@ -659,6 +419,8 @@ public class WebInterface implements SessionProvider { pageToadletRegistry.addPage(new EmptyImageTitlePage(this, loaders, templateRenderer)); pageToadletRegistry.addPage(new EmptyAlbumTitlePage(this, loaders, templateRenderer)); pageToadletRegistry.addPage(new DismissNotificationPage(this, loaders, templateRenderer)); + pageToadletRegistry.addPage(new DebugPage(this, loaders, templateRenderer)); + pageToadletRegistry.addDebugPage(new MetricsPage(this, loaders, templateRenderer, metricRegistry)); pageToadletRegistry.addPage(loaders.loadStaticPage("css/", "/static/css/", "text/css")); pageToadletRegistry.addPage(loaders.loadStaticPage("javascript/", "/static/javascript/", "text/javascript")); pageToadletRegistry.addPage(loaders.loadStaticPage("images/", "/static/images/", "image/png")); @@ -683,9 +445,6 @@ public class WebInterface implements SessionProvider { pageToadletRegistry.addPage(new UnfollowSoneAjaxPage(this)); pageToadletRegistry.addPage(new EditAlbumAjaxPage(this)); pageToadletRegistry.addPage(new EditImageAjaxPage(this, parserFilter, shortenFilter, renderFilter)); - pageToadletRegistry.addPage(new TrustAjaxPage(this)); - pageToadletRegistry.addPage(new DistrustAjaxPage(this)); - pageToadletRegistry.addPage(new UntrustAjaxPage(this)); pageToadletRegistry.addPage(new LikeAjaxPage(this)); pageToadletRegistry.addPage(new UnlikeAjaxPage(this)); pageToadletRegistry.addPage(new GetLikesAjaxPage(this)); @@ -698,328 +457,9 @@ public class WebInterface implements SessionProvider { pageToadletRegistry.registerToadlets(); } - /** - * Returns all {@link Sone#isLocal() local Sone}s that are referenced by - * {@link SonePart}s in the given text (after parsing it using - * {@link SoneTextParser}). - * - * @param text - * The text to parse - * @return All mentioned local Sones - */ - private Collection getMentionedSones(String text) { - /* we need no context to find mentioned Sones. */ - Set mentionedSones = new HashSet<>(); - for (Part part : soneTextParser.parse(text, null)) { - if (part instanceof SonePart) { - mentionedSones.add(((SonePart) part).getSone()); - } - } - return Collections2.filter(mentionedSones, Sone.LOCAL_SONE_FILTER); - } - - /** - * Returns the Sone insert notification for the given Sone. If no - * notification for the given Sone exists, a new notification is created and - * cached. - * - * @param sone - * The Sone to get the insert notification for - * @return The Sone insert notification - */ - private TemplateNotification getSoneInsertNotification(Sone sone) { - synchronized (soneInsertNotifications) { - TemplateNotification templateNotification = soneInsertNotifications.get(sone); - if (templateNotification == null) { - templateNotification = new TemplateNotification(loaders.loadTemplate("/templates/notify/soneInsertNotification.html")); - templateNotification.set("insertSone", sone); - soneInsertNotifications.put(sone, templateNotification); - } - return templateNotification; - } - } - - private boolean localSoneMentionedInNewPostOrReply(Post post) { - if (!post.getSone().isLocal()) { - if (!getMentionedSones(post.getText()).isEmpty() && !post.isKnown()) { - return true; - } - } - for (PostReply postReply : getCore().getReplies(post.getId())) { - if (postReply.getSone().isLocal()) { - continue; - } - if (!getMentionedSones(postReply.getText()).isEmpty() && !postReply.isKnown()) { - return true; - } - } - return false; - } - - // - // EVENT HANDLERS - // - - /** - * Notifies the web interface that a new {@link Sone} was found. - * - * @param newSoneFoundEvent - * The event - */ - @Subscribe - public void newSoneFound(NewSoneFoundEvent newSoneFoundEvent) { - newSoneNotification.add(newSoneFoundEvent.sone()); - if (!hasFirstStartNotification()) { - notificationManager.addNotification(newSoneNotification); - } - } - - /** - * Notifies the web interface that a new {@link Post} was found. - * - * @param newPostFoundEvent - * The event - */ - @Subscribe - public void newPostFound(NewPostFoundEvent newPostFoundEvent) { - Post post = newPostFoundEvent.post(); - boolean isLocal = post.getSone().isLocal(); - if (isLocal) { - localPostNotification.add(post); - } else { - newPostNotification.add(post); - } - if (!hasFirstStartNotification()) { - notificationManager.addNotification(isLocal ? localPostNotification : newPostNotification); - if (!getMentionedSones(post.getText()).isEmpty() && !isLocal) { - mentionNotification.add(post); - notificationManager.addNotification(mentionNotification); - } - } else { - getCore().markPostKnown(post); - } - } - - /** - * Notifies the web interface that a new {@link PostReply} was found. - * - * @param newPostReplyFoundEvent - * The event - */ - @Subscribe - public void newReplyFound(NewPostReplyFoundEvent newPostReplyFoundEvent) { - PostReply reply = newPostReplyFoundEvent.postReply(); - boolean isLocal = reply.getSone().isLocal(); - if (isLocal) { - localReplyNotification.add(reply); - } else { - newReplyNotification.add(reply); - } - if (!hasFirstStartNotification()) { - notificationManager.addNotification(isLocal ? localReplyNotification : newReplyNotification); - if (reply.getPost().isPresent() && localSoneMentionedInNewPostOrReply(reply.getPost().get())) { - mentionNotification.add(reply.getPost().get()); - notificationManager.addNotification(mentionNotification); - } - } else { - getCore().markReplyKnown(reply); - } - } - - /** - * Notifies the web interface that a {@link Sone} was marked as known. - * - * @param markSoneKnownEvent - * The event - */ - @Subscribe - public void markSoneKnown(MarkSoneKnownEvent markSoneKnownEvent) { - newSoneNotification.remove(markSoneKnownEvent.sone()); - } - - @Subscribe - public void markPostKnown(MarkPostKnownEvent markPostKnownEvent) { - removePost(markPostKnownEvent.post()); - } - - @Subscribe - public void markReplyKnown(MarkPostReplyKnownEvent markPostReplyKnownEvent) { - removeReply(markPostReplyKnownEvent.postReply()); - } - - @Subscribe - public void soneRemoved(SoneRemovedEvent soneRemovedEvent) { - newSoneNotification.remove(soneRemovedEvent.sone()); - } - - @Subscribe - public void postRemoved(PostRemovedEvent postRemovedEvent) { - removePost(postRemovedEvent.post()); - } - - private void removePost(Post post) { - newPostNotification.remove(post); - localPostNotification.remove(post); - if (!localSoneMentionedInNewPostOrReply(post)) { - mentionNotification.remove(post); - } - } - - @Subscribe - public void replyRemoved(PostReplyRemovedEvent postReplyRemovedEvent) { - removeReply(postReplyRemovedEvent.postReply()); - } - - private void removeReply(PostReply reply) { - newReplyNotification.remove(reply); - localReplyNotification.remove(reply); - if (reply.getPost().isPresent() && !localSoneMentionedInNewPostOrReply(reply.getPost().get())) { - mentionNotification.remove(reply.getPost().get()); - } - } - - /** - * Notifies the web interface that a Sone was locked. - * - * @param soneLockedEvent - * The event - */ - @Subscribe - public void soneLocked(SoneLockedEvent soneLockedEvent) { - final Sone sone = soneLockedEvent.sone(); - 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.sone()); - lockedSonesTickerObjects.remove(soneUnlockedEvent.sone()).cancel(false); - } - - /** - * Notifies the web interface that a {@link Sone} is being inserted. - * - * @param soneInsertingEvent - * The event - */ - @Subscribe - public void soneInserting(SoneInsertingEvent soneInsertingEvent) { - TemplateNotification soneInsertNotification = getSoneInsertNotification(soneInsertingEvent.sone()); - soneInsertNotification.set("soneStatus", "inserting"); - if (soneInsertingEvent.sone().getOptions().isSoneInsertNotificationEnabled()) { - notificationManager.addNotification(soneInsertNotification); - } - } - - /** - * Notifies the web interface that a {@link Sone} was inserted. - * - * @param soneInsertedEvent - * The event - */ - @Subscribe - public void soneInserted(SoneInsertedEvent soneInsertedEvent) { - TemplateNotification soneInsertNotification = getSoneInsertNotification(soneInsertedEvent.sone()); - soneInsertNotification.set("soneStatus", "inserted"); - soneInsertNotification.set("insertDuration", soneInsertedEvent.insertDuration() / 1000); - if (soneInsertedEvent.sone().getOptions().isSoneInsertNotificationEnabled()) { - notificationManager.addNotification(soneInsertNotification); - } - } - - /** - * Notifies the web interface that a {@link Sone} insert was aborted. - * - * @param soneInsertAbortedEvent - * The event - */ - @Subscribe - public void soneInsertAborted(SoneInsertAbortedEvent soneInsertAbortedEvent) { - TemplateNotification soneInsertNotification = getSoneInsertNotification(soneInsertAbortedEvent.sone()); - soneInsertNotification.set("soneStatus", "insert-aborted"); - soneInsertNotification.set("insert-error", soneInsertAbortedEvent.cause()); - if (soneInsertAbortedEvent.sone().getOptions().isSoneInsertNotificationEnabled()) { - notificationManager.addNotification(soneInsertNotification); - } - } - - /** - * Notifies the web interface that a new Sone version was found. - * - * @param updateFoundEvent - * The event - */ - @Subscribe - public void updateFound(UpdateFoundEvent updateFoundEvent) { - newVersionNotification.set("latestVersion", updateFoundEvent.version()); - newVersionNotification.set("latestEdition", updateFoundEvent.latestEdition()); - newVersionNotification.set("releaseTime", updateFoundEvent.releaseTime()); - newVersionNotification.set("disruptive", updateFoundEvent.disruptive()); - notificationManager.addNotification(newVersionNotification); - } - - /** - * Notifies the web interface that an image insert was started - * - * @param imageInsertStartedEvent - * The event - */ - @Subscribe - public void imageInsertStarted(ImageInsertStartedEvent imageInsertStartedEvent) { - insertingImagesNotification.add(imageInsertStartedEvent.image()); - notificationManager.addNotification(insertingImagesNotification); - } - - /** - * Notifies the web interface that an {@link Image} insert was aborted. - * - * @param imageInsertAbortedEvent - * The event - */ - @Subscribe - public void imageInsertAborted(ImageInsertAbortedEvent imageInsertAbortedEvent) { - insertingImagesNotification.remove(imageInsertAbortedEvent.image()); - } - - /** - * Notifies the web interface that an {@link Image} insert is finished. - * - * @param imageInsertFinishedEvent - * The event - */ - @Subscribe - public void imageInsertFinished(ImageInsertFinishedEvent imageInsertFinishedEvent) { - insertingImagesNotification.remove(imageInsertFinishedEvent.image()); - insertedImagesNotification.add(imageInsertFinishedEvent.image()); - notificationManager.addNotification(insertedImagesNotification); - } - - /** - * Notifies the web interface that an {@link Image} insert has failed. - * - * @param imageInsertFailedEvent - * The event - */ @Subscribe - public void imageInsertFailed(ImageInsertFailedEvent imageInsertFailedEvent) { - insertingImagesNotification.remove(imageInsertFailedEvent.image()); - imageInsertFailedNotification.add(imageInsertFailedEvent.image()); - notificationManager.addNotification(imageInsertFailedNotification); + public void debugActivated(@Nonnull DebugActivatedEvent debugActivatedEvent) { + pageToadletRegistry.activateDebugMode(); } } diff --git a/src/main/java/net/pterodactylus/sone/web/page/PageToadlet.java b/src/main/java/net/pterodactylus/sone/web/page/PageToadlet.java index 2b22377..45d6fff 100644 --- a/src/main/java/net/pterodactylus/sone/web/page/PageToadlet.java +++ b/src/main/java/net/pterodactylus/sone/web/page/PageToadlet.java @@ -1,5 +1,5 @@ /* - * Sone - PageToadlet.java - Copyright © 2010–2019 David Roden + * Sone - PageToadlet.java - Copyright © 2010–2020 David 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 @@ -34,7 +34,6 @@ import freenet.clients.http.SessionManager; import freenet.clients.http.Toadlet; import freenet.clients.http.ToadletContext; import freenet.clients.http.ToadletContextClosedException; -import freenet.l10n.NodeL10n; import freenet.support.MultiValueTable; import freenet.support.api.HTTPRequest; @@ -107,7 +106,7 @@ public class PageToadlet extends Toadlet implements LinkEnabledCallback, LinkFil * if the toadlet context is closed */ public void handleMethodGET(URI uri, HTTPRequest httpRequest, ToadletContext toadletContext) throws IOException, ToadletContextClosedException { - handleRequest(new FreenetRequest(uri, Method.GET, httpRequest, toadletContext, NodeL10n.getBase(), sessionManager)); + handleRequest(new FreenetRequest(uri, Method.GET, httpRequest, toadletContext, sessionManager)); } /** @@ -125,7 +124,7 @@ public class PageToadlet extends Toadlet implements LinkEnabledCallback, LinkFil * if the toadlet context is closed */ public void handleMethodPOST(URI uri, HTTPRequest httpRequest, ToadletContext toadletContext) throws IOException, ToadletContextClosedException { - handleRequest(new FreenetRequest(uri, Method.POST, httpRequest, toadletContext, NodeL10n.getBase(), sessionManager)); + handleRequest(new FreenetRequest(uri, Method.POST, httpRequest, toadletContext, sessionManager)); } /** diff --git a/src/main/kotlin/net/pterodactylus/sone/core/DefaultElementLoader.kt b/src/main/kotlin/net/pterodactylus/sone/core/DefaultElementLoader.kt index d01aeb4..409d18c 100644 --- a/src/main/kotlin/net/pterodactylus/sone/core/DefaultElementLoader.kt +++ b/src/main/kotlin/net/pterodactylus/sone/core/DefaultElementLoader.kt @@ -23,9 +23,9 @@ class DefaultElementLoader(private val freenetInterface: FreenetInterface, ticke @Inject constructor(freenetInterface: FreenetInterface): this(freenetInterface, Ticker.systemTicker()) - private val loadingLinks: Cache = CacheBuilder.newBuilder().build() - private val failureCache: Cache = CacheBuilder.newBuilder().ticker(ticker).expireAfterWrite(30, MINUTES).build() - private val elementCache: Cache = CacheBuilder.newBuilder().build() + private val loadingLinks: Cache = CacheBuilder.newBuilder().build() + private val failureCache: Cache = CacheBuilder.newBuilder().ticker(ticker).expireAfterWrite(30, MINUTES).build() + private val elementCache: Cache = CacheBuilder.newBuilder().build() private val callback = object: FreenetInterface.BackgroundFetchCallback { override fun shouldCancel(uri: FreenetURI, mimeType: String, size: Long): Boolean { return (size > 2097152) || (!mimeType.startsWith("image/") && !mimeType.startsWith("text/html")) diff --git a/src/main/kotlin/net/pterodactylus/sone/core/Preferences.kt b/src/main/kotlin/net/pterodactylus/sone/core/Preferences.kt index 783552e..05b3279 100644 --- a/src/main/kotlin/net/pterodactylus/sone/core/Preferences.kt +++ b/src/main/kotlin/net/pterodactylus/sone/core/Preferences.kt @@ -1,5 +1,5 @@ /* - * Sone - Preferences.kt - Copyright © 2013–2019 David Roden + * Sone - Preferences.kt - Copyright © 2013–2020 David 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 @@ -77,24 +77,6 @@ class Preferences(private val eventBus: EventBus) { get() = unsupported set(value) = _requireFullAccess.set(value) - private val _positiveTrust = DefaultOption(75, range(0, 100)) - val positiveTrust: Int get() = _positiveTrust.get() - var newPositiveTrust: Int? - get() = unsupported - set(value) = _positiveTrust.set(value) - - private val _negativeTrust = DefaultOption(-25, range(-100, 100)) - val negativeTrust: Int get() = _negativeTrust.get() - var newNegativeTrust: Int? - get() = unsupported - set(value) = _negativeTrust.set(value) - - private val _trustComment = DefaultOption("Set from Sone Web Interface") - val trustComment: String get() = _trustComment.get() - var newTrustComment: String? - get() = unsupported - set(value) = _trustComment.set(value) - private val _fcpInterfaceActive = DefaultOption(false) val fcpInterfaceActive: Boolean get() = _fcpInterfaceActive.get() var newFcpInterfaceActive: Boolean? @@ -125,9 +107,6 @@ class Preferences(private val eventBus: EventBus) { configuration.getIntValue("Option/CharactersPerPost").value = _charactersPerPost.real configuration.getIntValue("Option/PostCutOffLength").value = _postCutOffLength.real configuration.getBooleanValue("Option/RequireFullAccess").value = _requireFullAccess.real - configuration.getIntValue("Option/PositiveTrust").value = _positiveTrust.real - configuration.getIntValue("Option/NegativeTrust").value = _negativeTrust.real - configuration.getStringValue("Option/TrustComment").value = _trustComment.real configuration.getBooleanValue("Option/ActivateFcpInterface").value = _fcpInterfaceActive.real configuration.getIntValue("Option/FcpFullAccessRequired").value = toInt(_fcpFullAccessRequired.real) } diff --git a/src/main/kotlin/net/pterodactylus/sone/core/UpdateChecker.kt b/src/main/kotlin/net/pterodactylus/sone/core/UpdateChecker.kt new file mode 100644 index 0000000..0e67d0a --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/core/UpdateChecker.kt @@ -0,0 +1,123 @@ +/* + * Sone - UpdateChecker.kt - Copyright © 2011–2020 David 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 . + */ + +package net.pterodactylus.sone.core + +import com.google.common.eventbus.* +import com.google.common.primitives.* +import com.google.inject.Inject +import freenet.keys.* +import net.pterodactylus.sone.core.event.* +import net.pterodactylus.sone.main.* +import net.pterodactylus.sone.utils.* +import net.pterodactylus.util.version.Version +import java.io.* +import java.util.* +import java.util.logging.* +import java.util.logging.Logger.* +import javax.inject.Singleton + +/** + * Watches the official Sone homepage for new releases. + */ +@Singleton +class UpdateChecker @Inject constructor( + private val eventBus: EventBus, + private val freenetInterface: FreenetInterface, + private val currentRunningVersion: Version, + pluginHomepage: PluginHomepage) { + + private val logger: Logger = getLogger(UpdateChecker::class.java.name) + + private val currentUri by lazy { FreenetURI(pluginHomepage.homepage) } + + var latestEdition = SonePlugin.getLatestEdition() + private set + + var latestVersion: Version = currentRunningVersion + private set + + var latestVersionDate: Long = 0 + private set + + fun hasLatestVersion() = + latestVersion > currentRunningVersion + + fun start() { + freenetInterface.registerUsk(currentUri) { uri, edition, newKnownGood, newSlot -> + logger.log(Level.FINEST, String.format("Found update for %s: %d, %s, %s", uri, edition, newKnownGood, newSlot)) + if (newKnownGood || newSlot) { + try { + freenetInterface.fetchUri(uri.setMetaString(arrayOf("sone.properties"))) + ?.onNull { + logger.log(Level.WARNING, String.format("Could not fetch properties of latest homepage: %s", uri)) + }?.fetchResult + ?.asBucket()?.use { resultBucket -> + resultBucket.inputStream + .let { parseProperties(it) } + .let { extractCurrentVersion(it) } + .onNull { logger.log(Level.INFO, "Invalid data parsed from properties.") } + ?.takeIf { it.version > latestVersion } + ?.also { updateVersionInformation(it, edition) } + ?.also { logger.info { "Found new version: %s (%tc%s)".format(it.version, it.time, if (it.disruptive) ", disruptive" else "") } } + ?.also { eventBus.post(UpdateFoundEvent(it.version, it.time, edition, it.disruptive)) } + } + } catch (ioe1: IOException) { + logger.log(Level.WARNING, String.format("Could not parse sone.properties of %s!", uri), ioe1) + } + } + } + } + + fun stop() { + freenetInterface.unregisterUsk(currentUri) + } + + private fun updateVersionInformation(versionInformation: VersionInformation, edition: Long) { + latestVersion = versionInformation.version + latestVersionDate = versionInformation.time + latestEdition = edition + } + + private fun parseProperties(propertiesInputStream: InputStream) = + Properties().apply { + InputStreamReader(propertiesInputStream, "UTF-8").use { inputStreamReader -> + load(inputStreamReader) + } + } + + private fun extractCurrentVersion(properties: Properties) = + properties.getProperty("CurrentVersion/Version") + ?.let { Version.parse(it) } + ?.let { version -> + properties.getProperty("CurrentVersion/ReleaseTime") + ?.let { Longs.tryParse(it) } + ?.let { time -> + VersionInformation(version, time, disruptiveVersionBetweenCurrentAndFound(properties)) + } + } + + private fun disruptiveVersionBetweenCurrentAndFound(properties: Properties) = + properties.stringPropertyNames() + .filter { it.startsWith("DisruptiveVersion/") } + .map { it.removePrefix("DisruptiveVersion/") } + .map { Version.parse(it) } + .any { it > currentRunningVersion } + +} + +private data class VersionInformation(val version: Version, val time: Long, val disruptive: Boolean) diff --git a/src/main/kotlin/net/pterodactylus/sone/core/event/ConfigNotRead.kt b/src/main/kotlin/net/pterodactylus/sone/core/event/ConfigNotRead.kt new file mode 100644 index 0000000..97f2843 --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/core/event/ConfigNotRead.kt @@ -0,0 +1,26 @@ +/** + * Sone - ConfigNotRead.kt - Copyright © 2019–2020 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 . + */ + +package net.pterodactylus.sone.core.event + +/** + * Event that signals that Sone could not read an existing configuration + * successfully, and a new configuration was created. This is different from + * [FirstStart] in that `FirstStart` signals that there *was* no existing + * configuration to be read. + */ +class ConfigNotRead diff --git a/src/main/kotlin/net/pterodactylus/sone/core/event/DebugActivatedEvent.kt b/src/main/kotlin/net/pterodactylus/sone/core/event/DebugActivatedEvent.kt new file mode 100644 index 0000000..037cdb5 --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/core/event/DebugActivatedEvent.kt @@ -0,0 +1,20 @@ +/** + * Sone - DebugActivatedEvent.kt - Copyright © 2019–2020 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 . + */ + +package net.pterodactylus.sone.core.event + +class DebugActivatedEvent diff --git a/src/main/kotlin/net/pterodactylus/sone/core/event/FirstStart.kt b/src/main/kotlin/net/pterodactylus/sone/core/event/FirstStart.kt new file mode 100644 index 0000000..c58db50 --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/core/event/FirstStart.kt @@ -0,0 +1,24 @@ +/** + * Sone - FirstStart.kt - Copyright © 2019–2020 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 . + */ + +package net.pterodactylus.sone.core.event + +/** + * Event that signals that Sone was started for the first time. This event + * will only be triggered once, on startup. + */ +class FirstStart diff --git a/src/main/kotlin/net/pterodactylus/sone/core/event/ImageEvent.kt b/src/main/kotlin/net/pterodactylus/sone/core/event/ImageEvent.kt new file mode 100644 index 0000000..12d9ca1 --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/core/event/ImageEvent.kt @@ -0,0 +1,25 @@ +/* + * Sone - ImageEvent.kt - Copyright © 2013–2020 David 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 . + */ + +package net.pterodactylus.sone.core.event + +import net.pterodactylus.sone.data.* + +/** + * Base class for [Image] events. + */ +abstract class ImageEvent(val image: Image) diff --git a/src/main/kotlin/net/pterodactylus/sone/core/event/ImageInsertAbortedEvent.kt b/src/main/kotlin/net/pterodactylus/sone/core/event/ImageInsertAbortedEvent.kt new file mode 100644 index 0000000..37a221f --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/core/event/ImageInsertAbortedEvent.kt @@ -0,0 +1,25 @@ +/* + * Sone - ImageInsertAbortedEvent.kt - Copyright © 2013–2020 David 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 . + */ + +package net.pterodactylus.sone.core.event + +import net.pterodactylus.sone.data.* + +/** + * Event that signals that an [Image] insert is aborted. + */ +class ImageInsertAbortedEvent(image: Image) : ImageEvent(image) diff --git a/src/main/kotlin/net/pterodactylus/sone/core/event/ImageInsertFailedEvent.kt b/src/main/kotlin/net/pterodactylus/sone/core/event/ImageInsertFailedEvent.kt new file mode 100644 index 0000000..e19b3d8 --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/core/event/ImageInsertFailedEvent.kt @@ -0,0 +1,25 @@ +/* + * Sone - ImageInsertFailedEvent.kt - Copyright © 2013–2020 David 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 . + */ + +package net.pterodactylus.sone.core.event + +import net.pterodactylus.sone.data.* + +/** + * Event that signals that an [Image] insert has failed. + */ +class ImageInsertFailedEvent(image: Image, val cause: Throwable) : ImageEvent(image) diff --git a/src/main/kotlin/net/pterodactylus/sone/core/event/ImageInsertFinishedEvent.kt b/src/main/kotlin/net/pterodactylus/sone/core/event/ImageInsertFinishedEvent.kt new file mode 100644 index 0000000..46daec6 --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/core/event/ImageInsertFinishedEvent.kt @@ -0,0 +1,26 @@ +/* + * Sone - ImageInsertFinishedEvent.kt - Copyright © 2013–2020 David 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 . + */ + +package net.pterodactylus.sone.core.event + +import freenet.keys.* +import net.pterodactylus.sone.data.* + +/** + * Event that signals that an [Image] insert is finished. + */ +class ImageInsertFinishedEvent(image: Image, val resultingUri: FreenetURI) : ImageEvent(image) diff --git a/src/main/kotlin/net/pterodactylus/sone/core/event/ImageInsertStartedEvent.kt b/src/main/kotlin/net/pterodactylus/sone/core/event/ImageInsertStartedEvent.kt new file mode 100644 index 0000000..57478fa --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/core/event/ImageInsertStartedEvent.kt @@ -0,0 +1,25 @@ +/* + * Sone - ImageInsertStartedEvent.kt - Copyright © 2013–2020 David 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 . + */ + +package net.pterodactylus.sone.core.event + +import net.pterodactylus.sone.data.* + +/** + * Event that signals that an [Image] is not being inserted. + */ +class ImageInsertStartedEvent(image: Image) : ImageEvent(image) diff --git a/src/main/kotlin/net/pterodactylus/sone/core/event/MarkPostKnownEvent.kt b/src/main/kotlin/net/pterodactylus/sone/core/event/MarkPostKnownEvent.kt new file mode 100644 index 0000000..e479202 --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/core/event/MarkPostKnownEvent.kt @@ -0,0 +1,26 @@ +/* + * Sone - MarkPostKnownEvent.kt - Copyright © 2013–2020 David 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 . + */ + +package net.pterodactylus.sone.core.event + +import net.pterodactylus.sone.data.* + +/** + * Event that signals that a [Post] has been marked as + * [known][Post.isKnown]. + */ +class MarkPostKnownEvent(post: Post) : PostEvent(post) diff --git a/src/main/kotlin/net/pterodactylus/sone/core/event/MarkPostReplyKnownEvent.kt b/src/main/kotlin/net/pterodactylus/sone/core/event/MarkPostReplyKnownEvent.kt new file mode 100644 index 0000000..af9dce4 --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/core/event/MarkPostReplyKnownEvent.kt @@ -0,0 +1,26 @@ +/* + * Sone - MarkPostReplyKnownEvent.kt - Copyright © 2013–2020 David 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 . + */ + +package net.pterodactylus.sone.core.event + +import net.pterodactylus.sone.data.* + +/** + * Event that signals that a [PostReply] has been marked as + * [known][PostReply.isKnown]. + */ +class MarkPostReplyKnownEvent(postReply: PostReply) : PostReplyEvent(postReply) diff --git a/src/main/kotlin/net/pterodactylus/sone/core/event/MarkSoneKnownEvent.kt b/src/main/kotlin/net/pterodactylus/sone/core/event/MarkSoneKnownEvent.kt new file mode 100644 index 0000000..b3b8fbc --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/core/event/MarkSoneKnownEvent.kt @@ -0,0 +1,26 @@ +/* + * Sone - MarkSoneKnownEvent.kt - Copyright © 2013–2020 David 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 . + */ + +package net.pterodactylus.sone.core.event + +import net.pterodactylus.sone.data.* + +/** + * Event that signals that a [Sone] has been marked as + * [known][Sone.isKnown]. + */ +class MarkSoneKnownEvent(sone: Sone) : SoneEvent(sone) diff --git a/src/main/kotlin/net/pterodactylus/sone/core/event/MentionOfLocalSoneFoundEvent.kt b/src/main/kotlin/net/pterodactylus/sone/core/event/MentionOfLocalSoneFoundEvent.kt new file mode 100644 index 0000000..8b9b542 --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/core/event/MentionOfLocalSoneFoundEvent.kt @@ -0,0 +1,27 @@ +/** + * Sone - MentionOfLocalSoneFoundEvent.kt - Copyright © 2019–2020 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 . + */ + +package net.pterodactylus.sone.core.event + +import net.pterodactylus.sone.data.* + +/** + * Event that signals that a new post or reply was found that mentioned a local + * Sone, which happens if the [SoneTextParser] locates a [SonePart] in a post + * or reply. + */ +data class MentionOfLocalSoneFoundEvent(val post: Post) diff --git a/src/main/kotlin/net/pterodactylus/sone/core/event/MentionOfLocalSoneRemovedEvent.kt b/src/main/kotlin/net/pterodactylus/sone/core/event/MentionOfLocalSoneRemovedEvent.kt new file mode 100644 index 0000000..2413a80 --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/core/event/MentionOfLocalSoneRemovedEvent.kt @@ -0,0 +1,27 @@ +/** + * Sone - MentionOfLocalSoneRemovedEvent.kt - Copyright © 2019–2020 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 . + */ + +package net.pterodactylus.sone.core.event + +import net.pterodactylus.sone.data.* + +/** + * Event that signals that a post or reply that mentioned a local Sone was + * removed so that the given post and its replies do not contain a mention of + * a local Sone anymore. + */ +data class MentionOfLocalSoneRemovedEvent(val post: Post) diff --git a/src/main/kotlin/net/pterodactylus/sone/core/event/NewPostFoundEvent.kt b/src/main/kotlin/net/pterodactylus/sone/core/event/NewPostFoundEvent.kt index ba7f957..876f79c 100644 --- a/src/main/kotlin/net/pterodactylus/sone/core/event/NewPostFoundEvent.kt +++ b/src/main/kotlin/net/pterodactylus/sone/core/event/NewPostFoundEvent.kt @@ -1,5 +1,5 @@ /* - * Sone - NewPostFoundEvent.kt - Copyright © 2013–2019 David Roden + * Sone - NewPostFoundEvent.kt - Copyright © 2013–2020 David 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 @@ -22,9 +22,4 @@ import net.pterodactylus.sone.data.Post /** * Event that signals that a new post was found. */ -data class NewPostFoundEvent(val post: Post) { - - @Deprecated(message = "will go away", replaceWith = ReplaceWith("post")) - fun post() = post - -} +data class NewPostFoundEvent(val post: Post) diff --git a/src/main/kotlin/net/pterodactylus/sone/core/event/NewPostReplyFoundEvent.kt b/src/main/kotlin/net/pterodactylus/sone/core/event/NewPostReplyFoundEvent.kt index a70d1b9..ec339d5 100644 --- a/src/main/kotlin/net/pterodactylus/sone/core/event/NewPostReplyFoundEvent.kt +++ b/src/main/kotlin/net/pterodactylus/sone/core/event/NewPostReplyFoundEvent.kt @@ -1,5 +1,5 @@ /* - * Sone - NewPostReplyFoundEvent.kt - Copyright © 2013–2019 David Roden + * Sone - NewPostReplyFoundEvent.kt - Copyright © 2013–2020 David 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 @@ -22,9 +22,4 @@ import net.pterodactylus.sone.data.PostReply /** * Event that signals that a new [PostReply] was found. */ -data class NewPostReplyFoundEvent(val postReply: PostReply) { - - @Deprecated(message = "will go away", replaceWith = ReplaceWith("postReply")) - fun postReply() = postReply - -} +data class NewPostReplyFoundEvent(val postReply: PostReply) diff --git a/src/main/kotlin/net/pterodactylus/sone/core/event/NewSoneFoundEvent.kt b/src/main/kotlin/net/pterodactylus/sone/core/event/NewSoneFoundEvent.kt new file mode 100644 index 0000000..bb7c933 --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/core/event/NewSoneFoundEvent.kt @@ -0,0 +1,25 @@ +/* + * Sone - NewSoneFoundEvent.kt - Copyright © 2013–2020 David 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 . + */ + +package net.pterodactylus.sone.core.event + +import net.pterodactylus.sone.data.* + +/** + * Event that signals that a new remote Sone was found. + */ +class NewSoneFoundEvent(sone: Sone) : SoneEvent(sone) diff --git a/src/main/kotlin/net/pterodactylus/sone/core/event/PostEvent.kt b/src/main/kotlin/net/pterodactylus/sone/core/event/PostEvent.kt new file mode 100644 index 0000000..becafdd --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/core/event/PostEvent.kt @@ -0,0 +1,25 @@ +/* + * Sone - PostEvent.kt - Copyright © 2013–2020 David 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 . + */ + +package net.pterodactylus.sone.core.event + +import net.pterodactylus.sone.data.* + +/** + * Base class for post events. + */ +abstract class PostEvent(val post: Post) diff --git a/src/main/kotlin/net/pterodactylus/sone/core/event/PostRemovedEvent.kt b/src/main/kotlin/net/pterodactylus/sone/core/event/PostRemovedEvent.kt index 117d800..f68214b 100644 --- a/src/main/kotlin/net/pterodactylus/sone/core/event/PostRemovedEvent.kt +++ b/src/main/kotlin/net/pterodactylus/sone/core/event/PostRemovedEvent.kt @@ -1,5 +1,5 @@ /* - * Sone - PostRemovedEvent.kt - Copyright © 2013–2019 David Roden + * Sone - PostRemovedEvent.kt - Copyright © 2013–2020 David 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 @@ -22,9 +22,4 @@ import net.pterodactylus.sone.data.Post /** * Event that signals that a [Post] was removed. */ -data class PostRemovedEvent(val post: Post) { - - @Deprecated(message = "will go away", replaceWith = ReplaceWith("post")) - fun post() = post - -} +data class PostRemovedEvent(val post: Post) diff --git a/src/main/kotlin/net/pterodactylus/sone/core/event/PostReplyEvent.kt b/src/main/kotlin/net/pterodactylus/sone/core/event/PostReplyEvent.kt new file mode 100644 index 0000000..72e0daa --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/core/event/PostReplyEvent.kt @@ -0,0 +1,25 @@ +/* + * Sone - PostReplyEvent.kt - Copyright © 2013–2020 David 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 . + */ + +package net.pterodactylus.sone.core.event + +import net.pterodactylus.sone.data.* + +/** + * Base class for [PostReply] events. + */ +open class PostReplyEvent(val postReply: PostReply) diff --git a/src/main/kotlin/net/pterodactylus/sone/core/event/PostReplyRemovedEvent.kt b/src/main/kotlin/net/pterodactylus/sone/core/event/PostReplyRemovedEvent.kt index 6aa495b..6c71b8e 100644 --- a/src/main/kotlin/net/pterodactylus/sone/core/event/PostReplyRemovedEvent.kt +++ b/src/main/kotlin/net/pterodactylus/sone/core/event/PostReplyRemovedEvent.kt @@ -1,5 +1,5 @@ /* - * Sone - PostReplyRemovedEvent.kt - Copyright © 2013–2019 David Roden + * Sone - PostReplyRemovedEvent.kt - Copyright © 2013–2020 David 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 diff --git a/src/main/kotlin/net/pterodactylus/sone/core/event/Shutdown.kt b/src/main/kotlin/net/pterodactylus/sone/core/event/Shutdown.kt new file mode 100644 index 0000000..975dd96 --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/core/event/Shutdown.kt @@ -0,0 +1,23 @@ +/** + * Sone - Shutdown.kt - Copyright © 2019–2020 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 . + */ + +package net.pterodactylus.sone.core.event + +/** + * Event that signals the shutdown of Sone. + */ +class Shutdown diff --git a/src/main/kotlin/net/pterodactylus/sone/core/event/SoneEvent.kt b/src/main/kotlin/net/pterodactylus/sone/core/event/SoneEvent.kt new file mode 100644 index 0000000..d84bab5 --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/core/event/SoneEvent.kt @@ -0,0 +1,25 @@ +/* + * Sone - SoneEvent.kt - Copyright © 2013–2020 David 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 . + */ + +package net.pterodactylus.sone.core.event + +import net.pterodactylus.sone.data.* + +/** + * Base class for Sone events. + */ +abstract class SoneEvent(val sone: Sone) diff --git a/src/main/kotlin/net/pterodactylus/sone/core/event/SoneInsertAbortedEvent.kt b/src/main/kotlin/net/pterodactylus/sone/core/event/SoneInsertAbortedEvent.kt new file mode 100644 index 0000000..73ea564 --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/core/event/SoneInsertAbortedEvent.kt @@ -0,0 +1,25 @@ +/* + * Sone - SoneInsertAbortedEvent.kt - Copyright © 2013–2020 David 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 . + */ + +package net.pterodactylus.sone.core.event + +import net.pterodactylus.sone.data.* + +/** + * Event that signals that a [Sone] insert was aborted. + */ +class SoneInsertAbortedEvent(sone: Sone, val cause: Throwable) : SoneEvent(sone) diff --git a/src/main/kotlin/net/pterodactylus/sone/core/event/SoneInsertedEvent.kt b/src/main/kotlin/net/pterodactylus/sone/core/event/SoneInsertedEvent.kt new file mode 100644 index 0000000..f22dabe --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/core/event/SoneInsertedEvent.kt @@ -0,0 +1,25 @@ +/* + * Sone - SoneInsertedEvent.kt - Copyright © 2013–2020 David 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 . + */ + +package net.pterodactylus.sone.core.event + +import net.pterodactylus.sone.data.* + +/** + * Event that signals that a [Sone] was inserted. + */ +class SoneInsertedEvent(sone: Sone, val insertDuration: Long, val insertFingerprint: String) : SoneEvent(sone) diff --git a/src/main/kotlin/net/pterodactylus/sone/core/event/SoneInsertingEvent.kt b/src/main/kotlin/net/pterodactylus/sone/core/event/SoneInsertingEvent.kt new file mode 100644 index 0000000..f928556 --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/core/event/SoneInsertingEvent.kt @@ -0,0 +1,25 @@ +/* + * Sone - SoneInsertingEvent.kt - Copyright © 2013–2020 David 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 . + */ + +package net.pterodactylus.sone.core.event + +import net.pterodactylus.sone.data.* + +/** + * Event that signals that a [Sone] is now being inserted. + */ +class SoneInsertingEvent(sone: Sone) : SoneEvent(sone) diff --git a/src/main/kotlin/net/pterodactylus/sone/core/event/SoneLockedEvent.kt b/src/main/kotlin/net/pterodactylus/sone/core/event/SoneLockedEvent.kt new file mode 100644 index 0000000..1d77318 --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/core/event/SoneLockedEvent.kt @@ -0,0 +1,26 @@ +/* + * Sone - SoneLockedEvent.kt - Copyright © 2013–2020 David 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 . + */ + +package net.pterodactylus.sone.core.event + +import net.pterodactylus.sone.data.* + +/** + * Event that signals that a [Sone] was locked. Only + * [local Sones][Sone.isLocal] can be locked. + */ +class SoneLockedEvent(sone: Sone) : SoneEvent(sone) diff --git a/src/main/kotlin/net/pterodactylus/sone/core/event/SoneLockedOnStartup.kt b/src/main/kotlin/net/pterodactylus/sone/core/event/SoneLockedOnStartup.kt new file mode 100644 index 0000000..ac11c4d --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/core/event/SoneLockedOnStartup.kt @@ -0,0 +1,25 @@ +/** + * Sone - SoneLockedOnStartup.kt - Copyright © 2019–2020 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 . + */ + +package net.pterodactylus.sone.core.event + +import net.pterodactylus.sone.data.* + +/** + * Signals that a Sone was locked on startup because it’s empty. + */ +class SoneLockedOnStartup(sone: Sone) : SoneEvent(sone) diff --git a/src/main/kotlin/net/pterodactylus/sone/core/event/SoneRemovedEvent.kt b/src/main/kotlin/net/pterodactylus/sone/core/event/SoneRemovedEvent.kt new file mode 100644 index 0000000..ea08dae --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/core/event/SoneRemovedEvent.kt @@ -0,0 +1,25 @@ +/* + * Sone - SoneRemovedEvent.kt - Copyright © 2013–2020 David 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 . + */ + +package net.pterodactylus.sone.core.event + +import net.pterodactylus.sone.data.* + +/** + * Event that signals that a [Sone] was removed. + */ +class SoneRemovedEvent(sone: Sone) : SoneEvent(sone) diff --git a/src/main/kotlin/net/pterodactylus/sone/core/event/SoneUnlockedEvent.kt b/src/main/kotlin/net/pterodactylus/sone/core/event/SoneUnlockedEvent.kt new file mode 100644 index 0000000..3cc4203 --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/core/event/SoneUnlockedEvent.kt @@ -0,0 +1,26 @@ +/* + * Sone - SoneUnlockedEvent.kt - Copyright © 2013–2020 David 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 . + */ + +package net.pterodactylus.sone.core.event + +import net.pterodactylus.sone.data.* + +/** + * Event that signals that a [Sone] was unlocked. Only + * [local Sones][Sone.isLocal] can be locked. + */ +class SoneUnlockedEvent(sone: Sone) : SoneEvent(sone) diff --git a/src/main/kotlin/net/pterodactylus/sone/core/event/Startup.kt b/src/main/kotlin/net/pterodactylus/sone/core/event/Startup.kt new file mode 100644 index 0000000..79bd7a9 --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/core/event/Startup.kt @@ -0,0 +1,23 @@ +/** + * Sone - Startup.kt - Copyright © 2019–2020 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 . + */ + +package net.pterodactylus.sone.core.event + +/** + * Event that signals the startup of Sone. + */ +class Startup diff --git a/src/main/kotlin/net/pterodactylus/sone/core/event/UpdateFoundEvent.kt b/src/main/kotlin/net/pterodactylus/sone/core/event/UpdateFoundEvent.kt new file mode 100644 index 0000000..82a4cf5 --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/core/event/UpdateFoundEvent.kt @@ -0,0 +1,25 @@ +/* + * Sone - UpdateFoundEvent.kt - Copyright © 2013–2020 David 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 . + */ + +package net.pterodactylus.sone.core.event + +import net.pterodactylus.util.version.* + +/** + * Event that signals that an update for Sone was found. + */ +data class UpdateFoundEvent(val version: Version, val releaseTime: Long, val latestEdition: Long, val isDisruptive: Boolean) diff --git a/src/main/kotlin/net/pterodactylus/sone/core/event/WebOfTrustAppeared.kt b/src/main/kotlin/net/pterodactylus/sone/core/event/WebOfTrustAppeared.kt new file mode 100644 index 0000000..46fc327 --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/core/event/WebOfTrustAppeared.kt @@ -0,0 +1,23 @@ +/** + * Sone - WebOfTrustAppeared.kt - Copyright © 2019–2020 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 . + */ + +package net.pterodactylus.sone.core.event + +/** + * Event that signals that the web of trust is reachable. + */ +class WebOfTrustAppeared diff --git a/src/main/kotlin/net/pterodactylus/sone/core/event/WebOfTrustDisappeared.kt b/src/main/kotlin/net/pterodactylus/sone/core/event/WebOfTrustDisappeared.kt new file mode 100644 index 0000000..8382895 --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/core/event/WebOfTrustDisappeared.kt @@ -0,0 +1,23 @@ +/** + * Sone - WebOfTrustDisappeared.kt - Copyright © 2019–2020 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 . + */ + +package net.pterodactylus.sone.core.event + +/** + * Event that signals that the web of trust is not reachable. + */ +class WebOfTrustDisappeared diff --git a/src/main/kotlin/net/pterodactylus/sone/data/Albums.kt b/src/main/kotlin/net/pterodactylus/sone/data/Albums.kt new file mode 100644 index 0000000..0c79a84 --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/data/Albums.kt @@ -0,0 +1,23 @@ +/** + * Sone - Albums.kt - Copyright © 2019–2020 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 . + */ + +package net.pterodactylus.sone.data + +/** Returns all images contained in this album and all its albums. */ +val Album.allImages: Collection + get() = + images + albums.flatMap { it.allImages } diff --git a/src/main/kotlin/net/pterodactylus/sone/data/Fingerprintable.kt b/src/main/kotlin/net/pterodactylus/sone/data/Fingerprintable.kt index 97a54f8..7bba095 100644 --- a/src/main/kotlin/net/pterodactylus/sone/data/Fingerprintable.kt +++ b/src/main/kotlin/net/pterodactylus/sone/data/Fingerprintable.kt @@ -1,5 +1,5 @@ /* - * Sone - Fingerprintable.kt - Copyright © 2011–2019 David Roden + * Sone - Fingerprintable.kt - Copyright © 2011–2020 David 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 diff --git a/src/main/kotlin/net/pterodactylus/sone/data/Identified.kt b/src/main/kotlin/net/pterodactylus/sone/data/Identified.kt index 1cbad81..28e456f 100644 --- a/src/main/kotlin/net/pterodactylus/sone/data/Identified.kt +++ b/src/main/kotlin/net/pterodactylus/sone/data/Identified.kt @@ -1,5 +1,5 @@ /* - * Sone - Identified.kt - Copyright © 2013–2019 David Roden + * Sone - Identified.kt - Copyright © 2013–2020 David 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 diff --git a/src/main/kotlin/net/pterodactylus/sone/database/AlbumBuilder.kt b/src/main/kotlin/net/pterodactylus/sone/database/AlbumBuilder.kt index acace46..dd42f07 100644 --- a/src/main/kotlin/net/pterodactylus/sone/database/AlbumBuilder.kt +++ b/src/main/kotlin/net/pterodactylus/sone/database/AlbumBuilder.kt @@ -1,5 +1,5 @@ /* - * Sone - AlbumBuilder.kt - Copyright © 2013–2019 David Roden + * Sone - AlbumBuilder.kt - Copyright © 2013–2020 David 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 diff --git a/src/main/kotlin/net/pterodactylus/sone/database/AlbumBuilderFactory.kt b/src/main/kotlin/net/pterodactylus/sone/database/AlbumBuilderFactory.kt index f153f33..e27528f 100644 --- a/src/main/kotlin/net/pterodactylus/sone/database/AlbumBuilderFactory.kt +++ b/src/main/kotlin/net/pterodactylus/sone/database/AlbumBuilderFactory.kt @@ -1,5 +1,5 @@ /* - * Sone - AlbumBuilderFactory.kt - Copyright © 2013–2019 David Roden + * Sone - AlbumBuilderFactory.kt - Copyright © 2013–2020 David 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 diff --git a/src/main/kotlin/net/pterodactylus/sone/database/AlbumDatabase.kt b/src/main/kotlin/net/pterodactylus/sone/database/AlbumDatabase.kt index e61d429..3ccaef4 100644 --- a/src/main/kotlin/net/pterodactylus/sone/database/AlbumDatabase.kt +++ b/src/main/kotlin/net/pterodactylus/sone/database/AlbumDatabase.kt @@ -1,5 +1,5 @@ /* - * Sone - AlbumDatabase.kt - Copyright © 2013–2019 David Roden + * Sone - AlbumDatabase.kt - Copyright © 2013–2020 David 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 diff --git a/src/main/kotlin/net/pterodactylus/sone/database/AlbumProvider.kt b/src/main/kotlin/net/pterodactylus/sone/database/AlbumProvider.kt index db0146d..b1fcf62 100644 --- a/src/main/kotlin/net/pterodactylus/sone/database/AlbumProvider.kt +++ b/src/main/kotlin/net/pterodactylus/sone/database/AlbumProvider.kt @@ -1,5 +1,5 @@ /* - * Sone - AlbumProvider.kt - Copyright © 2013–2019 David Roden + * Sone - AlbumProvider.kt - Copyright © 2013–2020 David 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 diff --git a/src/main/kotlin/net/pterodactylus/sone/database/AlbumStore.kt b/src/main/kotlin/net/pterodactylus/sone/database/AlbumStore.kt index ddc7bb2..3d93ff1 100644 --- a/src/main/kotlin/net/pterodactylus/sone/database/AlbumStore.kt +++ b/src/main/kotlin/net/pterodactylus/sone/database/AlbumStore.kt @@ -1,5 +1,5 @@ /* - * Sone - AlbumStore.kt - Copyright © 2013–2019 David Roden + * Sone - AlbumStore.kt - Copyright © 2013–2020 David 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 diff --git a/src/main/kotlin/net/pterodactylus/sone/database/Database.kt b/src/main/kotlin/net/pterodactylus/sone/database/Database.kt index 17337e2..28417e8 100644 --- a/src/main/kotlin/net/pterodactylus/sone/database/Database.kt +++ b/src/main/kotlin/net/pterodactylus/sone/database/Database.kt @@ -1,5 +1,5 @@ /* - * Sone - Database.kt - Copyright © 2013–2019 David Roden + * Sone - Database.kt - Copyright © 2013–2020 David 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 diff --git a/src/main/kotlin/net/pterodactylus/sone/database/ImageBuilder.kt b/src/main/kotlin/net/pterodactylus/sone/database/ImageBuilder.kt index c9ddbaa..d104602 100644 --- a/src/main/kotlin/net/pterodactylus/sone/database/ImageBuilder.kt +++ b/src/main/kotlin/net/pterodactylus/sone/database/ImageBuilder.kt @@ -1,5 +1,5 @@ /* - * Sone - ImageBuilder.kt - Copyright © 2013–2019 David Roden + * Sone - ImageBuilder.kt - Copyright © 2013–2020 David 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 diff --git a/src/main/kotlin/net/pterodactylus/sone/database/ImageBuilderFactory.kt b/src/main/kotlin/net/pterodactylus/sone/database/ImageBuilderFactory.kt index f9f8be7..faad2ca 100644 --- a/src/main/kotlin/net/pterodactylus/sone/database/ImageBuilderFactory.kt +++ b/src/main/kotlin/net/pterodactylus/sone/database/ImageBuilderFactory.kt @@ -1,5 +1,5 @@ /* - * Sone - ImageBuilderFactory.kt - Copyright © 2013–2019 David Roden + * Sone - ImageBuilderFactory.kt - Copyright © 2013–2020 David 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 diff --git a/src/main/kotlin/net/pterodactylus/sone/database/ImageDatabase.kt b/src/main/kotlin/net/pterodactylus/sone/database/ImageDatabase.kt index b55c073..81eecc6 100644 --- a/src/main/kotlin/net/pterodactylus/sone/database/ImageDatabase.kt +++ b/src/main/kotlin/net/pterodactylus/sone/database/ImageDatabase.kt @@ -1,5 +1,5 @@ /* - * Sone - ImageDatabase.kt - Copyright © 2013–2019 David Roden + * Sone - ImageDatabase.kt - Copyright © 2013–2020 David 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 diff --git a/src/main/kotlin/net/pterodactylus/sone/database/ImageProvider.kt b/src/main/kotlin/net/pterodactylus/sone/database/ImageProvider.kt index 0cde52a..9aa81ac 100644 --- a/src/main/kotlin/net/pterodactylus/sone/database/ImageProvider.kt +++ b/src/main/kotlin/net/pterodactylus/sone/database/ImageProvider.kt @@ -1,5 +1,5 @@ /* - * Sone - ImageProvider.kt - Copyright © 2013–2019 David Roden + * Sone - ImageProvider.kt - Copyright © 2013–2020 David 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 diff --git a/src/main/kotlin/net/pterodactylus/sone/database/ImageStore.kt b/src/main/kotlin/net/pterodactylus/sone/database/ImageStore.kt index 4be9dd9..4bff9e7 100644 --- a/src/main/kotlin/net/pterodactylus/sone/database/ImageStore.kt +++ b/src/main/kotlin/net/pterodactylus/sone/database/ImageStore.kt @@ -1,5 +1,5 @@ /* - * Sone - ImageStore.kt - Copyright © 2013–2019 David Roden + * Sone - ImageStore.kt - Copyright © 2013–2020 David 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 diff --git a/src/main/kotlin/net/pterodactylus/sone/database/PostBuilder.kt b/src/main/kotlin/net/pterodactylus/sone/database/PostBuilder.kt index 542bf78..634eda6 100644 --- a/src/main/kotlin/net/pterodactylus/sone/database/PostBuilder.kt +++ b/src/main/kotlin/net/pterodactylus/sone/database/PostBuilder.kt @@ -1,5 +1,5 @@ /* - * Sone - PostBuilder.kt - Copyright © 2013–2019 David Roden + * Sone - PostBuilder.kt - Copyright © 2013–2020 David 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 diff --git a/src/main/kotlin/net/pterodactylus/sone/database/PostBuilderFactory.kt b/src/main/kotlin/net/pterodactylus/sone/database/PostBuilderFactory.kt index e20248f..ba1b3a1 100644 --- a/src/main/kotlin/net/pterodactylus/sone/database/PostBuilderFactory.kt +++ b/src/main/kotlin/net/pterodactylus/sone/database/PostBuilderFactory.kt @@ -1,5 +1,5 @@ /* - * Sone - PostBuilderFactory.kt - Copyright © 2013–2019 David Roden + * Sone - PostBuilderFactory.kt - Copyright © 2013–2020 David 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 diff --git a/src/main/kotlin/net/pterodactylus/sone/database/PostDatabase.kt b/src/main/kotlin/net/pterodactylus/sone/database/PostDatabase.kt index 02b9fd1..cef238a 100644 --- a/src/main/kotlin/net/pterodactylus/sone/database/PostDatabase.kt +++ b/src/main/kotlin/net/pterodactylus/sone/database/PostDatabase.kt @@ -1,5 +1,5 @@ /* - * Sone - PostDatabase.kt - Copyright © 2013–2019 David Roden + * Sone - PostDatabase.kt - Copyright © 2013–2020 David 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 diff --git a/src/main/kotlin/net/pterodactylus/sone/database/PostProvider.kt b/src/main/kotlin/net/pterodactylus/sone/database/PostProvider.kt index abf7ab9..2641e3b 100644 --- a/src/main/kotlin/net/pterodactylus/sone/database/PostProvider.kt +++ b/src/main/kotlin/net/pterodactylus/sone/database/PostProvider.kt @@ -1,5 +1,5 @@ /* - * Sone - PostProvider.kt - Copyright © 2011–2019 David Roden + * Sone - PostProvider.kt - Copyright © 2011–2020 David 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 diff --git a/src/main/kotlin/net/pterodactylus/sone/database/PostReplyBuilder.kt b/src/main/kotlin/net/pterodactylus/sone/database/PostReplyBuilder.kt index d8d7f8d..9ff01b9 100644 --- a/src/main/kotlin/net/pterodactylus/sone/database/PostReplyBuilder.kt +++ b/src/main/kotlin/net/pterodactylus/sone/database/PostReplyBuilder.kt @@ -1,5 +1,5 @@ /* - * Sone - PostReplyBuilder.kt - Copyright © 2013–2019 David Roden + * Sone - PostReplyBuilder.kt - Copyright © 2013–2020 David 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 diff --git a/src/main/kotlin/net/pterodactylus/sone/database/PostReplyBuilderFactory.kt b/src/main/kotlin/net/pterodactylus/sone/database/PostReplyBuilderFactory.kt index 50b58e8..b2788b5 100644 --- a/src/main/kotlin/net/pterodactylus/sone/database/PostReplyBuilderFactory.kt +++ b/src/main/kotlin/net/pterodactylus/sone/database/PostReplyBuilderFactory.kt @@ -1,5 +1,5 @@ /* - * Sone - PostReplyBuilderFactory.kt - Copyright © 2013–2019 David Roden + * Sone - PostReplyBuilderFactory.kt - Copyright © 2013–2020 David 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 diff --git a/src/main/kotlin/net/pterodactylus/sone/database/PostReplyDatabase.kt b/src/main/kotlin/net/pterodactylus/sone/database/PostReplyDatabase.kt index 316c772..458ba0c 100644 --- a/src/main/kotlin/net/pterodactylus/sone/database/PostReplyDatabase.kt +++ b/src/main/kotlin/net/pterodactylus/sone/database/PostReplyDatabase.kt @@ -1,5 +1,5 @@ /* - * Sone - PostReplyDatabase.kt - Copyright © 2013–2019 David Roden + * Sone - PostReplyDatabase.kt - Copyright © 2013–2020 David 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 diff --git a/src/main/kotlin/net/pterodactylus/sone/database/PostReplyProvider.kt b/src/main/kotlin/net/pterodactylus/sone/database/PostReplyProvider.kt index cc797d7..c29e443 100644 --- a/src/main/kotlin/net/pterodactylus/sone/database/PostReplyProvider.kt +++ b/src/main/kotlin/net/pterodactylus/sone/database/PostReplyProvider.kt @@ -1,5 +1,5 @@ /* - * Sone - PostReplyProvider.kt - Copyright © 2013–2019 David Roden + * Sone - PostReplyProvider.kt - Copyright © 2013–2020 David 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 @@ -17,11 +17,14 @@ package net.pterodactylus.sone.database +import com.google.inject.* import net.pterodactylus.sone.data.* +import net.pterodactylus.sone.database.memory.* /** * Interface for objects that can provide [PostReply]s. */ +@ImplementedBy(MemoryDatabase::class) interface PostReplyProvider { fun getPostReply(id: String): PostReply? diff --git a/src/main/kotlin/net/pterodactylus/sone/database/PostReplyStore.kt b/src/main/kotlin/net/pterodactylus/sone/database/PostReplyStore.kt index f4839b1..671ab53 100644 --- a/src/main/kotlin/net/pterodactylus/sone/database/PostReplyStore.kt +++ b/src/main/kotlin/net/pterodactylus/sone/database/PostReplyStore.kt @@ -1,5 +1,5 @@ /* - * Sone - PostReplyStore.kt - Copyright © 2013–2019 David Roden + * Sone - PostReplyStore.kt - Copyright © 2013–2020 David 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 diff --git a/src/main/kotlin/net/pterodactylus/sone/database/PostStore.kt b/src/main/kotlin/net/pterodactylus/sone/database/PostStore.kt index 84ea39e..f033d59 100644 --- a/src/main/kotlin/net/pterodactylus/sone/database/PostStore.kt +++ b/src/main/kotlin/net/pterodactylus/sone/database/PostStore.kt @@ -1,5 +1,5 @@ /* - * Sone - PostStore.kt - Copyright © 2013–2019 David Roden + * Sone - PostStore.kt - Copyright © 2013–2020 David 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 diff --git a/src/main/kotlin/net/pterodactylus/sone/database/ReplyBuilder.kt b/src/main/kotlin/net/pterodactylus/sone/database/ReplyBuilder.kt index bfc74ac..47d0e0d 100644 --- a/src/main/kotlin/net/pterodactylus/sone/database/ReplyBuilder.kt +++ b/src/main/kotlin/net/pterodactylus/sone/database/ReplyBuilder.kt @@ -1,5 +1,5 @@ /* - * Sone - ReplyBuilder.kt - Copyright © 2013–2019 David Roden + * Sone - ReplyBuilder.kt - Copyright © 2013–2020 David 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 diff --git a/src/main/kotlin/net/pterodactylus/sone/database/SoneProvider.kt b/src/main/kotlin/net/pterodactylus/sone/database/SoneProvider.kt index 4156d66..fa57340 100644 --- a/src/main/kotlin/net/pterodactylus/sone/database/SoneProvider.kt +++ b/src/main/kotlin/net/pterodactylus/sone/database/SoneProvider.kt @@ -1,5 +1,5 @@ /* - * Sone - SoneProvider.kt - Copyright © 2011–2019 David Roden + * Sone - SoneProvider.kt - Copyright © 2011–2020 David 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 diff --git a/src/main/kotlin/net/pterodactylus/sone/fcp/AbstractSoneCommand.kt b/src/main/kotlin/net/pterodactylus/sone/fcp/AbstractSoneCommand.kt index 915c536..08e0593 100644 --- a/src/main/kotlin/net/pterodactylus/sone/fcp/AbstractSoneCommand.kt +++ b/src/main/kotlin/net/pterodactylus/sone/fcp/AbstractSoneCommand.kt @@ -1,5 +1,5 @@ /* - * Sone - AbstractSoneCommand.kt - Copyright © 2011–2019 David Roden + * Sone - AbstractSoneCommand.kt - Copyright © 2011–2020 David 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 diff --git a/src/main/kotlin/net/pterodactylus/sone/freenet/AsyncFreenetInterface.kt b/src/main/kotlin/net/pterodactylus/sone/freenet/AsyncFreenetInterface.kt new file mode 100644 index 0000000..8219d7e --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/freenet/AsyncFreenetInterface.kt @@ -0,0 +1,27 @@ +package net.pterodactylus.sone.freenet + +import freenet.client.* +import freenet.keys.* +import kotlinx.coroutines.* +import net.pterodactylus.sone.core.* + +class AsyncFreenetInterface(private val freenetClient: FreenetClient) { + + suspend fun fetchUri(freenetUri: FreenetURI): Fetched { + var currentUri = freenetUri + var result: FetchResult? = null + while (result == null) { + try { + result = withContext(Dispatchers.Default) { freenetClient.fetch(currentUri) } + } catch (fetchException: FetchException) { + if (fetchException.mode == FetchException.FetchExceptionMode.PERMANENT_REDIRECT) { + currentUri = fetchException.newURI + continue + } else + throw fetchException + } + } + return Fetched(currentUri, result) + } + +} diff --git a/src/main/kotlin/net/pterodactylus/sone/freenet/BaseL10nTranslation.kt b/src/main/kotlin/net/pterodactylus/sone/freenet/BaseL10nTranslation.kt new file mode 100644 index 0000000..1b41f70 --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/freenet/BaseL10nTranslation.kt @@ -0,0 +1,16 @@ +package net.pterodactylus.sone.freenet + +import freenet.l10n.* +import java.util.* + +/** + * [Translation] implementation based on Fred’s [BaseL10n]. + */ +class BaseL10nTranslation(private val baseL10n: BaseL10n) : Translation { + + override val currentLocale: Locale + get() = Locale(baseL10n.selectedLanguage.shortCode) + + override fun translate(key: String): String = baseL10n.getString(key) + +} diff --git a/src/main/kotlin/net/pterodactylus/sone/freenet/FreenetClient.kt b/src/main/kotlin/net/pterodactylus/sone/freenet/FreenetClient.kt new file mode 100644 index 0000000..8fbbe57 --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/freenet/FreenetClient.kt @@ -0,0 +1,20 @@ +package net.pterodactylus.sone.freenet + +import freenet.client.* +import freenet.keys.* + +/** + * Facade for Freenet’s [freenet.client.HighLevelSimpleClient] to allow testing. + */ +interface FreenetClient { + + fun fetch(freenetKey: FreenetURI): FetchResult + +} + +class DefaultFreenetClient(private val highLevelSimpleClient: HighLevelSimpleClient) : FreenetClient { + + override fun fetch(freenetKey: FreenetURI): FetchResult = + highLevelSimpleClient.fetch(freenetKey) + +} diff --git a/src/main/kotlin/net/pterodactylus/sone/freenet/FreenetURIs.kt b/src/main/kotlin/net/pterodactylus/sone/freenet/FreenetURIs.kt new file mode 100644 index 0000000..6d40b17 --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/freenet/FreenetURIs.kt @@ -0,0 +1,6 @@ +package net.pterodactylus.sone.freenet + +import freenet.keys.* +import net.pterodactylus.sone.utils.* + +val FreenetURI.routingKeyString: String get() = routingKey.asFreenetBase64 diff --git a/src/main/kotlin/net/pterodactylus/sone/freenet/L10nFilter.kt b/src/main/kotlin/net/pterodactylus/sone/freenet/L10nFilter.kt new file mode 100644 index 0000000..50d240b --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/freenet/L10nFilter.kt @@ -0,0 +1,51 @@ +/* + * Sone - L10nFilter.kt - Copyright © 2010–2020 David 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 . + */ + +package net.pterodactylus.sone.freenet + +import net.pterodactylus.util.template.* +import java.text.* + +/** + * [Filter] implementation replaces [String] values with their + * translated equivalents. + */ +class L10nFilter(private val translation: Translation) : Filter { + + override fun format(templateContext: TemplateContext?, data: Any?, parameters: Map?): String { + val parameterValues = getParameters(data, parameters) + val text = getText(data) + return if (parameterValues.isEmpty()) { + translation.translate(text) + } else + MessageFormat(translation.translate(text), translation.currentLocale).format(parameterValues.toTypedArray()) + } + + private fun getText(data: Any?) = (data as? L10nText)?.text ?: data.toString() + + private fun getParameters(data: Any?, parameters: Map?) = + if (data is L10nText) + data.parameters + else + (parameters ?: emptyMap()).let { params -> + generateSequence(0) { it + 1 } + .takeWhile { it.toString() in params } + .map { params[it.toString()] } + .toList() + } + +} diff --git a/src/main/kotlin/net/pterodactylus/sone/freenet/Translation.kt b/src/main/kotlin/net/pterodactylus/sone/freenet/Translation.kt new file mode 100644 index 0000000..d583c49 --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/freenet/Translation.kt @@ -0,0 +1,38 @@ +/** + * Sone - Translation.kt - Copyright © 2019–2020 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 . + */ + +package net.pterodactylus.sone.freenet + +import java.util.* + +/** + * Facade for Fred’s [freenet.l10n.BaseL10n] object. + */ +interface Translation { + + /** The currently selected locale. */ + val currentLocale: Locale + + /** + * Returns the translated string for the given key, defaulting to `""`. + * + * @param key The key to return the translated string for + * @return The translated string, or `""` if there is no translation + */ + fun translate(key: String): String + +} diff --git a/src/main/kotlin/net/pterodactylus/sone/freenet/plugin/FredPluginConnector.kt b/src/main/kotlin/net/pterodactylus/sone/freenet/plugin/FredPluginConnector.kt new file mode 100644 index 0000000..98849c4 --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/freenet/plugin/FredPluginConnector.kt @@ -0,0 +1,35 @@ +/* Fred’s plugin stuff is mostly deprecated. ¯\_(ツ)_/¯ */ +@file:Suppress("DEPRECATION") + +package net.pterodactylus.sone.freenet.plugin + +import freenet.pluginmanager.* +import freenet.support.* +import freenet.support.api.* +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.* +import javax.inject.* + +/** + * [PluginConnector] implementation that uses a [PluginRespiratorFacade] and coroutines to send + * a request to another plugin and receive a reply. + */ +class FredPluginConnector @Inject constructor(private val pluginRespiratorFacade: PluginRespiratorFacade) : PluginConnector { + + override suspend fun sendRequest(pluginName: String, fields: SimpleFieldSet, data: Bucket?): PluginReply { + val receivedReply = Channel() + val responseReceiver = FredPluginTalker { _, _, responseFields, responseData -> + GlobalScope.launch { + receivedReply.send(PluginReply(responseFields, responseData)) + } + } + try { + val pluginTalker = pluginRespiratorFacade.getPluginTalker(responseReceiver, pluginName, "") + pluginTalker.send(fields, data) + return receivedReply.receive() + } catch (e: PluginNotFoundException) { + throw PluginException(cause = e) + } + } + +} diff --git a/src/main/kotlin/net/pterodactylus/sone/freenet/plugin/PluginConnector.kt b/src/main/kotlin/net/pterodactylus/sone/freenet/plugin/PluginConnector.kt new file mode 100644 index 0000000..88feb13 --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/freenet/plugin/PluginConnector.kt @@ -0,0 +1,43 @@ +/* + * Sone - PluginConnector.kt - Copyright © 2010–2020 David 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 . + */ + +package net.pterodactylus.sone.freenet.plugin + +import freenet.support.* +import freenet.support.api.* + +/** + * Interface for talking to other plugins. Other plugins are identified by their + * name and a unique connection identifier. + */ +interface PluginConnector { + + /** + * Sends a message to another plugin running in the same node. + * + * @param pluginName The fully qualified name of the plugin + * @param fields The message being sent + * @param data Optional data + * @return The reply from the plugin + * @throws PluginException if the plugin identified by [pluginName] does not exist + */ + @Throws(PluginException::class) + suspend fun sendRequest(pluginName: String, fields: SimpleFieldSet, data: Bucket? = null): PluginReply + +} + +data class PluginReply(val fields: SimpleFieldSet, val data: Bucket?) diff --git a/src/main/kotlin/net/pterodactylus/sone/freenet/plugin/PluginException.kt b/src/main/kotlin/net/pterodactylus/sone/freenet/plugin/PluginException.kt new file mode 100644 index 0000000..fadd8b5 --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/freenet/plugin/PluginException.kt @@ -0,0 +1,25 @@ +/* + * Sone - PluginException.kt - Copyright © 2010–2020 David 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 . + */ + +package net.pterodactylus.sone.freenet.plugin + +import net.pterodactylus.sone.freenet.wot.* + +/** + * Exception that signals an error when communicating with a plugin. + */ +class PluginException(message: String? = null, cause: Throwable? = null) : WebOfTrustException(message, cause) diff --git a/src/main/kotlin/net/pterodactylus/sone/freenet/plugin/PluginRespiratorFacade.kt b/src/main/kotlin/net/pterodactylus/sone/freenet/plugin/PluginRespiratorFacade.kt new file mode 100644 index 0000000..e1d5591 --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/freenet/plugin/PluginRespiratorFacade.kt @@ -0,0 +1,66 @@ +/** + * Sone - PluginRespiratorFacade.kt - Copyright © 2019–2020 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 . + */ + +/* Yes, this handle Fred-based stuff that’s mostly deprecated. */ +@file:Suppress("DEPRECATION") + +package net.pterodactylus.sone.freenet.plugin + +import freenet.pluginmanager.* +import freenet.support.* +import freenet.support.api.* +import javax.inject.* + +/** + * Facade for the only method of a [plugin respirator][PluginRespirator] that Sone actually uses, + * for easier testing. + */ +interface PluginRespiratorFacade { + + @Throws(PluginNotFoundException::class) + fun getPluginTalker(pluginTalker: FredPluginTalker, pluginName: String, identifier: String): PluginTalkerFacade + +} + +/** + * Facade for a [plugin talker][PluginTalker], for easier testing. + */ +interface PluginTalkerFacade { + + fun send(pluginParameters: SimpleFieldSet, data: Bucket?) + +} + +/** + * Fred-based [PluginRespiratorFacade] implementation that proxies the given real [PluginRespirator]. + */ +class FredPluginRespiratorFacade @Inject constructor(private val pluginRespirator: PluginRespirator) : PluginRespiratorFacade { + + override fun getPluginTalker(pluginTalker: FredPluginTalker, pluginName: String, identifier: String) = + FredPluginTalkerFacade(pluginRespirator.getPluginTalker(pluginTalker, pluginName, identifier)) + +} + +/** + * Fred-based [PluginTalkerFacade] implementation that proxies the given real [PluginTalker]. + */ +class FredPluginTalkerFacade(private val pluginTalker: PluginTalker) : PluginTalkerFacade { + + override fun send(pluginParameters: SimpleFieldSet, data: Bucket?) = + pluginTalker.send(pluginParameters, data) + +} diff --git a/src/main/kotlin/net/pterodactylus/sone/freenet/wot/Context.kt b/src/main/kotlin/net/pterodactylus/sone/freenet/wot/Context.kt new file mode 100644 index 0000000..8b9dda6 --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/freenet/wot/Context.kt @@ -0,0 +1,24 @@ +/* + * Sone - Context.kt - Copyright © 2014–2020 David 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 . + */ + +package net.pterodactylus.sone.freenet.wot + +/** + * Custom container for the Web of Trust context. This allows easier + * configuration of dependency injection. + */ +class Context(val context: String) diff --git a/src/main/kotlin/net/pterodactylus/sone/freenet/wot/DefaultIdentity.kt b/src/main/kotlin/net/pterodactylus/sone/freenet/wot/DefaultIdentity.kt new file mode 100644 index 0000000..6097916 --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/freenet/wot/DefaultIdentity.kt @@ -0,0 +1,109 @@ +/* + * Sone - DefaultIdentity.kt - Copyright © 2010–2020 David 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 . + */ + +package net.pterodactylus.sone.freenet.wot + +import java.util.Collections.* + +/** + * A Web of Trust identity. + */ +open class DefaultIdentity(private val id: String, private val nickname: String?, private val requestUri: String) : Identity { + + private val contexts = mutableSetOf().synchronized() + private val properties = mutableMapOf().synchronized() + private val trustCache = mutableMapOf().synchronized() + + override fun getId() = id + override fun getNickname() = nickname + override fun getRequestUri() = requestUri + override fun getContexts() = synchronized(contexts) { contexts.toSet() } + + override fun hasContext(context: String) = context in contexts + + override fun setContexts(contexts: Set) { + synchronized(this.contexts) { + this.contexts.clear() + this.contexts.addAll(contexts) + } + } + + override fun addContext(context: String): Identity = apply { + synchronized(this.contexts) { + contexts += context + } + } + + override fun removeContext(context: String): Identity = apply { + synchronized(this.contexts) { + contexts -= context + } + } + + override fun getProperties() = synchronized(properties) { properties.toMap() } + + override fun setProperties(properties: Map) { + synchronized(this.properties) { + this.properties.clear() + this.properties.putAll(properties) + } + } + + override fun getProperty(name: String) = synchronized(properties) { properties[name] } + + override fun setProperty(name: String, value: String): Identity = apply { + synchronized(properties) { + properties[name] = value + } + } + + override fun removeProperty(name: String): Identity = apply { + synchronized(properties) { + properties -= name + } + } + + override fun getTrust(ownIdentity: OwnIdentity) = synchronized(trustCache) { + trustCache[ownIdentity] + } + + override fun setTrust(ownIdentity: OwnIdentity, trust: Trust) = apply { + synchronized(trustCache) { + trustCache[ownIdentity] = trust + } + } + + override fun removeTrust(ownIdentity: OwnIdentity) = apply { + synchronized(trustCache) { + trustCache -= ownIdentity + } + } + + override fun hashCode() = id.hashCode() + + override fun equals(other: Any?) = if (other !is Identity) { + false + } else { + other.id == getId() + } + + override fun toString() = "${javaClass.simpleName}[id=$id,nickname=$nickname,contexts=$contexts,properties=$properties]" + +} + +private fun Set.synchronized(): MutableSet = synchronizedSet(this) +private fun Map.synchronized(): MutableMap = synchronizedMap(this) diff --git a/src/main/kotlin/net/pterodactylus/sone/freenet/wot/DefaultOwnIdentity.kt b/src/main/kotlin/net/pterodactylus/sone/freenet/wot/DefaultOwnIdentity.kt new file mode 100644 index 0000000..59b6c17 --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/freenet/wot/DefaultOwnIdentity.kt @@ -0,0 +1,38 @@ +/* + * Sone - DefaultOwnIdentity.kt - Copyright © 2010–2020 David 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 . + */ + +package net.pterodactylus.sone.freenet.wot + +/** + * An own identity is an identity that the owner of the node has full control + * over. + */ +class DefaultOwnIdentity(id: String, nickname: String, requestUri: String, private val insertUri: String) : DefaultIdentity(id, nickname, requestUri), OwnIdentity { + + override fun getInsertUri(): String { + return insertUri + } + + override fun addContext(context: String) = super.addContext(context) as OwnIdentity + + override fun removeContext(context: String) = super.removeContext(context) as OwnIdentity + + override fun setProperty(name: String, value: String) = super.setProperty(name, value) as OwnIdentity + + override fun removeProperty(name: String) = super.removeProperty(name) as OwnIdentity + +} diff --git a/src/main/kotlin/net/pterodactylus/sone/freenet/wot/IdentityChangeEventSender.kt b/src/main/kotlin/net/pterodactylus/sone/freenet/wot/IdentityChangeEventSender.kt new file mode 100644 index 0000000..e6a33af --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/freenet/wot/IdentityChangeEventSender.kt @@ -0,0 +1,63 @@ +/* + * Sone - IdentityChangeEventSender.kt - Copyright © 2013–2020 David 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 . + */ + +package net.pterodactylus.sone.freenet.wot + +import com.google.common.eventbus.* +import net.pterodactylus.sone.freenet.wot.event.* + +/** + * Detects changes in [Identity]s trusted by multiple [OwnIdentity]s. + * + * @see IdentityChangeDetector + */ +class IdentityChangeEventSender(private val eventBus: EventBus, private val oldIdentities: Map>) { + + fun detectChanges(identities: Map>) { + val identityChangeDetector = IdentityChangeDetector(oldIdentities.keys) + identityChangeDetector.onNewIdentity = addNewOwnIdentityAndItsTrustedIdentities(identities) + identityChangeDetector.onRemovedIdentity = removeOwnIdentityAndItsTrustedIdentities(oldIdentities) + identityChangeDetector.onUnchangedIdentity = detectChangesInTrustedIdentities(identities, oldIdentities) + identityChangeDetector.detectChanges(identities.keys) + } + + private fun addNewOwnIdentityAndItsTrustedIdentities(newIdentities: Map>) = + { identity: Identity -> + eventBus.post(OwnIdentityAddedEvent(identity as OwnIdentity)) + newIdentities[identity] + ?.map { IdentityAddedEvent(identity, it) } + ?.forEach(eventBus::post) ?: Unit + } + + private fun removeOwnIdentityAndItsTrustedIdentities(oldIdentities: Map>) = + { identity: Identity -> + eventBus.post(OwnIdentityRemovedEvent(identity as OwnIdentity)) + oldIdentities[identity] + ?.map { IdentityRemovedEvent(identity, it) } + ?.forEach(eventBus::post) ?: Unit + } + + private fun detectChangesInTrustedIdentities(newIdentities: Map>, oldIdentities: Map>) = + { ownIdentity: Identity -> + val identityChangeDetector = IdentityChangeDetector(oldIdentities[ownIdentity as OwnIdentity]!!) + identityChangeDetector.onNewIdentity = { eventBus.post(IdentityAddedEvent(ownIdentity, it)) } + identityChangeDetector.onRemovedIdentity = { eventBus.post(IdentityRemovedEvent(ownIdentity, it)) } + identityChangeDetector.onChangedIdentity = { eventBus.post(IdentityUpdatedEvent(ownIdentity, it)) } + identityChangeDetector.detectChanges(newIdentities[ownIdentity]!!) + } + +} diff --git a/src/main/kotlin/net/pterodactylus/sone/freenet/wot/IdentityLoader.kt b/src/main/kotlin/net/pterodactylus/sone/freenet/wot/IdentityLoader.kt new file mode 100644 index 0000000..474ab57 --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/freenet/wot/IdentityLoader.kt @@ -0,0 +1,64 @@ +/* + * Sone - IdentityLoader.kt - Copyright © 2013–2020 David 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 . + */ + +package net.pterodactylus.sone.freenet.wot + +import com.google.common.base.* +import com.google.inject.* +import net.pterodactylus.sone.freenet.plugin.* +import java.util.concurrent.TimeUnit.* +import java.util.logging.* + +/** + * Loads [OwnIdentity]s and the [Identity]s they trust. + */ +class IdentityLoader @Inject constructor(private val webOfTrustConnector: WebOfTrustConnector, private val context: Context? = null) { + + private val logger: Logger = Logger.getLogger(IdentityLoader::class.java.name) + + @Throws(WebOfTrustException::class) + fun loadIdentities() = + time({ stopwatch, identities -> "Loaded ${identities.size} own identities in ${stopwatch.elapsed(MILLISECONDS) / 1000.0}s." }) { + webOfTrustConnector.loadAllOwnIdentities() + }.let(this::loadTrustedIdentitiesForOwnIdentities) + + @Throws(PluginException::class) + private fun loadTrustedIdentitiesForOwnIdentities(ownIdentities: Collection) = + ownIdentities + .also { logger.fine { "Getting trusted identities for ${it.size} own identities..." } } + .associateWith { ownIdentity -> + logger.fine { "Getting trusted identities for $ownIdentity..." } + if (ownIdentity.doesNotHaveCorrectContext()) { + logger.fine { "Skipping $ownIdentity because of incorrect context." } + emptySet() + } else { + logger.fine { "Loading trusted identities for $ownIdentity from WoT..." } + time({ stopwatch, identities -> "Loaded ${identities.size} identities for ${ownIdentity.nickname} in ${stopwatch.elapsed(MILLISECONDS) / 1000.0}s." }) { + webOfTrustConnector.loadTrustedIdentities(ownIdentity, context?.context) + } + } + } + + private fun OwnIdentity.doesNotHaveCorrectContext() = + context?.let { it.context !in contexts } ?: false + + private fun time(logMessage: (Stopwatch, R) -> String, loader: () -> R) = + Stopwatch.createStarted().let { stopwatch -> + loader().also { logger.fine(logMessage(stopwatch, it)) } + } + +} diff --git a/src/main/kotlin/net/pterodactylus/sone/freenet/wot/IdentityManager.kt b/src/main/kotlin/net/pterodactylus/sone/freenet/wot/IdentityManager.kt new file mode 100644 index 0000000..e90af91 --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/freenet/wot/IdentityManager.kt @@ -0,0 +1,18 @@ +package net.pterodactylus.sone.freenet.wot + +import net.pterodactylus.util.service.Service + +import com.google.common.eventbus.EventBus +import com.google.inject.ImplementedBy + +/** + * Connects to a [WebOfTrustConnector] and sends identity events to an + * [EventBus]. + */ +@ImplementedBy(IdentityManagerImpl::class) +interface IdentityManager : Service { + + val isConnected: Boolean + val allOwnIdentities: Set + +} diff --git a/src/main/kotlin/net/pterodactylus/sone/freenet/wot/IdentityManagerImpl.kt b/src/main/kotlin/net/pterodactylus/sone/freenet/wot/IdentityManagerImpl.kt new file mode 100644 index 0000000..67e70e0 --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/freenet/wot/IdentityManagerImpl.kt @@ -0,0 +1,91 @@ +/* + * Sone - IdentityManagerImpl.kt - Copyright © 2010–2020 David 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 . + */ + +package net.pterodactylus.sone.freenet.wot + +import com.google.common.eventbus.* +import com.google.inject.* +import net.pterodactylus.util.service.* +import java.util.concurrent.TimeUnit.* +import java.util.logging.* +import java.util.logging.Logger.* + +/** + * The identity manager takes care of loading and storing identities, their + * contexts, and properties. It does so in a way that does not expose errors via + * exceptions but it only logs them and tries to return sensible defaults. + * + * + * It is also responsible for polling identities from the Web of Trust plugin + * and sending events to the [EventBus] when [Identity]s and + * [OwnIdentity]s are discovered or disappearing. + */ +@Singleton +class IdentityManagerImpl @Inject constructor( + private val eventBus: EventBus, + private val webOfTrustConnector: WebOfTrustConnector, + private val identityLoader: IdentityLoader +) : AbstractService("Sone Identity Manager", false), IdentityManager { + + private val currentOwnIdentities = mutableSetOf() + + override val isConnected: Boolean + get() = notThrowing { webOfTrustConnector.ping() } + + override val allOwnIdentities: Set + get() = synchronized(currentOwnIdentities) { + currentOwnIdentities.toSet() + } + + override fun serviceRun() { + var oldIdentities = mapOf>() + + while (!shouldStop()) { + try { + val currentIdentities = identityLoader.loadIdentities() + + val identityChangeEventSender = IdentityChangeEventSender(eventBus, oldIdentities) + identityChangeEventSender.detectChanges(currentIdentities) + + oldIdentities = currentIdentities + + synchronized(currentOwnIdentities) { + currentOwnIdentities.clear() + currentOwnIdentities.addAll(currentIdentities.keys) + } + } catch (wote1: WebOfTrustException) { + logger.log(Level.WARNING, "WoT has disappeared!", wote1) + } catch (e: Exception) { + logger.log(Level.SEVERE, "Uncaught exception in IdentityManager thread!", e) + } + + /* wait a minute before checking again. */ + sleep(SECONDS.toMillis(60)) + } + } + +} + +private val logger: Logger = getLogger(IdentityManagerImpl::class.java.name) + +private fun notThrowing(action: () -> Unit): Boolean = + try { + action() + true + } catch (e: Exception) { + false + } diff --git a/src/main/kotlin/net/pterodactylus/sone/freenet/wot/PluginWebOfTrustConnector.kt b/src/main/kotlin/net/pterodactylus/sone/freenet/wot/PluginWebOfTrustConnector.kt new file mode 100644 index 0000000..2fad1d1 --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/freenet/wot/PluginWebOfTrustConnector.kt @@ -0,0 +1,121 @@ +/* + * Sone - PluginWebOfTrustConnector.kt - Copyright © 2010–2020 David 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 . + */ + +package net.pterodactylus.sone.freenet.wot + +import com.google.inject.* +import freenet.support.* +import kotlinx.coroutines.* +import net.pterodactylus.sone.freenet.* +import net.pterodactylus.sone.freenet.plugin.* +import java.lang.String.* +import java.util.logging.* +import java.util.logging.Logger +import java.util.logging.Logger.* + +/** + * Connector for the Web of Trust plugin. + */ +class PluginWebOfTrustConnector @Inject constructor(private val pluginConnector: PluginConnector) : WebOfTrustConnector { + + private val logger: Logger = getLogger(PluginWebOfTrustConnector::class.java.name) + + @Throws(PluginException::class) + override fun loadAllOwnIdentities(): Set = + performRequest(SimpleFieldSetBuilder().put("Message", "GetOwnIdentities").get()) + .fields + .parseIdentities { parseOwnIdentity(it) } + + @Throws(PluginException::class) + override fun loadTrustedIdentities(ownIdentity: OwnIdentity, context: String?): Set = + performRequest(SimpleFieldSetBuilder().put("Message", "GetIdentitiesByScore").put("Truster", ownIdentity.id).put("Selection", "+").put("Context", context ?: "").put("WantTrustValues", "true").get()) + .fields + .parseIdentities { parseTrustedIdentity(it, ownIdentity) } + + @Throws(PluginException::class) + override fun addContext(ownIdentity: OwnIdentity, context: String) { + performRequest(SimpleFieldSetBuilder().put("Message", "AddContext").put("Identity", ownIdentity.id).put("Context", context).get()) + } + + @Throws(PluginException::class) + override fun removeContext(ownIdentity: OwnIdentity, context: String) { + performRequest(SimpleFieldSetBuilder().put("Message", "RemoveContext").put("Identity", ownIdentity.id).put("Context", context).get()) + } + + override fun setProperty(ownIdentity: OwnIdentity, name: String, value: String) { + performRequest(SimpleFieldSetBuilder().put("Message", "SetProperty").put("Identity", ownIdentity.id).put("Property", name).put("Value", value).get()) + } + + override fun removeProperty(ownIdentity: OwnIdentity, name: String) { + performRequest(SimpleFieldSetBuilder().put("Message", "RemoveProperty").put("Identity", ownIdentity.id).put("Property", name).get()) + } + + override fun ping() { + performRequest(SimpleFieldSetBuilder().put("Message", "Ping").get()) + } + + private fun performRequest(fields: SimpleFieldSet): PluginReply { + logger.log(Level.FINE, format("Sending FCP Request: %s", fields.get("Message"))) + return runBlocking { + pluginConnector.sendRequest(WOT_PLUGIN_NAME, fields).also { + logger.log(Level.FINEST, format("Received FCP Response for %s: %s", fields.get("Message"), it.fields.get("Message"))) + if ("Error" == it.fields.get("Message")) { + throw PluginException("Could not perform request for " + fields.get("Message")) + } + } + } + } + +} + +private const val WOT_PLUGIN_NAME = "plugins.WebOfTrust.WebOfTrust" + +private fun SimpleFieldSet.parseIdentities(parser: SimpleFieldSet.(Int) -> I) = + scanPrefix { "Identity$it" } + .map { parser(this, it) } + .toSet() + +private fun SimpleFieldSet.parseOwnIdentity(index: Int) = + DefaultOwnIdentity(get("Identity$index"), get("Nickname$index"), get("RequestURI$index"), get("InsertURI$index")) + .setContextsAndProperties(this@parseOwnIdentity, index) + +private fun SimpleFieldSet.parseTrustedIdentity(index: Int, ownIdentity: OwnIdentity) = + DefaultIdentity(get("Identity$index"), get("Nickname$index"), get("RequestURI$index")) + .setContextsAndProperties(this@parseTrustedIdentity, index) + .apply { setTrust(ownIdentity, this@parseTrustedIdentity.parseTrust(index.toString())) } + +private fun I.setContextsAndProperties(simpleFieldSet: SimpleFieldSet, index: Int) = apply { + contexts = simpleFieldSet.contexts("Contexts$index.") + properties = simpleFieldSet.properties("Properties$index.") +} + +private fun SimpleFieldSet.parseTrust(index: String = "") = + Trust(get("Trust$index")?.toIntOrNull(), get("Score$index")?.toIntOrNull(), get("Rank$index")?.toIntOrNull()) + +private fun SimpleFieldSet.contexts(prefix: String) = + scanPrefix { "${prefix}Context$it" } + .map { get("${prefix}Context$it") } + .toSet() + +private fun SimpleFieldSet.properties(prefix: String) = + scanPrefix { "${prefix}Property${it}.Name" } + .map { get("${prefix}Property${it}.Name") to get("${prefix}Property${it}.Value") } + .toMap() + +private fun SimpleFieldSet.scanPrefix(prefix: (Int) -> String) = + generateSequence(0, Int::inc) + .takeWhile { get(prefix(it)) != null } diff --git a/src/main/kotlin/net/pterodactylus/sone/freenet/wot/Trust.kt b/src/main/kotlin/net/pterodactylus/sone/freenet/wot/Trust.kt new file mode 100644 index 0000000..779dd20 --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/freenet/wot/Trust.kt @@ -0,0 +1,23 @@ +/* + * Sone - Trust.kt - Copyright © 2010–2020 David 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 . + */ + +package net.pterodactylus.sone.freenet.wot + +/** + * Container class for trust in the web of trust. + */ +data class Trust(val explicit: Int?, val implicit: Int?, val distance: Int?) diff --git a/src/main/kotlin/net/pterodactylus/sone/freenet/wot/WebOfTrustConnector.kt b/src/main/kotlin/net/pterodactylus/sone/freenet/wot/WebOfTrustConnector.kt new file mode 100644 index 0000000..e31dce6 --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/freenet/wot/WebOfTrustConnector.kt @@ -0,0 +1,86 @@ +package net.pterodactylus.sone.freenet.wot + +import net.pterodactylus.sone.freenet.plugin.* + +/** + * Connector for the web of trust plugin. + */ +interface WebOfTrustConnector { + + /** + * Loads all own identities from the Web of Trust plugin. + * + * @return All own identity + * @throws WebOfTrustException if the own identities can not be loaded + */ + @Throws(WebOfTrustException::class) + fun loadAllOwnIdentities(): Set + + /** + * Loads all identities that the given identities trusts with a score of + * more than 0 and the (optional) given context. + * + * @param ownIdentity The own identity + * @param context The context to filter, or `null` + * @return All trusted identities + * @throws PluginException if an error occured talking to the Web of Trust plugin + */ + @Throws(PluginException::class) + fun loadTrustedIdentities(ownIdentity: OwnIdentity, context: String? = null): Set + + /** + * Adds the given context to the given identity. + * + * @param ownIdentity The identity to add the context to + * @param context The context to add + * @throws PluginException if an error occured talking to the Web of Trust plugin + */ + @Throws(PluginException::class) + fun addContext(ownIdentity: OwnIdentity, context: String) + + /** + * Removes the given context from the given identity. + * + * @param ownIdentity The identity to remove the context from + * @param context The context to remove + * @throws PluginException if an error occured talking to the Web of Trust plugin + */ + @Throws(PluginException::class) + fun removeContext(ownIdentity: OwnIdentity, context: String) + + /** + * Sets the property with the given name to the given value. + * + * @param ownIdentity The identity to set the property on + * @param name The name of the property to set + * @param value The value to set + * @throws PluginException if an error occured talking to the Web of Trust plugin + */ + @Throws(PluginException::class) + fun setProperty(ownIdentity: OwnIdentity, name: String, value: String) + + /** + * Removes the property with the given name. + * + * @param ownIdentity The identity to remove the property from + * @param name The name of the property to remove + * @throws PluginException if an error occured talking to the Web of Trust plugin + */ + @Throws(PluginException::class) + fun removeProperty(ownIdentity: OwnIdentity, name: String) + + /** + * Pings the Web of Trust plugin. If the plugin can not be reached, a + * [PluginException] is thrown. + * + * @throws PluginException if the plugin is not loaded + */ + @Throws(PluginException::class) + fun ping() + + /** + * Stops the web of trust connector. + */ + fun stop() = Unit + +} diff --git a/src/main/kotlin/net/pterodactylus/sone/freenet/wot/WebOfTrustException.kt b/src/main/kotlin/net/pterodactylus/sone/freenet/wot/WebOfTrustException.kt new file mode 100644 index 0000000..7d730e9 --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/freenet/wot/WebOfTrustException.kt @@ -0,0 +1,24 @@ +/* + * Sone - WebOfTrustException.kt - Copyright © 2010–2020 David 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 . + */ + +package net.pterodactylus.sone.freenet.wot + +/** + * Exception that signals an error processing web of trust identities, mostly + * when communicating with the web of trust plugin. + */ +open class WebOfTrustException(message: String? = null, cause: Throwable?) : Exception(message, cause) diff --git a/src/main/kotlin/net/pterodactylus/sone/freenet/wot/WebOfTrustPinger.kt b/src/main/kotlin/net/pterodactylus/sone/freenet/wot/WebOfTrustPinger.kt new file mode 100644 index 0000000..db9809e --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/freenet/wot/WebOfTrustPinger.kt @@ -0,0 +1,56 @@ +/** + * Sone - WebOfTrustPinger.kt - Copyright © 2019–2020 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 . + */ + +package net.pterodactylus.sone.freenet.wot + +import com.google.common.eventbus.* +import net.pterodactylus.sone.core.event.* +import net.pterodactylus.sone.freenet.plugin.* +import net.pterodactylus.sone.utils.* +import java.util.concurrent.atomic.* +import java.util.function.* +import javax.inject.* + +/** + * [Runnable] that is scheduled via an [Executor][java.util.concurrent.Executor], + * checks whether the web of trust plugin can be communicated with, sends + * events if its status changes and reschedules itself. + */ +class WebOfTrustPinger @Inject constructor( + private val eventBus: EventBus, + @Named("webOfTrustReacher") private val webOfTrustReacher: Runnable, + @Named("webOfTrustReschedule") private val reschedule: Consumer) : Runnable { + + private val lastState = AtomicBoolean(false) + + override fun run() { + try { + webOfTrustReacher() + if (!lastState.get()) { + eventBus.post(WebOfTrustAppeared()) + lastState.set(true) + } + } catch (e: PluginException) { + if (lastState.get()) { + eventBus.post(WebOfTrustDisappeared()) + lastState.set(false) + } + } + reschedule(this) + } + +} diff --git a/src/main/kotlin/net/pterodactylus/sone/freenet/wot/event/IdentityAddedEvent.kt b/src/main/kotlin/net/pterodactylus/sone/freenet/wot/event/IdentityAddedEvent.kt new file mode 100644 index 0000000..d316841 --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/freenet/wot/event/IdentityAddedEvent.kt @@ -0,0 +1,25 @@ +/* + * Sone - IdentityAddedEvent.kt - Copyright © 2013–2020 David 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 . + */ + +package net.pterodactylus.sone.freenet.wot.event + +import net.pterodactylus.sone.freenet.wot.* + +/** + * Event that signals that an [Identity] was added. + */ +data class IdentityAddedEvent(val ownIdentity: OwnIdentity, val identity: Identity) diff --git a/src/main/kotlin/net/pterodactylus/sone/freenet/wot/event/IdentityRemovedEvent.kt b/src/main/kotlin/net/pterodactylus/sone/freenet/wot/event/IdentityRemovedEvent.kt new file mode 100644 index 0000000..72c262c --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/freenet/wot/event/IdentityRemovedEvent.kt @@ -0,0 +1,26 @@ +/* + * Sone - IdentityRemovedEvent.kt - Copyright © 2013–2020 David 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 . + */ + +package net.pterodactylus.sone.freenet.wot.event + +import net.pterodactylus.sone.freenet.wot.Identity +import net.pterodactylus.sone.freenet.wot.OwnIdentity + +/** + * Event that signals that an [Identity] was removed. + */ +data class IdentityRemovedEvent(val ownIdentity: OwnIdentity, val identity: Identity) diff --git a/src/main/kotlin/net/pterodactylus/sone/freenet/wot/event/IdentityUpdatedEvent.kt b/src/main/kotlin/net/pterodactylus/sone/freenet/wot/event/IdentityUpdatedEvent.kt new file mode 100644 index 0000000..edf34c1 --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/freenet/wot/event/IdentityUpdatedEvent.kt @@ -0,0 +1,25 @@ +/* + * Sone - IdentityUpdatedEvent.kt - Copyright © 2013–2020 David 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 . + */ + +package net.pterodactylus.sone.freenet.wot.event + +import net.pterodactylus.sone.freenet.wot.* + +/** + * Event that signals that an [Identity] was updated. + */ +data class IdentityUpdatedEvent(val ownIdentity: OwnIdentity, val identity: Identity) diff --git a/src/main/kotlin/net/pterodactylus/sone/freenet/wot/event/OwnIdentityAddedEvent.kt b/src/main/kotlin/net/pterodactylus/sone/freenet/wot/event/OwnIdentityAddedEvent.kt new file mode 100644 index 0000000..3620237 --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/freenet/wot/event/OwnIdentityAddedEvent.kt @@ -0,0 +1,25 @@ +/* + * Sone - OwnIdentityAddedEvent.kt - Copyright © 2013–2020 David 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 . + */ + +package net.pterodactylus.sone.freenet.wot.event + +import net.pterodactylus.sone.freenet.wot.OwnIdentity + +/** + * Event that signals that an [OwnIdentity] was added. + */ +data class OwnIdentityAddedEvent(val ownIdentity: OwnIdentity) diff --git a/src/main/kotlin/net/pterodactylus/sone/freenet/wot/event/OwnIdentityRemovedEvent.kt b/src/main/kotlin/net/pterodactylus/sone/freenet/wot/event/OwnIdentityRemovedEvent.kt new file mode 100644 index 0000000..3f6fdfc --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/freenet/wot/event/OwnIdentityRemovedEvent.kt @@ -0,0 +1,25 @@ +/* + * Sone - OwnIdentityRemovedEvent.kt - Copyright © 2013–2020 David 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 . + */ + +package net.pterodactylus.sone.freenet.wot.event + +import net.pterodactylus.sone.freenet.wot.* + +/** + * Event that signals that an [OwnIdentity] was removed. + */ +data class OwnIdentityRemovedEvent(val ownIdentity: OwnIdentity) diff --git a/src/main/kotlin/net/pterodactylus/sone/main/FreenetModule.kt b/src/main/kotlin/net/pterodactylus/sone/main/FreenetModule.kt index 9b5fa2e..1438c2a 100644 --- a/src/main/kotlin/net/pterodactylus/sone/main/FreenetModule.kt +++ b/src/main/kotlin/net/pterodactylus/sone/main/FreenetModule.kt @@ -5,6 +5,7 @@ import freenet.client.* import freenet.clients.http.* import freenet.node.* import freenet.pluginmanager.* +import net.pterodactylus.sone.freenet.plugin.* import javax.inject.Provider import javax.inject.Singleton @@ -14,8 +15,9 @@ import javax.inject.Singleton class FreenetModule(private val pluginRespirator: PluginRespirator) : Module { override fun configure(binder: Binder): Unit = binder.run { - bind(PluginRespirator::class.java).toProvider(Provider { pluginRespirator }) - pluginRespirator.node!!.let { node -> bind(Node::class.java).toProvider(Provider { node }) } + bind(PluginRespiratorFacade::class.java).toProvider(Provider { FredPluginRespiratorFacade(pluginRespirator) }).`in`(Singleton::class.java) + bind(PluginConnector::class.java).to(FredPluginConnector::class.java).`in`(Singleton::class.java) + bind(Node::class.java).toProvider(Provider { pluginRespirator.node }) bind(HighLevelSimpleClient::class.java).toProvider(Provider { pluginRespirator.hlSimpleClient!! }) bind(ToadletContainer::class.java).toProvider(Provider { pluginRespirator.toadletContainer }) bind(PageMaker::class.java).toProvider(Provider { pluginRespirator.pageMaker }) diff --git a/src/main/kotlin/net/pterodactylus/sone/main/SoneModule.kt b/src/main/kotlin/net/pterodactylus/sone/main/SoneModule.kt new file mode 100644 index 0000000..749de0d --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/main/SoneModule.kt @@ -0,0 +1,85 @@ +package net.pterodactylus.sone.main + +import com.codahale.metrics.* +import com.google.common.base.* +import com.google.common.eventbus.* +import com.google.inject.* +import com.google.inject.matcher.* +import com.google.inject.name.Names.* +import com.google.inject.spi.* +import net.pterodactylus.sone.database.* +import net.pterodactylus.sone.database.memory.* +import net.pterodactylus.sone.freenet.* +import net.pterodactylus.sone.freenet.wot.* +import net.pterodactylus.util.config.* +import net.pterodactylus.util.config.ConfigurationException +import net.pterodactylus.util.logging.* +import net.pterodactylus.util.version.Version +import java.io.* +import java.util.concurrent.* +import java.util.concurrent.Executors.* +import java.util.logging.* +import javax.inject.* +import javax.inject.Singleton + +open class SoneModule(private val sonePlugin: SonePlugin, private val eventBus: EventBus) : AbstractModule() { + + override fun configure() { + val sonePropertiesFile = File("sone.properties") + val firstStart = !sonePropertiesFile.exists() + var newConfig = false + val configuration = try { + Configuration(MapConfigurationBackend(sonePropertiesFile, false)) + } catch (ce: ConfigurationException) { + sonePropertiesFile.delete() + newConfig = true + Configuration(MapConfigurationBackend(sonePropertiesFile, true)) + } + val context = Context("Sone") + val loaders = configuration.getStringValue("Developer.LoadFromFilesystem") + .getValue(null) + ?.let { + configuration.getStringValue("Developer.FilesystemPath") + .getValue(null) + ?.let { DebugLoaders(it) } + } + + bind(Configuration::class.java).toInstance(configuration) + bind(EventBus::class.java).toInstance(eventBus) + bind(Boolean::class.java).annotatedWith(named("FirstStart")).toInstance(firstStart) + bind(Boolean::class.java).annotatedWith(named("NewConfig")).toInstance(newConfig) + bind(Context::class.java).toInstance(context) + bind(object : TypeLiteral>() {}).toInstance(Optional.of(context)) + bind(SonePlugin::class.java).toInstance(sonePlugin) + bind(Version::class.java).toInstance(sonePlugin.version.drop(1).parseVersion()) + bind(PluginVersion::class.java).toInstance(PluginVersion(sonePlugin.version)) + bind(PluginYear::class.java).toInstance(PluginYear(sonePlugin.year)) + bind(PluginHomepage::class.java).toInstance(PluginHomepage(sonePlugin.homepage)) + bind(Database::class.java).to(MemoryDatabase::class.java).`in`(Singleton::class.java) + bind(Translation::class.java).toInstance(BaseL10nTranslation(sonePlugin.l10n().base)) + loaders?.let { bind(Loaders::class.java).toInstance(it) } + bind(MetricRegistry::class.java).`in`(Singleton::class.java) + bind(WebOfTrustConnector::class.java).to(PluginWebOfTrustConnector::class.java).`in`(Singleton::class.java) + bind(TickerShutdown::class.java).`in`(Singleton::class.java) + + bindListener(Matchers.any(), object : TypeListener { + override fun hear(typeLiteral: TypeLiteral, typeEncounter: TypeEncounter) { + typeEncounter.register(InjectionListener { injectee -> + logger.fine { "Injecting $injectee..." } + eventBus.register(injectee) + }) + } + }) + } + + @Provides + @Singleton + @Named("notification") + fun getNotificationTicker(): ScheduledExecutorService = + newSingleThreadScheduledExecutor() + + private val logger: Logger = Logging.getLogger(javaClass) + +} + +private fun String.parseVersion(): Version = Version.parse(this) diff --git a/src/main/kotlin/net/pterodactylus/sone/main/SoneModuleCreator.kt b/src/main/kotlin/net/pterodactylus/sone/main/SoneModuleCreator.kt deleted file mode 100644 index 58c8d9e..0000000 --- a/src/main/kotlin/net/pterodactylus/sone/main/SoneModuleCreator.kt +++ /dev/null @@ -1,65 +0,0 @@ -package net.pterodactylus.sone.main - -import com.google.common.base.* -import com.google.common.eventbus.* -import com.google.inject.* -import com.google.inject.matcher.* -import com.google.inject.name.Names.* -import com.google.inject.spi.* -import net.pterodactylus.sone.database.* -import net.pterodactylus.sone.database.memory.* -import net.pterodactylus.sone.freenet.wot.* -import net.pterodactylus.util.config.* -import net.pterodactylus.util.config.ConfigurationException -import net.pterodactylus.util.version.Version -import java.io.* - -class SoneModuleCreator { - - fun createModule(sonePlugin: SonePlugin) = object : AbstractModule() { - override fun configure() { - val sonePropertiesFile = File("sone.properties") - val firstStart = !sonePropertiesFile.exists() - var newConfig = false - val configuration = try { - Configuration(MapConfigurationBackend(sonePropertiesFile, false)) - } catch (ce: ConfigurationException) { - sonePropertiesFile.delete() - newConfig = true - Configuration(MapConfigurationBackend(sonePropertiesFile, true)) - } - val context = Context("Sone") - val loaders = configuration.getStringValue("Developer.LoadFromFilesystem") - .getValue(null) - ?.let { - configuration.getStringValue("Developer.FilesystemPath") - .getValue(null) - ?.let { DebugLoaders(it) } - } - val eventBus = EventBus() - - bind(Configuration::class.java).toInstance(configuration) - bind(EventBus::class.java).toInstance(eventBus) - bind(Boolean::class.java).annotatedWith(named("FirstStart")).toInstance(firstStart) - bind(Boolean::class.java).annotatedWith(named("NewConfig")).toInstance(newConfig) - bind(Context::class.java).toInstance(context) - bind(object : TypeLiteral>() {}).toInstance(Optional.of(context)) - bind(SonePlugin::class.java).toInstance(sonePlugin) - bind(Version::class.java).toInstance(sonePlugin.version.parseVersion()) - bind(PluginVersion::class.java).toInstance(PluginVersion(sonePlugin.version)) - bind(PluginYear::class.java).toInstance(PluginYear(sonePlugin.year)) - bind(PluginHomepage::class.java).toInstance(PluginHomepage(sonePlugin.homepage)) - bind(Database::class.java).to(MemoryDatabase::class.java).`in`(Singleton::class.java) - loaders?.let { bind(Loaders::class.java).toInstance(it) } - - bindListener(Matchers.any(), object : TypeListener { - override fun hear(typeLiteral: TypeLiteral, typeEncounter: TypeEncounter) { - typeEncounter.register(InjectionListener { injectee -> eventBus.register(injectee) }) - } - }) - } - } - -} - -private fun String.parseVersion(): Version = Version.parse(this) diff --git a/src/main/kotlin/net/pterodactylus/sone/main/TickerShutdown.kt b/src/main/kotlin/net/pterodactylus/sone/main/TickerShutdown.kt new file mode 100644 index 0000000..de15cbe --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/main/TickerShutdown.kt @@ -0,0 +1,36 @@ +/** + * Sone - TickerShutdown.kt - Copyright © 2019–2020 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 . + */ + +package net.pterodactylus.sone.main + +import com.google.common.eventbus.* +import net.pterodactylus.sone.core.event.* +import java.util.concurrent.* +import javax.inject.* + +/** + * Wrapper around all [tickers][ScheduledExecutorService] used in Sone, + * ensuring proper shutdown. + */ +class TickerShutdown @Inject constructor(@Named("notification") private val notificationTicker: ScheduledExecutorService) { + + @Subscribe + fun shutdown(@Suppress("UNUSED_PARAMETER") shutdown: Shutdown) { + notificationTicker.shutdown() + } + +} diff --git a/src/main/kotlin/net/pterodactylus/sone/notify/ListNotification.kt b/src/main/kotlin/net/pterodactylus/sone/notify/ListNotification.kt new file mode 100644 index 0000000..60d7093 --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/notify/ListNotification.kt @@ -0,0 +1,95 @@ +/* + * Sone - ListNotification.kt - Copyright © 2010–2020 David 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 . + */ + +package net.pterodactylus.sone.notify + +import net.pterodactylus.util.notify.* +import net.pterodactylus.util.template.* +import java.lang.System.* +import java.util.concurrent.* + +/** + * Notification that maintains a list of elements. + * + * @param + * The type of the items + */ +class ListNotification : TemplateNotification { + + private val key: String + private val realElements = CopyOnWriteArrayList() + + val elements: List get() = realElements.toList() + + val isEmpty + get() = elements.isEmpty() + + @JvmOverloads + constructor(id: String, key: String, template: Template, dismissable: Boolean = true) : super(id, currentTimeMillis(), currentTimeMillis(), dismissable, template) { + this.key = key + template.initialContext.set(key, realElements) + } + + constructor(listNotification: ListNotification) : super(listNotification.id, listNotification.createdTime, listNotification.lastUpdatedTime, listNotification.isDismissable, Template()) { + this.key = listNotification.key + template.add(listNotification.template) + template.initialContext.set(key, realElements) + } + + fun setElements(elements: Collection) { + realElements.clear() + realElements.addAll(elements.distinct()) + touch() + } + + fun add(element: T) { + if (element !in realElements) { + realElements.add(element) + touch() + } + } + + fun remove(element: T) { + while (realElements.remove(element)) { + /* do nothing, just remove all instances of the element. */ + } + if (realElements.isEmpty()) { + dismiss() + } + touch() + } + + override fun dismiss() { + super.dismiss() + realElements.clear() + } + + override fun hashCode() = + realElements.fold(super.hashCode()) { hash, element -> hash xor element.hashCode() } + + override fun equals(other: Any?): Boolean { + if (other !is ListNotification<*>) { + return false + } + val listNotification = other as ListNotification<*>? + if (!super.equals(listNotification)) { + return false + } + return (key == listNotification.key) && (realElements == listNotification.realElements) + } + +} diff --git a/src/main/kotlin/net/pterodactylus/sone/notify/Notifications.kt b/src/main/kotlin/net/pterodactylus/sone/notify/Notifications.kt new file mode 100644 index 0000000..5df9484 --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/notify/Notifications.kt @@ -0,0 +1,32 @@ +/** + * Sone - Notifications.kt - Copyright © 2019–2020 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 . + */ + +package net.pterodactylus.sone.notify + +import net.pterodactylus.util.notify.* + +/** + * Returns whether the notification manager contains a notification with the given ID. + */ +operator fun NotificationManager.contains(id: String) = + getNotification(id) != null + +/** + * Returns whether the notification manager currently has a “first start” notification. + */ +fun NotificationManager.hasFirstStartNotification() = + "first-start-notification" in this diff --git a/src/main/kotlin/net/pterodactylus/sone/template/DurationFormatFilter.kt b/src/main/kotlin/net/pterodactylus/sone/template/DurationFormatFilter.kt new file mode 100644 index 0000000..db49ef1 --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/template/DurationFormatFilter.kt @@ -0,0 +1,67 @@ +package net.pterodactylus.sone.template + +import net.pterodactylus.util.template.* +import java.time.* + +class DurationFormatFilter : Filter { + + override fun format(templateContext: TemplateContext?, data: Any?, parameters: Map?): Any? { + if (data is Number) { + val scale = parameters?.get("scale") + val duration = when (scale) { + "ms" -> Duration.ofSeconds(data.toLong() / 1_000, (data.toDouble() * 1_000_000 % 1_000_000_000).toLong()) + "μs" -> Duration.ofSeconds(data.toLong() / 1_000_000, (data.toDouble() * 1_000 % 1_000_000_000).toLong()) + "ns" -> Duration.ofSeconds(data.toLong() / 1_000_000_000, data.toLong() % 1_000_000_000) + else -> Duration.ofSeconds(data.toLong(), (data.toDouble() * 1_000_000_000 % 1_000_000_000).toLong()) + } + return FixedDuration.values() + .map { it to it.number(duration) } + .firstOrNull { it.second >= 1 } + ?.let { "${"%.1f".format(it.second)}${it.first.symbol}" } + ?: "0s" + } + return data + } + +} + +@Suppress("unused") +private enum class FixedDuration { + + WEEKS { + override fun number(duration: Duration) = DAYS.number(duration) / 7.0 + override val symbol = "w" + }, + DAYS { + override fun number(duration: Duration) = HOURS.number(duration) / 24 + override val symbol = "d" + }, + HOURS { + override fun number(duration: Duration) = MINUTES.number(duration) / 60 + override val symbol = "h" + }, + MINUTES { + override fun number(duration: Duration) = SECONDS.number(duration) / 60 + override val symbol = "m" + }, + SECONDS { + override fun number(duration: Duration) = duration.seconds + duration.nano / 1_000_000_000.0 + override val symbol = "s" + }, + MILLIS { + override fun number(duration: Duration) = duration.nano / 1_000_000.0 + override val symbol = "ms" + }, + MICROS { + override fun number(duration: Duration) = duration.nano / 1_000.0 + override val symbol = "μs" + }, + NANOS { + override fun number(duration: Duration) = duration.nano.toDouble() + override val symbol = "ns" + }; + + abstract fun number(duration: Duration): Double + abstract val symbol: String + +} diff --git a/src/main/kotlin/net/pterodactylus/sone/template/HistogramRenderer.kt b/src/main/kotlin/net/pterodactylus/sone/template/HistogramRenderer.kt new file mode 100644 index 0000000..dba32b2 --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/template/HistogramRenderer.kt @@ -0,0 +1,46 @@ +package net.pterodactylus.sone.template + +import com.codahale.metrics.* +import net.pterodactylus.sone.utils.* +import net.pterodactylus.util.template.* + +/** + * [Filter] that renders a [Histogram] as a table row. + */ +class HistogramRenderer : Filter { + + override fun format(templateContext: TemplateContext, data: Any?, parameters: Map?): Any? { + templateContext["metricName"] = (parameters?.get("name") as String?)?.dotToCamel()?.let { "Page.Metrics.$it.Title" } + (data as? Histogram)?.snapshot?.run { + templateContext["count"] = data.count + templateContext["min"] = min + templateContext["max"] = max + templateContext["mean"] = mean + templateContext["median"] = median + templateContext["percentile75"] = get75thPercentile() + templateContext["percentile95"] = get95thPercentile() + templateContext["percentile98"] = get98thPercentile() + templateContext["percentile99"] = get99thPercentile() + templateContext["percentile999"] = get999thPercentile() + } + return template.render(templateContext) + } + +} + +private val template = """ + <% metricName|l10n|html> + <% count|html> + <% min|duration scale=='μs'|html> + <% max|duration scale=='μs'|html> + <% mean|duration scale=='μs'|html> + <% median|duration scale=='μs'|html> + <% percentile75|duration scale=='μs'|html> + <% percentile95|duration scale=='μs'|html> + <% percentile98|duration scale=='μs'|html> + <% percentile99|duration scale=='μs'|html> + <% percentile999|duration scale=='μs'|html> +""".asTemplate() + +private fun String.dotToCamel() = + split(".").joinToString("", transform = String::capitalize) diff --git a/src/main/kotlin/net/pterodactylus/sone/template/PostAccessor.kt b/src/main/kotlin/net/pterodactylus/sone/template/PostAccessor.kt new file mode 100644 index 0000000..ab7f6ba --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/template/PostAccessor.kt @@ -0,0 +1,55 @@ +/* + * Sone - PostAccessor.kt - Copyright © 2010–2020 David 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 . + */ + +package net.pterodactylus.sone.template + +import net.pterodactylus.sone.core.* +import net.pterodactylus.sone.data.* +import net.pterodactylus.sone.utils.* +import net.pterodactylus.util.template.* + +/** + * Accessor for [Post] objects that adds additional properties: + * + * * `replies`: All replies to this post, sorted by time, oldest first + * * `likes`: All Sones that have liked the post + * * `liked`: `true` if the current Sone from the [template context][TemplateContext] has liked the post + * * `new`: `true` if the post is not known + * * `bookmarked`: `true` if the post is bookmarked + */ +class PostAccessor(private val core: Core) : ReflectionAccessor() { + + override fun get(templateContext: TemplateContext?, `object`: Any?, member: String): Any? = + (`object` as Post).let { post -> + when (member) { + "replies" -> core.getReplies(post) + "likes" -> core.getLikes(post) + "liked" -> templateContext.currentSone?.isLikedPostId(post.id) ?: false + "new" -> !post.isKnown + "bookmarked" -> core.isBookmarked(post) + "replySone" -> core.getReplies(post).lastOrNull { it.sone.isLocal }?.sone + ?: post.recipient.let { it.takeIf { it.isLocal } } + ?: post.sone.takeIf { it.isLocal } + ?: templateContext.currentSone + else -> super.get(templateContext, `object`, member) + } + } + +} + +private fun Core.getReplies(post: Post) = getReplies(post.id).filter { Reply.FUTURE_REPLY_FILTER.apply(it) } +private val TemplateContext?.currentSone: Sone? get() = this?.get("currentSone") as? Sone diff --git a/src/main/kotlin/net/pterodactylus/sone/text/SoneMentionDetector.kt b/src/main/kotlin/net/pterodactylus/sone/text/SoneMentionDetector.kt new file mode 100644 index 0000000..3e15ed3 --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/text/SoneMentionDetector.kt @@ -0,0 +1,94 @@ +/** + * Sone - SoneMentionDetector.kt - Copyright © 2019–2020 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 . + */ + +package net.pterodactylus.sone.text + +import com.google.common.eventbus.* +import net.pterodactylus.sone.core.event.* +import net.pterodactylus.sone.data.* +import net.pterodactylus.sone.database.* +import net.pterodactylus.sone.utils.* +import javax.inject.* + +/** + * Listens to [NewPostFoundEvent]s and [NewPostReplyFoundEvent], parses the + * texts and emits a [MentionOfLocalSoneFoundEvent] if a [SoneTextParser] + * finds a [SonePart] that points to a local [Sone]. + */ +class SoneMentionDetector @Inject constructor(private val eventBus: EventBus, private val soneTextParser: SoneTextParser, private val postReplyProvider: PostReplyProvider) { + + @Subscribe + fun onNewPost(newPostFoundEvent: NewPostFoundEvent) { + newPostFoundEvent.post.let { post -> + post.sone.isLocal.onFalse { + if (post.text.hasLinksToLocalSones()) { + mentionedPosts += post + eventBus.post(MentionOfLocalSoneFoundEvent(post)) + } + } + } + } + + @Subscribe + fun onNewPostReply(event: NewPostReplyFoundEvent) { + event.postReply.let { postReply -> + postReply.sone.isLocal.onFalse { + if (postReply.text.hasLinksToLocalSones()) { + postReply.post + .also { mentionedPosts += it } + .let(::MentionOfLocalSoneFoundEvent) + ?.also(eventBus::post) + } + } + } + } + + @Subscribe + fun onPostRemoved(event: PostRemovedEvent) { + unmentionPost(event.post) + } + + @Subscribe + fun onPostMarkedKnown(event: MarkPostKnownEvent) { + unmentionPost(event.post) + } + + @Subscribe + fun onReplyRemoved(event: PostReplyRemovedEvent) { + event.postReply.post.let { + if ((!it.text.hasLinksToLocalSones() || it.isKnown) && (it.replies.filterNot { it == event.postReply }.none { it.text.hasLinksToLocalSones() && !it.isKnown })) { + unmentionPost(it) + } + } + } + + private fun unmentionPost(post: Post) { + if (post in mentionedPosts) { + eventBus.post(MentionOfLocalSoneRemovedEvent(post)) + mentionedPosts -= post + } + } + + private val mentionedPosts = mutableSetOf() + + private fun String.hasLinksToLocalSones() = soneTextParser.parse(this, null) + .filterIsInstance() + .any { it.sone.isLocal } + + private val Post.replies get() = postReplyProvider.getReplies(id) + +} diff --git a/src/main/kotlin/net/pterodactylus/sone/text/SoneTextParser.kt b/src/main/kotlin/net/pterodactylus/sone/text/SoneTextParser.kt index 554c19b..b711ca8 100644 --- a/src/main/kotlin/net/pterodactylus/sone/text/SoneTextParser.kt +++ b/src/main/kotlin/net/pterodactylus/sone/text/SoneTextParser.kt @@ -7,6 +7,7 @@ import net.pterodactylus.sone.data.impl.* import net.pterodactylus.sone.database.* import net.pterodactylus.sone.text.LinkType.* import net.pterodactylus.sone.text.LinkType.USK +import net.pterodactylus.sone.utils.* import org.bitpedia.util.* import java.net.* import javax.inject.* @@ -71,7 +72,7 @@ class SoneTextParser @Inject constructor(private val soneProvider: SoneProvider? ?.takeIf { (it.size > 1) || ((it.size == 1) && (it.single() != "")) } ?.lastOrNull() ?: uri.docName - ?: "${uri.keyType}@${uri.routingKey.freenetBase64}" + ?: "${uri.keyType}@${uri.routingKey.asFreenetBase64}" }.let { FreenetLinkPart(linkWithoutBacklink.removeSuffix("/"), it, trusted = context?.routingKey?.contentEquals(FreenetURI(linkWithoutBacklink).routingKey) == true) } } catch (e: MalformedURLException) { PlainTextPart(linkWithoutBacklink) @@ -115,7 +116,7 @@ private fun List.mergeAdjacentPlainTextParts() = fold(emptyList()) { private fun List.removeEmptyPlainTextParts() = filterNot { it == PlainTextPart("") } -private val String.decodedId: String get() = Base64.encode(Base32.decode(this)) +private val String.decodedId: String get() = Base32.decode(this).asFreenetBase64 private val String.withoutProtocol get() = substring(indexOf("//") + 2) private val String.withoutUrlParameters get() = split('?').first() @@ -138,7 +139,7 @@ private val String.withoutMiddlePathComponents } private val String.withoutTrailingSlash get() = if (endsWith("/")) substring(0, length - 1) else this private val SoneTextParserContext.routingKey: ByteArray? get() = postingSone?.routingKey -private val Sone.routingKey: ByteArray get() = Base64.decode(id) +private val Sone.routingKey: ByteArray get() = id.fromFreenetBase64 private enum class LinkType(private val scheme: String, private val freenetLink: Boolean) { @@ -199,5 +200,3 @@ private fun isPunctuation(char: Char) = char in punctuationChars private val whitespace = Regex("[\\u000a\u0020\u00a0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u200b\u200c\u200d\u202f\u205f\u2060\u2800\u3000]") private data class NextLink(val position: Int, val linkType: LinkType, val link: String, val remainder: String) - -private val ByteArray.freenetBase64 get() = Base64.encode(this)!! diff --git a/src/main/kotlin/net/pterodactylus/sone/utils/Booleans.kt b/src/main/kotlin/net/pterodactylus/sone/utils/Booleans.kt index bfcb319..1d3e097 100644 --- a/src/main/kotlin/net/pterodactylus/sone/utils/Booleans.kt +++ b/src/main/kotlin/net/pterodactylus/sone/utils/Booleans.kt @@ -9,3 +9,19 @@ fun Boolean.ifTrue(block: () -> R): R? = if (this) block() else null * Returns the value of [block] if `this` is false, returns `null` otherwise. */ fun Boolean.ifFalse(block: () -> R): R? = if (!this) block() else null + +/** + * Returns `this` but runs the given block if `this` is `true`. + * + * @param block The block to run if `this` is `true` + * @return `this` + */ +fun Boolean.onTrue(block: () -> Unit): Boolean = also { if (this) block() } + +/** + * Returns `this` but runs the given block if `this` is `false`. + * + * @param block The block to run if `this` is `false` + * @return `this` + */ +fun Boolean.onFalse(block: () -> Unit): Boolean = this.also { if (!this) block() } diff --git a/src/main/kotlin/net/pterodactylus/sone/utils/Freenet.kt b/src/main/kotlin/net/pterodactylus/sone/utils/Freenet.kt new file mode 100644 index 0000000..2eb8ab0 --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/utils/Freenet.kt @@ -0,0 +1,23 @@ +/** + * Sone - Freenet.kt - Copyright © 2019–2020 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 . + */ + +package net.pterodactylus.sone.utils + +import freenet.support.* + +val ByteArray.asFreenetBase64: String get() = Base64.encode(this) +val String.fromFreenetBase64: ByteArray get() = Base64.decode(this) diff --git a/src/main/kotlin/net/pterodactylus/sone/utils/Functions.kt b/src/main/kotlin/net/pterodactylus/sone/utils/Functions.kt new file mode 100644 index 0000000..99f43b6 --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/utils/Functions.kt @@ -0,0 +1,9 @@ +package net.pterodactylus.sone.utils + +import java.util.function.* + +/** Allows easy invocation of Java Consumers. */ +operator fun Consumer.invoke(t: T) = accept(t) + +/** Allows easy invocation of Java Runnables. */ +operator fun Runnable.invoke() = run() diff --git a/src/main/kotlin/net/pterodactylus/sone/utils/Objects.kt b/src/main/kotlin/net/pterodactylus/sone/utils/Objects.kt index 6d1d413..d9ec162 100644 --- a/src/main/kotlin/net/pterodactylus/sone/utils/Objects.kt +++ b/src/main/kotlin/net/pterodactylus/sone/utils/Objects.kt @@ -5,3 +5,9 @@ val Any?.unit get() = Unit fun T?.throwOnNullIf(throwCondition: Boolean, exception: () -> Throwable) = if (this == null && throwCondition) throw exception() else this + +fun T?.onNull(block: () -> Unit) = this.also { + if (this == null) { + block() + } +} diff --git a/src/main/kotlin/net/pterodactylus/sone/utils/Renderables.kt b/src/main/kotlin/net/pterodactylus/sone/utils/Renderables.kt new file mode 100644 index 0000000..3ba9f8f --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/utils/Renderables.kt @@ -0,0 +1,27 @@ +/** + * Sone - Renderables.kt - Copyright © 2019–2020 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 . + */ + +package net.pterodactylus.sone.utils + +import net.pterodactylus.util.io.* +import java.io.* + +/** + * Renders the [Renderable] into a [String]. + */ +fun Renderable.render() = + StringWriter().use { it.also(::render) }.toString() diff --git a/src/main/kotlin/net/pterodactylus/sone/web/PageToadletRegistry.kt b/src/main/kotlin/net/pterodactylus/sone/web/PageToadletRegistry.kt index 97e6cfe..651bffd 100644 --- a/src/main/kotlin/net/pterodactylus/sone/web/PageToadletRegistry.kt +++ b/src/main/kotlin/net/pterodactylus/sone/web/PageToadletRegistry.kt @@ -18,21 +18,28 @@ class PageToadletRegistry @Inject constructor( ) { private val pages = mutableListOf>() + private val debugPages = mutableListOf>() private val registeredToadlets = mutableListOf() private val registered = AtomicBoolean(false) + private val debugActivated = AtomicBoolean(false) fun addPage(page: Page) { if (registered.get()) throw IllegalStateException() pages += page } + fun addDebugPage(page: Page) { + if (registered.get()) throw IllegalStateException() + debugPages += page + } + fun registerToadlets() { registered.set(true) pageMaker.addNavigationCategory("/Sone/index.html", soneMenuName, "$soneMenu.Tooltip", sonePlugin) addPages() } - private fun addPages() = + private fun addPages(pages: List> = this.pages) = pages .map { pageToadletFactory.createPageToadlet(it) } .onEach(registeredToadlets::plusAssign) @@ -55,4 +62,11 @@ class PageToadletRegistry @Inject constructor( registeredToadlets.forEach(toadletContainer::unregister) } + fun activateDebugMode() { + if (!debugActivated.get()) { + addPages(debugPages) + debugActivated.set(true) + } + } + } diff --git a/src/main/kotlin/net/pterodactylus/sone/web/WebInterfaceModule.kt b/src/main/kotlin/net/pterodactylus/sone/web/WebInterfaceModule.kt index dd5e9f6..3d87aa7 100644 --- a/src/main/kotlin/net/pterodactylus/sone/web/WebInterfaceModule.kt +++ b/src/main/kotlin/net/pterodactylus/sone/web/WebInterfaceModule.kt @@ -1,7 +1,6 @@ package net.pterodactylus.sone.web import com.google.inject.* -import freenet.l10n.* import freenet.support.api.* import net.pterodactylus.sone.core.* import net.pterodactylus.sone.data.* @@ -11,6 +10,7 @@ import net.pterodactylus.sone.freenet.wot.* import net.pterodactylus.sone.main.* import net.pterodactylus.sone.template.* import net.pterodactylus.sone.text.* +import net.pterodactylus.util.notify.* import net.pterodactylus.util.template.* import javax.inject.* import javax.inject.Singleton @@ -65,6 +65,7 @@ class WebInterfaceModule : AbstractModule() { addFilter("reparse", ReparseFilter()) addFilter("unknown", unknownDateFilter) addFilter("format", FormatFilter()) + addFilter("duration", DurationFormatFilter()) addFilter("sort", CollectionSortFilter()) addFilter("image-link", imageLinkFilter) addFilter("replyGroup", ReplyGroupFilter()) @@ -72,6 +73,7 @@ class WebInterfaceModule : AbstractModule() { addFilter("unique", UniqueElementFilter()) addFilter("mod", ModFilter()) addFilter("paginate", PaginationFilter()) + addFilter("render-histogram", HistogramRenderer()) addProvider(TemplateProvider.TEMPLATE_CONTEXT_PROVIDER) addProvider(loaders.templateProvider) @@ -98,8 +100,8 @@ class WebInterfaceModule : AbstractModule() { ProfileAccessor(core) @Provides - fun getL10nFilter(l10n: BaseL10n) = - L10nFilter(l10n) + fun getL10nFilter(translation: Translation) = + L10nFilter(translation) @Provides fun getParserFilter(core: Core, soneTextParser: SoneTextParser) = @@ -114,8 +116,8 @@ class WebInterfaceModule : AbstractModule() { LinkedElementsFilter(elementLoader) @Provides - fun getUnknownDateFilter(l10n: BaseL10n) = - UnknownDateFilter(l10n, "View.Sone.Text.UnknownDate") + fun getUnknownDateFilter(translation: Translation) = + UnknownDateFilter(translation, "View.Sone.Text.UnknownDate") @Provides fun getImageLinkFilter(core: Core) = @@ -125,4 +127,9 @@ class WebInterfaceModule : AbstractModule() { @Named("toadletPathPrefix") fun getPathPrefix(): String = "/Sone/" + @Provides + @Singleton + fun getNotificationManager() = + NotificationManager() + } diff --git a/src/main/kotlin/net/pterodactylus/sone/web/ajax/CreatePostAjaxPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/ajax/CreatePostAjaxPage.kt index 79e66ad..28af3e3 100644 --- a/src/main/kotlin/net/pterodactylus/sone/web/ajax/CreatePostAjaxPage.kt +++ b/src/main/kotlin/net/pterodactylus/sone/web/ajax/CreatePostAjaxPage.kt @@ -19,7 +19,7 @@ class CreatePostAjaxPage @Inject constructor(webInterface: WebInterface) : Logge ?.let { text -> val sender = request.parameters["sender"].emptyToNull?.let(core::getSone) ?: currentSone val recipient = request.parameters["recipient"]?.let(core::getSone) - core.createPost(sender, recipient.asOptional(), text).let { post -> + core.createPost(sender, recipient, text).let { post -> createSuccessJsonObject().apply { put("postId", post.id) put("sone", sender.id) diff --git a/src/main/kotlin/net/pterodactylus/sone/web/ajax/DistrustAjaxPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/ajax/DistrustAjaxPage.kt deleted file mode 100644 index cbeed6e..0000000 --- a/src/main/kotlin/net/pterodactylus/sone/web/ajax/DistrustAjaxPage.kt +++ /dev/null @@ -1,29 +0,0 @@ -package net.pterodactylus.sone.web.ajax - -import net.pterodactylus.sone.core.* -import net.pterodactylus.sone.data.* -import net.pterodactylus.sone.utils.* -import net.pterodactylus.sone.web.* -import net.pterodactylus.sone.web.page.* -import javax.inject.* - -/** - * AJAX page that lets the user distrust a Sone. - * - * @see Core.distrustSone(Sone, Sone) - */ -@ToadletPath("distrustSone.ajax") -class DistrustAjaxPage @Inject constructor(webInterface: WebInterface) : LoggedInJsonPage(webInterface) { - - override fun createJsonObject(currentSone: Sone, request: FreenetRequest) = - request.parameters["sone"] - ?.let(core::getSone) - ?.let { sone -> - createSuccessJsonObject() - .put("trustValue", core.preferences.negativeTrust) - .also { - core.distrustSone(currentSone, sone) - } - } ?: createErrorJsonObject("invalid-sone-id") - -} diff --git a/src/main/kotlin/net/pterodactylus/sone/web/ajax/GetNotificationsAjaxPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/ajax/GetNotificationsAjaxPage.kt index db6e5c5..66c8ed2 100644 --- a/src/main/kotlin/net/pterodactylus/sone/web/ajax/GetNotificationsAjaxPage.kt +++ b/src/main/kotlin/net/pterodactylus/sone/web/ajax/GetNotificationsAjaxPage.kt @@ -3,8 +3,7 @@ package net.pterodactylus.sone.web.ajax import net.pterodactylus.sone.data.Sone import net.pterodactylus.sone.data.SoneOptions import net.pterodactylus.sone.main.SonePlugin -import net.pterodactylus.sone.utils.jsonArray -import net.pterodactylus.sone.utils.jsonObject +import net.pterodactylus.sone.utils.* import net.pterodactylus.sone.web.WebInterface import net.pterodactylus.sone.web.page.* import net.pterodactylus.util.notify.Notification @@ -74,5 +73,3 @@ private val SoneOptions?.asJsonObject "ShowNotification/NewReplies" to options.isShowNewReplyNotifications ) } ?: jsonObject {} - -private fun Notification.render() = StringWriter().use { it.also { render(it) } }.toString() diff --git a/src/main/kotlin/net/pterodactylus/sone/web/ajax/GetTranslationAjaxPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/ajax/GetTranslationAjaxPage.kt index 41188ea..e2c41b6 100644 --- a/src/main/kotlin/net/pterodactylus/sone/web/ajax/GetTranslationAjaxPage.kt +++ b/src/main/kotlin/net/pterodactylus/sone/web/ajax/GetTranslationAjaxPage.kt @@ -16,6 +16,6 @@ class GetTranslationAjaxPage @Inject constructor(webInterface: WebInterface) : J override fun createJsonObject(request: FreenetRequest) = createSuccessJsonObject() - .put("value", webInterface.l10n.getString(request.parameters["key"])) + .put("value", webInterface.translation.translate(request.parameters["key"] ?: "")) } diff --git a/src/main/kotlin/net/pterodactylus/sone/web/ajax/TrustAjaxPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/ajax/TrustAjaxPage.kt deleted file mode 100644 index 9f0de87..0000000 --- a/src/main/kotlin/net/pterodactylus/sone/web/ajax/TrustAjaxPage.kt +++ /dev/null @@ -1,24 +0,0 @@ -package net.pterodactylus.sone.web.ajax - -import net.pterodactylus.sone.data.* -import net.pterodactylus.sone.utils.* -import net.pterodactylus.sone.web.* -import net.pterodactylus.sone.web.page.* -import javax.inject.* - -/** - * AJAX page that lets the user trust a Sone. - * - * @see net.pterodactylus.sone.core.Core.trustSone - */ -@ToadletPath("trustSone.ajax") -class TrustAjaxPage @Inject constructor(webInterface: WebInterface) : LoggedInJsonPage(webInterface) { - - override fun createJsonObject(currentSone: Sone, request: FreenetRequest) = - request.parameters["sone"] - ?.let(core::getSone) - ?.let { core.trustSone(currentSone, it) } - ?.let { createSuccessJsonObject().put("trustValue", core.preferences.positiveTrust) } - ?: createErrorJsonObject("invalid-sone-id") - -} diff --git a/src/main/kotlin/net/pterodactylus/sone/web/ajax/UntrustAjaxPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/ajax/UntrustAjaxPage.kt deleted file mode 100644 index e4b5edb..0000000 --- a/src/main/kotlin/net/pterodactylus/sone/web/ajax/UntrustAjaxPage.kt +++ /dev/null @@ -1,22 +0,0 @@ -package net.pterodactylus.sone.web.ajax - -import net.pterodactylus.sone.data.Sone -import net.pterodactylus.sone.utils.parameters -import net.pterodactylus.sone.web.WebInterface -import net.pterodactylus.sone.web.page.* -import javax.inject.Inject - -/** - * AJAX page that lets the user [untrust][net.pterodactylus.sone.core.Core.untrustSone] a [Sone]. - */ -@ToadletPath("untrustSone.ajax") -class UntrustAjaxPage @Inject constructor(webInterface: WebInterface) : LoggedInJsonPage(webInterface) { - - override fun createJsonObject(currentSone: Sone, request: FreenetRequest) = - request.parameters["sone"] - ?.let(core::getSone) - ?.also { core.untrustSone(currentSone, it) } - ?.let { createSuccessJsonObject() } - ?: createErrorJsonObject("invalid-sone-id") - -} diff --git a/src/main/kotlin/net/pterodactylus/sone/web/notification/ConfigNotReadHandler.kt b/src/main/kotlin/net/pterodactylus/sone/web/notification/ConfigNotReadHandler.kt new file mode 100644 index 0000000..6092311 --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/web/notification/ConfigNotReadHandler.kt @@ -0,0 +1,35 @@ +/** + * Sone - ConfigNotReadHandler.kt - Copyright © 2019–2020 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 . + */ + +package net.pterodactylus.sone.web.notification + +import com.google.common.eventbus.* +import net.pterodactylus.sone.core.event.* +import net.pterodactylus.util.notify.* +import javax.inject.* + +/** + * Handler for [ConfigNotRead] events. + */ +class ConfigNotReadHandler @Inject constructor(private val notificationManager: NotificationManager, @Named("configNotRead") private val notification: TemplateNotification) { + + @Subscribe + fun configNotRead(@Suppress("UNUSED_PARAMETER") configNotRead: ConfigNotRead) { + notificationManager.addNotification(notification) + } + +} diff --git a/src/main/kotlin/net/pterodactylus/sone/web/notification/FirstStartHandler.kt b/src/main/kotlin/net/pterodactylus/sone/web/notification/FirstStartHandler.kt new file mode 100644 index 0000000..3dc8689 --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/web/notification/FirstStartHandler.kt @@ -0,0 +1,35 @@ +/** + * Sone - FirstStartHandler.kt - Copyright © 2019–2020 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 . + */ + +package net.pterodactylus.sone.web.notification + +import com.google.common.eventbus.* +import net.pterodactylus.sone.core.event.* +import net.pterodactylus.util.notify.* +import javax.inject.* + +/** + * Handles the notification shown on first start of Sone. + */ +class FirstStartHandler @Inject constructor(private val notificationManager: NotificationManager, @Named("firstStart") private val notification: TemplateNotification) { + + @Subscribe + fun firstStart(@Suppress("UNUSED_PARAMETER") firstStart: FirstStart) { + notificationManager.addNotification(notification) + } + +} diff --git a/src/main/kotlin/net/pterodactylus/sone/web/notification/ImageInsertHandler.kt b/src/main/kotlin/net/pterodactylus/sone/web/notification/ImageInsertHandler.kt new file mode 100644 index 0000000..bab7599 --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/web/notification/ImageInsertHandler.kt @@ -0,0 +1,66 @@ +/** + * Sone - ImageInsertHandler.kt - Copyright © 2019–2020 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 . + */ + +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 javax.inject.* + +/** + * Notification handler for the various image-insert-related events. + * + * @see ImageInsertStartedEvent + * @see ImageInsertAbortedEvent + * @see ImageInsertFailedEvent + * @see ImageInsertFinishedEvent + */ +class ImageInsertHandler @Inject constructor( + private val notificationManager: NotificationManager, + @Named("imageInserting") private val imageInsertingNotification: ListNotification, + @Named("imageFailed") private val imageFailedNotification: ListNotification, + @Named("imageInserted") private val imageInsertedNotification: ListNotification) { + + @Subscribe + fun imageInsertStarted(imageInsertStartedEvent: ImageInsertStartedEvent) { + imageInsertingNotification.add(imageInsertStartedEvent.image) + notificationManager.addNotification(imageInsertingNotification) + } + + @Subscribe + fun imageInsertAborted(imageInsertAbortedEvent: ImageInsertAbortedEvent) { + imageInsertingNotification.remove(imageInsertAbortedEvent.image) + } + + @Subscribe + fun imageInsertFailed(imageInsertFailedEvent: ImageInsertFailedEvent) { + imageInsertingNotification.remove(imageInsertFailedEvent.image) + imageFailedNotification.add(imageInsertFailedEvent.image) + notificationManager.addNotification(imageFailedNotification) + } + + @Subscribe + fun imageInsertFinished(imageInsertFinishedEvent: ImageInsertFinishedEvent) { + imageInsertingNotification.remove(imageInsertFinishedEvent.image) + imageInsertedNotification.add(imageInsertFinishedEvent.image) + notificationManager.addNotification(imageInsertedNotification) + } + +} diff --git a/src/main/kotlin/net/pterodactylus/sone/web/notification/LocalPostHandler.kt b/src/main/kotlin/net/pterodactylus/sone/web/notification/LocalPostHandler.kt new file mode 100644 index 0000000..e7b8f05 --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/web/notification/LocalPostHandler.kt @@ -0,0 +1,59 @@ +/** + * Sone - LocalPostHandler.kt - Copyright © 2019–2020 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 . + */ + +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 javax.inject.* + +/** + * Handler for local posts. + */ +class LocalPostHandler @Inject constructor(private val notificationManager: NotificationManager, @Named("localPost") private val notification: ListNotification) { + + @Subscribe + fun newPostFound(newPostFoundEvent: NewPostFoundEvent) { + newPostFoundEvent.post.onLocal { post -> + notification.add(post) + if (!notificationManager.hasFirstStartNotification()) { + notificationManager.addNotification(notification) + } + } + } + + @Subscribe + fun postRemoved(postRemovedEvent: PostRemovedEvent) { + postRemovedEvent.post.onLocal { post -> + notification.remove(post) + } + } + + @Subscribe + fun postMarkedAsKnown(markPostKnownEvent: MarkPostKnownEvent) { + markPostKnownEvent.post.onLocal { post -> + notification.remove(post) + } + } + +} + +private fun Post.onLocal(action: (Post) -> Unit) = + if (sone.isLocal) action(this) else Unit diff --git a/src/main/kotlin/net/pterodactylus/sone/web/notification/LocalReplyHandler.kt b/src/main/kotlin/net/pterodactylus/sone/web/notification/LocalReplyHandler.kt new file mode 100644 index 0000000..7e392d0 --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/web/notification/LocalReplyHandler.kt @@ -0,0 +1,54 @@ +/** + * Sone - LocalReplyHandler.kt - Copyright © 2019–2020 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 . + */ + +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 javax.inject.* + +/** + * Handler for local replies. + */ +class LocalReplyHandler @Inject constructor(private val notificationManager: NotificationManager, @Named("localReply") private val notification: ListNotification) { + + @Subscribe + fun newReplyFound(event: NewPostReplyFoundEvent) = + event.postReply.onLocal { + notification.add(it) + if (!notificationManager.hasFirstStartNotification()) { + notificationManager.addNotification(notification) + } + } + + @Subscribe + fun replyRemoved(event: PostReplyRemovedEvent) { + notification.remove(event.postReply) + } + + @Subscribe + fun replyMarkedAsKnown(event: MarkPostReplyKnownEvent) { + notification.remove(event.postReply) + } + +} + +private fun PostReply.onLocal(action: (PostReply) -> Unit) = + if (sone.isLocal) action(this) else Unit diff --git a/src/main/kotlin/net/pterodactylus/sone/web/notification/MarkPostKnownDuringFirstStartHandler.kt b/src/main/kotlin/net/pterodactylus/sone/web/notification/MarkPostKnownDuringFirstStartHandler.kt new file mode 100644 index 0000000..57962c2 --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/web/notification/MarkPostKnownDuringFirstStartHandler.kt @@ -0,0 +1,43 @@ +/** + * Sone - MarkPostKnownDuringFirstStartHandler.kt - Copyright © 2019–2020 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 . + */ + +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.sone.utils.* +import net.pterodactylus.util.notify.* +import java.util.function.* +import javax.inject.* + +/** + * Handler that marks a [new][NewPostFoundEvent] [post][Post] as known while + * the [notification manager][NotificationManager] shows a [first start notification] + * [NotificationManager.hasFirstStartNotification]. + */ +class MarkPostKnownDuringFirstStartHandler @Inject constructor(private val notificationManager: NotificationManager, private val markPostAsKnown: Consumer) { + + @Subscribe + fun newPostFound(newPostFoundEvent: NewPostFoundEvent) { + if (notificationManager.hasFirstStartNotification()) { + markPostAsKnown(newPostFoundEvent.post) + } + } + +} diff --git a/src/main/kotlin/net/pterodactylus/sone/web/notification/MarkPostReplyKnownDuringFirstStartHandler.kt b/src/main/kotlin/net/pterodactylus/sone/web/notification/MarkPostReplyKnownDuringFirstStartHandler.kt new file mode 100644 index 0000000..64285ee --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/web/notification/MarkPostReplyKnownDuringFirstStartHandler.kt @@ -0,0 +1,43 @@ +/** + * Sone - MarkPostReplyKnownDuringFirstStartHandler.kt - Copyright © 2020 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 . + */ + +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.sone.utils.* +import net.pterodactylus.util.notify.* +import java.util.function.* +import javax.inject.* + +/** + * Handler that marks post replies [as known][net.pterodactylus.sone.core.Core.markReplyKnown] + * while the [first start notification][net.pterodactylus.util.notify.NotificationManager.hasFirstStartNotification] + * is shown. + */ +class MarkPostReplyKnownDuringFirstStartHandler @Inject constructor(private val notificationManager: NotificationManager, private val markAsKnown: Consumer) { + + @Subscribe + fun newPostReply(event: NewPostReplyFoundEvent) { + if (notificationManager.hasFirstStartNotification()) { + markAsKnown(event.postReply) + } + } + +} diff --git a/src/main/kotlin/net/pterodactylus/sone/web/notification/NewRemotePostHandler.kt b/src/main/kotlin/net/pterodactylus/sone/web/notification/NewRemotePostHandler.kt new file mode 100644 index 0000000..91eaae0 --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/web/notification/NewRemotePostHandler.kt @@ -0,0 +1,53 @@ +/** + * Sone - NewRemotePostHandler.kt - Copyright © 2019–2020 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 . + */ + +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 javax.inject.* + +/** + * Handler for [NewPostFoundEvent]s that adds the new post to the “new posts” notification and + * displays the notification if the “first start” notification is not being shown. + */ +class NewRemotePostHandler @Inject constructor(private val notificationManager: NotificationManager, @Named("newRemotePost") private val notification: ListNotification) { + + @Subscribe + fun newPostFound(newPostFoundEvent: NewPostFoundEvent) { + if (!newPostFoundEvent.post.sone.isLocal) { + notification.add(newPostFoundEvent.post) + if (!notificationManager.hasFirstStartNotification()) { + notificationManager.addNotification(notification) + } + } + } + + @Subscribe + fun postRemoved(event: PostRemovedEvent) { + notification.remove(event.post) + } + + @Subscribe + fun postMarkedKnown(event: MarkPostKnownEvent) { + notification.remove(event.post) + } + +} diff --git a/src/main/kotlin/net/pterodactylus/sone/web/notification/NewSoneHandler.kt b/src/main/kotlin/net/pterodactylus/sone/web/notification/NewSoneHandler.kt new file mode 100644 index 0000000..eb384c7 --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/web/notification/NewSoneHandler.kt @@ -0,0 +1,50 @@ +/** + * Sone - NewSoneHandler.kt - Copyright © 2019–2020 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 . + */ + +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 javax.inject.* + +/** + * Notification handler for “new Sone discovered” events. + */ +class NewSoneHandler @Inject constructor(private val notificationManager: NotificationManager, @Named("newSone") private val notification: ListNotification) { + + @Subscribe + fun newSoneFound(newSoneFoundEvent: NewSoneFoundEvent) { + if (!notificationManager.hasFirstStartNotification()) { + notification.add(newSoneFoundEvent.sone) + notificationManager.addNotification(notification) + } + } + + @Subscribe + fun markedSoneKnown(markSoneKnownEvent: MarkSoneKnownEvent) { + notification.remove(markSoneKnownEvent.sone) + } + + @Subscribe + fun soneRemoved(soneRemovedEvent: SoneRemovedEvent) { + notification.remove(soneRemovedEvent.sone) + } + +} diff --git a/src/main/kotlin/net/pterodactylus/sone/web/notification/NewVersionHandler.kt b/src/main/kotlin/net/pterodactylus/sone/web/notification/NewVersionHandler.kt new file mode 100644 index 0000000..f48d89c --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/web/notification/NewVersionHandler.kt @@ -0,0 +1,39 @@ +/** + * Sone - NewVersionHandler.kt - Copyright © 2019–2020 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 . + */ + +package net.pterodactylus.sone.web.notification + +import com.google.common.eventbus.* +import net.pterodactylus.sone.core.event.* +import net.pterodactylus.util.notify.* +import javax.inject.* + +/** + * Handler for the “new version” notification. + */ +class NewVersionHandler @Inject constructor(private val notificationManager: NotificationManager, @Named("newVersion") private val notification: TemplateNotification) { + + @Subscribe + fun newVersionFound(updateFoundEvent: UpdateFoundEvent) { + notification.set("latestVersion", updateFoundEvent.version) + notification.set("releaseTime", updateFoundEvent.releaseTime) + notification.set("latestEdition", updateFoundEvent.latestEdition) + notification.set("disruptive", updateFoundEvent.isDisruptive) + notificationManager.addNotification(notification) + } + +} diff --git a/src/main/kotlin/net/pterodactylus/sone/web/notification/NotificationHandler.kt b/src/main/kotlin/net/pterodactylus/sone/web/notification/NotificationHandler.kt new file mode 100644 index 0000000..a24a6d8 --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/web/notification/NotificationHandler.kt @@ -0,0 +1,49 @@ +/** + * Sone - NotificationHandler.kt - Copyright © 2019–2020 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 . + */ + +package net.pterodactylus.sone.web.notification + +import net.pterodactylus.sone.freenet.wot.* +import net.pterodactylus.sone.text.* +import javax.inject.* + +/** + * Container that causes notification handlers to be created and (more importantly) registered + * on creation with the event bus. + */ +@Suppress("UNUSED_PARAMETER") +class NotificationHandler @Inject constructor( + markPostKnownDuringFirstStartHandler: MarkPostKnownDuringFirstStartHandler, + markPostReplyKnownDuringFirstStartHandler: MarkPostReplyKnownDuringFirstStartHandler, + newSoneHandler: NewSoneHandler, + newRemotePostHandler: NewRemotePostHandler, + remotePostReplyHandler: RemotePostReplyHandler, + soneLockedOnStartupHandler: SoneLockedOnStartupHandler, + soneLockedHandler: SoneLockedHandler, + localPostHandler: LocalPostHandler, + localReplyHandler: LocalReplyHandler, + newVersionHandler: NewVersionHandler, + imageInsertHandler: ImageInsertHandler, + firstStartHandler: FirstStartHandler, + configNotReadHandler: ConfigNotReadHandler, + startupHandler: StartupHandler, + webOfTrustPinger: WebOfTrustPinger, + webOfTrustHandler: WebOfTrustHandler, + soneMentionDetector: SoneMentionDetector, + soneMentionedHandler: SoneMentionedHandler, + soneInsertHandler: SoneInsertHandler +) diff --git a/src/main/kotlin/net/pterodactylus/sone/web/notification/NotificationHandlerModule.kt b/src/main/kotlin/net/pterodactylus/sone/web/notification/NotificationHandlerModule.kt new file mode 100644 index 0000000..18aefd6 --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/web/notification/NotificationHandlerModule.kt @@ -0,0 +1,192 @@ +/** + * Sone - NotificationHandlerModule.kt - Copyright © 2019–2020 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 . + */ + +package net.pterodactylus.sone.web.notification + +import com.google.inject.* +import com.google.inject.binder.* +import net.pterodactylus.sone.core.* +import net.pterodactylus.sone.data.* +import net.pterodactylus.sone.freenet.wot.* +import net.pterodactylus.sone.main.* +import net.pterodactylus.sone.notify.* +import net.pterodactylus.sone.text.* +import net.pterodactylus.util.notify.* +import java.util.concurrent.* +import java.util.concurrent.TimeUnit.* +import java.util.function.* +import javax.inject.* +import javax.inject.Singleton + +/** + * Guice module for creating all notification handlers. + */ +class NotificationHandlerModule : AbstractModule() { + + override fun configure() { + bind(NotificationHandler::class.java).`in`(Singleton::class.java) + bind().asSingleton() + bind().asSingleton() + bind().asSingleton() + bind().asSingleton() + bind().asSingleton() + bind().asSingleton() + bind().asSingleton() + bind().asSingleton() + bind().asSingleton() + bind().asSingleton() + bind().asSingleton() + bind().asSingleton() + bind().asSingleton() + bind().asSingleton() + bind().asSingleton() + bind().asSingleton() + bind().asSingleton() + bind().asSingleton() + } + + @Provides + fun getMarkPostKnownHandler(core: Core): Consumer = Consumer { core.markPostKnown(it) } + + @Provides + fun getMarkPostReplyKnownHandler(core: Core): Consumer = Consumer { core.markReplyKnown(it) } + + @Provides + @Singleton + @Named("soneLockedOnStartup") + fun getSoneLockedOnStartupNotification(loaders: Loaders) = + ListNotification("sone-locked-on-startup", "sones", loaders.loadTemplate("/templates/notify/soneLockedOnStartupNotification.html")) + + @Provides + @Named("newSone") + fun getNewSoneNotification(loaders: Loaders) = + ListNotification("new-sone-notification", "sones", loaders.loadTemplate("/templates/notify/newSoneNotification.html"), dismissable = false) + + @Provides + @Singleton + @Named("newRemotePost") + fun getNewPostNotification(loaders: Loaders) = + ListNotification("new-post-notification", "posts", loaders.loadTemplate("/templates/notify/newPostNotification.html"), dismissable = false) + + @Provides + @Singleton + @Named("newRemotePostReply") + fun getNewRemotePostReplyNotification(loaders: Loaders) = + ListNotification("new-reply-notification", "replies", loaders.loadTemplate("/templates/notify/newReplyNotification.html"), dismissable = false) + + @Provides + @Singleton + @Named("soneLocked") + fun getSoneLockedNotification(loaders: Loaders) = + ListNotification("sones-locked-notification", "sones", loaders.loadTemplate("/templates/notify/lockedSonesNotification.html"), dismissable = true) + + @Provides + @Singleton + @Named("localPost") + fun getLocalPostNotification(loaders: Loaders) = + ListNotification("local-post-notification", "posts", loaders.loadTemplate("/templates/notify/newPostNotification.html"), dismissable = false) + + @Provides + @Singleton + @Named("localReply") + fun getLocalReplyNotification(loaders: Loaders) = + ListNotification("local-reply-notification", "replies", loaders.loadTemplate("/templates/notify/newReplyNotification.html"), dismissable = false) + + @Provides + @Singleton + @Named("newVersion") + fun getNewVersionNotification(loaders: Loaders) = + TemplateNotification("new-version-notification", loaders.loadTemplate("/templates/notify/newVersionNotification.html")) + + @Provides + @Singleton + @Named("imageInserting") + fun getImageInsertingNotification(loaders: Loaders) = + ListNotification("inserting-images-notification", "images", loaders.loadTemplate("/templates/notify/inserting-images-notification.html"), dismissable = true) + + @Provides + @Singleton + @Named("imageFailed") + fun getImageInsertingFailedNotification(loaders: Loaders) = + ListNotification("image-insert-failed-notification", "images", loaders.loadTemplate("/templates/notify/image-insert-failed-notification.html"), dismissable = true) + + @Provides + @Singleton + @Named("imageInserted") + fun getImageInsertedNotification(loaders: Loaders) = + ListNotification("inserted-images-notification", "images", loaders.loadTemplate("/templates/notify/inserted-images-notification.html"), dismissable = true) + + @Provides + @Singleton + @Named("firstStart") + fun getFirstStartNotification(loaders: Loaders) = + TemplateNotification("first-start-notification", loaders.loadTemplate("/templates/notify/firstStartNotification.html")) + + @Provides + @Singleton + @Named("configNotRead") + fun getConfigNotReadNotification(loaders: Loaders) = + TemplateNotification("config-not-read-notification", loaders.loadTemplate("/templates/notify/configNotReadNotification.html")) + + @Provides + @Singleton + @Named("startup") + fun getStartupNotification(loaders: Loaders) = + TemplateNotification("startup-notification", loaders.loadTemplate("/templates/notify/startupNotification.html")) + + @Provides + @Singleton + @Named("webOfTrust") + fun getWebOfTrustNotification(loaders: Loaders) = + TemplateNotification("wot-missing-notification", loaders.loadTemplate("/templates/notify/wotMissingNotification.html")) + + @Provides + @Singleton + @Named("webOfTrustReacher") + fun getWebOfTrustReacher(webOfTrustConnector: WebOfTrustConnector): Runnable = + Runnable { webOfTrustConnector.ping() } + + @Provides + @Singleton + @Named("webOfTrustReschedule") + fun getWebOfTrustReschedule(@Named("notification") ticker: ScheduledExecutorService) = + Consumer { ticker.schedule(it, 15, SECONDS) } + + @Provides + @Singleton + @Named("soneMentioned") + fun getSoneMentionedNotification(loaders: Loaders) = + ListNotification("mention-notification", "posts", loaders.loadTemplate("/templates/notify/mentionNotification.html"), dismissable = false) + + @Provides + @Singleton + fun getSoneNotificationSupplier(loaders: Loaders): SoneInsertNotificationSupplier = + mutableMapOf() + .let { cache -> + { sone -> + cache.computeIfAbsent(sone) { + loaders.loadTemplate("/templates/notify/soneInsertNotification.html") + .let(::TemplateNotification) + .also { it["insertSone"] = sone } + } + } + } + + private inline fun bind(): AnnotatedBindingBuilder = bind(T::class.java) + private fun ScopedBindingBuilder.asSingleton() = `in`(Singleton::class.java) + +} diff --git a/src/main/kotlin/net/pterodactylus/sone/web/notification/RemotePostReplyHandler.kt b/src/main/kotlin/net/pterodactylus/sone/web/notification/RemotePostReplyHandler.kt new file mode 100644 index 0000000..7dec7c9 --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/web/notification/RemotePostReplyHandler.kt @@ -0,0 +1,55 @@ +/** + * Sone - RemotePostReplyHandler.kt - Copyright © 2020 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 . + */ + +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.sone.utils.* +import net.pterodactylus.util.notify.* +import javax.inject.* + +/** + * Handler for remote replies. + */ +class RemotePostReplyHandler @Inject constructor(private val notificationManager: NotificationManager, @Named("newRemotePostReply") private val notification: ListNotification) { + + @Subscribe + fun newPostReplyFound(event: NewPostReplyFoundEvent) { + event.postReply.let { postReply -> + postReply.sone.isLocal.onFalse { + if (!notificationManager.hasFirstStartNotification()) { + notification.add(event.postReply) + notificationManager.addNotification(notification) + } + } + } + } + + @Subscribe + fun postReplyRemoved(event: PostReplyRemovedEvent) { + notification.remove(event.postReply) + } + + @Subscribe + fun postReplyMarkedAsKnown(event: MarkPostReplyKnownEvent) { + notification.remove(event.postReply) + } + +} diff --git a/src/main/kotlin/net/pterodactylus/sone/web/notification/SoneInsertHandler.kt b/src/main/kotlin/net/pterodactylus/sone/web/notification/SoneInsertHandler.kt new file mode 100644 index 0000000..b12acb4 --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/web/notification/SoneInsertHandler.kt @@ -0,0 +1,58 @@ +/** + * Sone - SoneInsertHandler.kt - Copyright © 2020 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 . + */ + +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.util.notify.* +import javax.inject.* + +/** + * Handler for all notifications concerning Sone-insert events. + */ +class SoneInsertHandler @Inject constructor(private val notificationManager: NotificationManager, private val soneNotifications: SoneInsertNotificationSupplier) { + + @Subscribe + fun soneInserting(event: SoneInsertingEvent) { + showNotification(event.sone, "inserting") + } + + @Subscribe + fun soneInserted(event: SoneInsertedEvent) { + showNotification(event.sone, "inserted", "insertDuration" to event.insertDuration / 1000) + } + + @Subscribe + fun soneInsertAborted(event: SoneInsertAbortedEvent) { + showNotification(event.sone, "insert-aborted") + } + + private fun showNotification(sone: Sone, status: String, vararg templateVariables: Pair) { + if (sone.options.isSoneInsertNotificationEnabled) { + soneNotifications(sone).let { notification -> + notification["soneStatus"] = status + templateVariables.forEach { notification[it.first] = it.second } + notificationManager.addNotification(notification) + } + } + } + +} + +typealias SoneInsertNotificationSupplier = (@JvmSuppressWildcards Sone) -> @JvmSuppressWildcards TemplateNotification 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 index 0000000..cb799a8 --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/web/notification/SoneLockedHandler.kt @@ -0,0 +1,65 @@ +/** + * Sone - SoneLockedHandler.kt - Copyright © 2019–2020 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 . + */ + +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.* +import javax.inject.* + +/** + * Handler for [SoneLockedEvent]s and [SoneUnlockedEvent]s that can schedule notifications after + * a certain timeout. + */ +class SoneLockedHandler @Inject constructor( + private val notificationManager: NotificationManager, + @Named("soneLocked") private val notification: ListNotification, + @Named("notification") private val executor: ScheduledExecutorService) { + + private val future: AtomicReference> = AtomicReference() + + @Subscribe + fun soneLocked(soneLockedEvent: SoneLockedEvent) { + synchronized(future) { + notification.add(soneLockedEvent.sone) + future.get()?.also(this::cancelPreviousFuture) + future.set(executor.schedule(::showNotification, 5, TimeUnit.MINUTES)) + } + } + + @Subscribe + fun soneUnlocked(soneUnlockedEvent: SoneUnlockedEvent) { + synchronized(future) { + notification.remove(soneUnlockedEvent.sone) + future.get()?.also(::cancelPreviousFuture) + } + } + + private fun cancelPreviousFuture(future: ScheduledFuture<*>) { + future.cancel(true) + } + + private fun showNotification() { + notificationManager.addNotification(notification) + } + +} diff --git a/src/main/kotlin/net/pterodactylus/sone/web/notification/SoneLockedOnStartupHandler.kt b/src/main/kotlin/net/pterodactylus/sone/web/notification/SoneLockedOnStartupHandler.kt new file mode 100644 index 0000000..8adea76 --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/web/notification/SoneLockedOnStartupHandler.kt @@ -0,0 +1,40 @@ +/** + * Sone - SoneLockedOnStartupHandler.kt - Copyright © 2019–2020 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 . + */ + +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 javax.inject.* + +/** + * Handler for [SoneLockedOnStartup][net.pterodactylus.sone.core.event.SoneLockedOnStartup] events + * that adds the appropriate notification to the [NotificationManager]. + */ +class SoneLockedOnStartupHandler @Inject constructor(private val notificationManager: NotificationManager, @Named("soneLockedOnStartup") private val notification: ListNotification) { + + @Subscribe + @Suppress("UnstableApiUsage") + fun soneLockedOnStartup(soneLockedOnStartup: SoneLockedOnStartup) { + notification.add(soneLockedOnStartup.sone) + notificationManager.addNotification(notification) + } + +} diff --git a/src/main/kotlin/net/pterodactylus/sone/web/notification/SoneMentionedHandler.kt b/src/main/kotlin/net/pterodactylus/sone/web/notification/SoneMentionedHandler.kt new file mode 100644 index 0000000..a03e490 --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/web/notification/SoneMentionedHandler.kt @@ -0,0 +1,47 @@ +/** + * Sone - SoneMentionedHandler.kt - Copyright © 2020 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 . + */ + +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 javax.inject.* + +/** + * Handler for the [MentionOfLocalSoneFoundEvent] and + * [MentionOfLocalSoneRemovedEvent] events that add the corresponding + * notification to the notification manager. + */ +class SoneMentionedHandler @Inject constructor(private val notificationManager: NotificationManager, @Named("soneMentioned") private val notification: ListNotification) { + + @Subscribe + fun mentionOfLocalSoneFound(event: MentionOfLocalSoneFoundEvent) { + if (!notificationManager.hasFirstStartNotification()) { + notification.add(event.post) + notificationManager.addNotification(notification) + } + } + + @Subscribe + fun mentionOfLocalSoneRemoved(event: MentionOfLocalSoneRemovedEvent) { + notification.remove(event.post) + } + +} diff --git a/src/main/kotlin/net/pterodactylus/sone/web/notification/StartupHandler.kt b/src/main/kotlin/net/pterodactylus/sone/web/notification/StartupHandler.kt new file mode 100644 index 0000000..3776d43 --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/web/notification/StartupHandler.kt @@ -0,0 +1,41 @@ +/** + * Sone - StartupHandler.kt - Copyright © 2019–2020 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 . + */ + +package net.pterodactylus.sone.web.notification + +import com.google.common.eventbus.* +import net.pterodactylus.sone.core.event.* +import net.pterodactylus.util.notify.* +import java.util.concurrent.* +import java.util.concurrent.TimeUnit.* +import javax.inject.* + +/** + * Handler for the [Startup] event notification. + */ +class StartupHandler @Inject constructor( + private val notificationManager: NotificationManager, + @Named("startup") private val notification: TemplateNotification, + @Named("notification") private val ticker: ScheduledExecutorService) { + + @Subscribe + fun startup(@Suppress("UNUSED_PARAMETER") startup: Startup) { + notificationManager.addNotification(notification) + ticker.schedule({ notificationManager.removeNotification(notification) }, 2, MINUTES) + } + +} diff --git a/src/main/kotlin/net/pterodactylus/sone/web/notification/WebOfTrustHandler.kt b/src/main/kotlin/net/pterodactylus/sone/web/notification/WebOfTrustHandler.kt new file mode 100644 index 0000000..900a885 --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/web/notification/WebOfTrustHandler.kt @@ -0,0 +1,41 @@ +/** + * Sone - WebOfTrustHandler.kt - Copyright © 2019–2020 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 . + */ + +package net.pterodactylus.sone.web.notification + +import com.google.common.eventbus.* +import net.pterodactylus.sone.core.event.* +import net.pterodactylus.util.notify.* +import javax.inject.* + +/** + * Handler for web of trust-related notifications and the [WebOfTrustAppeared] + * and [WebOfTrustDisappeared] events. + */ +class WebOfTrustHandler @Inject constructor(private val notificationManager: NotificationManager, @Named("webOfTrust") private val notification: TemplateNotification) { + + @Subscribe + fun webOfTrustAppeared(@Suppress("UNUSED_PARAMETER") webOfTrustAppeared: WebOfTrustAppeared) { + notificationManager.removeNotification(notification) + } + + @Subscribe + fun webOfTrustDisappeared(@Suppress("UNUSED_PARAMETER") webOfTrustDisappeared: WebOfTrustDisappeared) { + notificationManager.addNotification(notification) + } + +} diff --git a/src/main/kotlin/net/pterodactylus/sone/web/page/FreenetPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/page/FreenetPage.kt index 2af0eca..68875d6 100644 --- a/src/main/kotlin/net/pterodactylus/sone/web/page/FreenetPage.kt +++ b/src/main/kotlin/net/pterodactylus/sone/web/page/FreenetPage.kt @@ -1,5 +1,5 @@ /* - * Sone - FreenetPage.kt - Copyright © 2011–2019 David Roden + * Sone - FreenetPage.kt - Copyright © 2011–2020 David 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 diff --git a/src/main/kotlin/net/pterodactylus/sone/web/page/FreenetRequest.kt b/src/main/kotlin/net/pterodactylus/sone/web/page/FreenetRequest.kt index 54c8f7f..38e0100 100644 --- a/src/main/kotlin/net/pterodactylus/sone/web/page/FreenetRequest.kt +++ b/src/main/kotlin/net/pterodactylus/sone/web/page/FreenetRequest.kt @@ -1,5 +1,5 @@ /* - * Sone - FreenetRequest.kt - Copyright © 2011–2019 David Roden + * Sone - FreenetRequest.kt - Copyright © 2011–2020 David 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 @@ -19,8 +19,8 @@ package net.pterodactylus.sone.web.page import freenet.clients.http.* import freenet.clients.http.SessionManager.* -import freenet.l10n.* import freenet.support.api.* +import net.pterodactylus.sone.freenet.* import net.pterodactylus.util.web.* import java.net.* import java.util.UUID.* @@ -28,7 +28,6 @@ import java.util.UUID.* open class FreenetRequest(uri: URI, method: Method, val httpRequest: HTTPRequest, val toadletContext: ToadletContext, - val l10n: BaseL10n, val sessionManager: SessionManager ) : Request(uri, method) { diff --git a/src/main/kotlin/net/pterodactylus/sone/web/page/FreenetTemplatePage.kt b/src/main/kotlin/net/pterodactylus/sone/web/page/FreenetTemplatePage.kt index 2dcef51..3912f98 100644 --- a/src/main/kotlin/net/pterodactylus/sone/web/page/FreenetTemplatePage.kt +++ b/src/main/kotlin/net/pterodactylus/sone/web/page/FreenetTemplatePage.kt @@ -1,5 +1,5 @@ /* - * Sone - FreenetTemplatePage.kt - Copyright © 2010–2019 David Roden + * Sone - FreenetTemplatePage.kt - Copyright © 2010–2020 David 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 @@ -98,6 +98,9 @@ open class FreenetTemplatePage( /* do nothing. */ } + fun redirectTo(target: String?): Nothing = + throw RedirectException(target) + class RedirectException(val target: String?) : Exception() { override fun toString(): String = format("RedirectException{target='%s'}", target) } diff --git a/src/main/kotlin/net/pterodactylus/sone/web/page/PageToadletFactory.kt b/src/main/kotlin/net/pterodactylus/sone/web/page/PageToadletFactory.kt index 3c84c09..4c671ad 100644 --- a/src/main/kotlin/net/pterodactylus/sone/web/page/PageToadletFactory.kt +++ b/src/main/kotlin/net/pterodactylus/sone/web/page/PageToadletFactory.kt @@ -1,5 +1,5 @@ /* - * Sone - PageToadletFactory.kt - Copyright © 2010–2019 David Roden + * Sone - PageToadletFactory.kt - Copyright © 2010–2020 David 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 diff --git a/src/main/kotlin/net/pterodactylus/sone/web/page/SoneRequest.kt b/src/main/kotlin/net/pterodactylus/sone/web/page/SoneRequest.kt index d3eed37..703f953 100644 --- a/src/main/kotlin/net/pterodactylus/sone/web/page/SoneRequest.kt +++ b/src/main/kotlin/net/pterodactylus/sone/web/page/SoneRequest.kt @@ -1,16 +1,16 @@ package net.pterodactylus.sone.web.page import freenet.clients.http.* -import freenet.l10n.* import freenet.support.api.* import net.pterodactylus.sone.core.* import net.pterodactylus.sone.web.* import net.pterodactylus.util.web.* import java.net.* -class SoneRequest(uri: URI, method: Method, httpRequest: HTTPRequest, toadletContext: ToadletContext, l10n: BaseL10n, sessionManager: SessionManager, - val core: Core, - val webInterface: WebInterface -) : FreenetRequest(uri, method, httpRequest, toadletContext, l10n, sessionManager) +class SoneRequest(uri: URI, method: Method, httpRequest: HTTPRequest, toadletContext: ToadletContext, sessionManager: SessionManager, + val core: Core, + val webInterface: WebInterface +) : FreenetRequest(uri, method, httpRequest, toadletContext, sessionManager) -fun FreenetRequest.toSoneRequest(core: Core, webInterface: WebInterface) = SoneRequest(uri, method, httpRequest, toadletContext, l10n, sessionManager, core, webInterface) +fun FreenetRequest.toSoneRequest(core: Core, webInterface: WebInterface) = + SoneRequest(uri, method, httpRequest, toadletContext, sessionManager, core, webInterface) diff --git a/src/main/kotlin/net/pterodactylus/sone/web/pages/BookmarkPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/pages/BookmarkPage.kt index 1dc12e4..c03a299 100644 --- a/src/main/kotlin/net/pterodactylus/sone/web/pages/BookmarkPage.kt +++ b/src/main/kotlin/net/pterodactylus/sone/web/pages/BookmarkPage.kt @@ -21,7 +21,7 @@ class BookmarkPage @Inject constructor(webInterface: WebInterface, loaders: Load soneRequest.core.getPost(postId)?.let { soneRequest.core.bookmarkPost(it) } - throw RedirectException(returnPage) + redirectTo(returnPage) } } diff --git a/src/main/kotlin/net/pterodactylus/sone/web/pages/CreateAlbumPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/pages/CreateAlbumPage.kt index a00b4bf..b6ce673 100644 --- a/src/main/kotlin/net/pterodactylus/sone/web/pages/CreateAlbumPage.kt +++ b/src/main/kotlin/net/pterodactylus/sone/web/pages/CreateAlbumPage.kt @@ -35,10 +35,10 @@ class CreateAlbumPage @Inject constructor(webInterface: WebInterface, loaders: L setDescription(TextFilter.filter(soneRequest.httpRequest.getHeader("Host"), description)) }.update() } catch (e: AlbumTitleMustNotBeEmpty) { - throw RedirectException("emptyAlbumTitle.html") + redirectTo("emptyAlbumTitle.html") } soneRequest.core.touchConfiguration() - throw RedirectException("imageBrowser.html?album=${album.id}") + redirectTo("imageBrowser.html?album=${album.id}") } } diff --git a/src/main/kotlin/net/pterodactylus/sone/web/pages/CreatePostPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/pages/CreatePostPage.kt index b47c2b1..515de60 100644 --- a/src/main/kotlin/net/pterodactylus/sone/web/pages/CreatePostPage.kt +++ b/src/main/kotlin/net/pterodactylus/sone/web/pages/CreatePostPage.kt @@ -28,8 +28,8 @@ class CreatePostPage @Inject constructor(webInterface: WebInterface, loaders: Lo } val sender = soneRequest.core.getLocalSone(soneRequest.httpRequest.getPartAsStringFailsafe("sender", 43)) ?: currentSone val recipient = soneRequest.core.getSone(soneRequest.httpRequest.getPartAsStringFailsafe("recipient", 43)) - soneRequest.core.createPost(sender, recipient.asOptional(), TextFilter.filter(soneRequest.httpRequest.getHeader("Host"), text)) - throw RedirectException(returnPage) + soneRequest.core.createPost(sender, recipient, TextFilter.filter(soneRequest.httpRequest.getHeader("Host"), text)) + redirectTo(returnPage) } } diff --git a/src/main/kotlin/net/pterodactylus/sone/web/pages/CreateReplyPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/pages/CreateReplyPage.kt index 562e647..7714b77 100644 --- a/src/main/kotlin/net/pterodactylus/sone/web/pages/CreateReplyPage.kt +++ b/src/main/kotlin/net/pterodactylus/sone/web/pages/CreateReplyPage.kt @@ -26,10 +26,10 @@ class CreateReplyPage @Inject constructor(webInterface: WebInterface, loaders: L templateContext["errorTextEmpty"] = true return } - val post = soneRequest.core.getPost(postId) ?: throw RedirectException("noPermission.html") + val post = soneRequest.core.getPost(postId) ?: redirectTo("noPermission.html") val sender = soneRequest.core.getLocalSone(soneRequest.httpRequest.getPartAsStringFailsafe("sender", 43)) ?: currentSone soneRequest.core.createReply(sender, post, TextFilter.filter(soneRequest.httpRequest.getHeader("Host"), text)) - throw RedirectException(returnPage) + redirectTo(returnPage) } } diff --git a/src/main/kotlin/net/pterodactylus/sone/web/pages/CreateSonePage.kt b/src/main/kotlin/net/pterodactylus/sone/web/pages/CreateSonePage.kt index b2056c2..0b690da 100644 --- a/src/main/kotlin/net/pterodactylus/sone/web/pages/CreateSonePage.kt +++ b/src/main/kotlin/net/pterodactylus/sone/web/pages/CreateSonePage.kt @@ -31,7 +31,7 @@ class CreateSonePage @Inject constructor(webInterface: WebInterface, loaders: Lo logger.log(Level.SEVERE, "Could not create Sone for OwnIdentity: $ownIdentity") } setCurrentSone(soneRequest.toadletContext, sone) - throw RedirectException("index.html") + redirectTo("index.html") } templateContext["errorNoIdentity"] = true } diff --git a/src/main/kotlin/net/pterodactylus/sone/web/pages/DebugPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/pages/DebugPage.kt new file mode 100644 index 0000000..824f6ef --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/web/pages/DebugPage.kt @@ -0,0 +1,18 @@ +package net.pterodactylus.sone.web.pages + +import net.pterodactylus.sone.main.* +import net.pterodactylus.sone.web.* +import net.pterodactylus.sone.web.page.* +import net.pterodactylus.util.template.* +import javax.inject.* + +@ToadletPath("debug") +class DebugPage @Inject constructor(webInterface: WebInterface, loaders: Loaders, templateRenderer: TemplateRenderer) : + SoneTemplatePage(webInterface, loaders, templateRenderer) { + + override fun handleRequest(soneRequest: SoneRequest, templateContext: TemplateContext) { + soneRequest.core.setDebug() + redirectTo("./") + } + +} diff --git a/src/main/kotlin/net/pterodactylus/sone/web/pages/DeleteAlbumPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/pages/DeleteAlbumPage.kt index d55c7cb..bd86c95 100644 --- a/src/main/kotlin/net/pterodactylus/sone/web/pages/DeleteAlbumPage.kt +++ b/src/main/kotlin/net/pterodactylus/sone/web/pages/DeleteAlbumPage.kt @@ -18,18 +18,18 @@ class DeleteAlbumPage @Inject constructor(webInterface: WebInterface, loaders: L override fun handleRequest(soneRequest: SoneRequest, currentSone: Sone, templateContext: TemplateContext) { if (soneRequest.isPOST) { - val album = soneRequest.core.getAlbum(soneRequest.httpRequest.getPartAsStringFailsafe("album", 36)) ?: throw RedirectException("invalid.html") + val album = soneRequest.core.getAlbum(soneRequest.httpRequest.getPartAsStringFailsafe("album", 36)) ?: redirectTo("invalid.html") if (!album.sone.isLocal) { - throw RedirectException("noPermission.html") + redirectTo("noPermission.html") } if (soneRequest.httpRequest.getPartAsStringFailsafe("abortDelete", 4) == "true") { - throw RedirectException("imageBrowser.html?album=${album.id}") + redirectTo("imageBrowser.html?album=${album.id}") } soneRequest.core.deleteAlbum(album) - throw RedirectException(if (album.parent.isRoot) "imageBrowser.html?sone=${album.sone.id}" else "imageBrowser.html?album=${album.parent.id}") + redirectTo(if (album.parent.isRoot) "imageBrowser.html?sone=${album.sone.id}" else "imageBrowser.html?album=${album.parent.id}") } val album = soneRequest.core.getAlbum(soneRequest.httpRequest.getParam("album")) - templateContext["album"] = album ?: throw RedirectException("invalid.html") + templateContext["album"] = album ?: redirectTo("invalid.html") } } diff --git a/src/main/kotlin/net/pterodactylus/sone/web/pages/DeleteImagePage.kt b/src/main/kotlin/net/pterodactylus/sone/web/pages/DeleteImagePage.kt index a9d601c..6459bfd 100644 --- a/src/main/kotlin/net/pterodactylus/sone/web/pages/DeleteImagePage.kt +++ b/src/main/kotlin/net/pterodactylus/sone/web/pages/DeleteImagePage.kt @@ -18,19 +18,19 @@ class DeleteImagePage @Inject constructor(webInterface: WebInterface, loaders: L override fun handleRequest(soneRequest: SoneRequest, currentSone: Sone, templateContext: TemplateContext) { if (soneRequest.isPOST) { - val image = soneRequest.core.getImage(soneRequest.httpRequest.getPartAsStringFailsafe("image", 36)) ?: throw RedirectException("invalid.html") + val image = soneRequest.core.getImage(soneRequest.httpRequest.getPartAsStringFailsafe("image", 36)) ?: redirectTo("invalid.html") if (!image.sone.isLocal) { - throw RedirectException("noPermission.html") + redirectTo("noPermission.html") } if (soneRequest.httpRequest.isPartSet("abortDelete")) { - throw RedirectException("imageBrowser.html?image=${image.id}") + redirectTo("imageBrowser.html?image=${image.id}") } soneRequest.core.deleteImage(image) - throw RedirectException("imageBrowser.html?album=${image.album.id}") + redirectTo("imageBrowser.html?album=${image.album.id}") } - val image = soneRequest.core.getImage(soneRequest.httpRequest.getParam("image")) ?: throw RedirectException("invalid.html") + val image = soneRequest.core.getImage(soneRequest.httpRequest.getParam("image")) ?: redirectTo("invalid.html") if (!image.sone.isLocal) { - throw RedirectException("noPermission.html") + redirectTo("noPermission.html") } templateContext["image"] = image } diff --git a/src/main/kotlin/net/pterodactylus/sone/web/pages/DeletePostPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/pages/DeletePostPage.kt index b3749b3..2396370 100644 --- a/src/main/kotlin/net/pterodactylus/sone/web/pages/DeletePostPage.kt +++ b/src/main/kotlin/net/pterodactylus/sone/web/pages/DeletePostPage.kt @@ -18,22 +18,22 @@ class DeletePostPage @Inject constructor(webInterface: WebInterface, loaders: Lo override fun handleRequest(soneRequest: SoneRequest, currentSone: Sone, templateContext: TemplateContext) { if (soneRequest.isPOST) { - val post = soneRequest.core.getPost(soneRequest.httpRequest.getPartAsStringFailsafe("post", 36)) ?: throw RedirectException("noPermission.html") + val post = soneRequest.core.getPost(soneRequest.httpRequest.getPartAsStringFailsafe("post", 36)) ?: redirectTo("noPermission.html") val returnPage = soneRequest.httpRequest.getPartAsStringFailsafe("returnPage", 256) if (!post.sone.isLocal) { - throw RedirectException("noPermission.html") + redirectTo("noPermission.html") } if (soneRequest.httpRequest.isPartSet("confirmDelete")) { soneRequest.core.deletePost(post) - throw RedirectException(returnPage) + redirectTo(returnPage) } else if (soneRequest.httpRequest.isPartSet("abortDelete")) { - throw RedirectException(returnPage) + redirectTo(returnPage) } templateContext["post"] = post templateContext["returnPage"] = returnPage return } - templateContext["post"] = soneRequest.core.getPost(soneRequest.httpRequest.getParam("post")) ?: throw RedirectException("noPermission.html") + templateContext["post"] = soneRequest.core.getPost(soneRequest.httpRequest.getParam("post")) ?: redirectTo("noPermission.html") templateContext["returnPage"] = soneRequest.httpRequest.getParam("returnPage") } diff --git a/src/main/kotlin/net/pterodactylus/sone/web/pages/DeleteProfileFieldPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/pages/DeleteProfileFieldPage.kt index 0fabad9..6303b11 100644 --- a/src/main/kotlin/net/pterodactylus/sone/web/pages/DeleteProfileFieldPage.kt +++ b/src/main/kotlin/net/pterodactylus/sone/web/pages/DeleteProfileFieldPage.kt @@ -18,13 +18,13 @@ class DeleteProfileFieldPage @Inject constructor(webInterface: WebInterface, loa override fun handleRequest(soneRequest: SoneRequest, currentSone: Sone, templateContext: TemplateContext) { if (soneRequest.isPOST) { - val field = currentSone.profile.getFieldById(soneRequest.httpRequest.getPartAsStringFailsafe("field", 36)) ?: throw RedirectException("invalid.html") + val field = currentSone.profile.getFieldById(soneRequest.httpRequest.getPartAsStringFailsafe("field", 36)) ?: redirectTo("invalid.html") if (soneRequest.httpRequest.getPartAsStringFailsafe("confirm", 4) == "true") { currentSone.profile = currentSone.profile.apply { removeField(field) } } - throw RedirectException("editProfile.html#profile-fields") + redirectTo("editProfile.html#profile-fields") } - val field = currentSone.profile.getFieldById(soneRequest.httpRequest.getParam("field")) ?: throw RedirectException("invalid.html") + val field = currentSone.profile.getFieldById(soneRequest.httpRequest.getParam("field")) ?: redirectTo("invalid.html") templateContext["field"] = field } diff --git a/src/main/kotlin/net/pterodactylus/sone/web/pages/DeleteReplyPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/pages/DeleteReplyPage.kt index e3f30ce..e6fbce8 100644 --- a/src/main/kotlin/net/pterodactylus/sone/web/pages/DeleteReplyPage.kt +++ b/src/main/kotlin/net/pterodactylus/sone/web/pages/DeleteReplyPage.kt @@ -19,17 +19,17 @@ class DeleteReplyPage @Inject constructor(webInterface: WebInterface, loaders: L override fun handleRequest(soneRequest: SoneRequest, currentSone: Sone, templateContext: TemplateContext) { if (soneRequest.isPOST) { val replyId = soneRequest.httpRequest.getPartAsStringFailsafe("reply", 36) - val reply = soneRequest.core.getPostReply(replyId) ?: throw RedirectException("noPermission.html") + val reply = soneRequest.core.getPostReply(replyId) ?: redirectTo("noPermission.html") if (!reply.sone.isLocal) { - throw RedirectException("noPermission.html") + redirectTo("noPermission.html") } val returnPage = soneRequest.httpRequest.getPartAsStringFailsafe("returnPage", 256) if (soneRequest.httpRequest.isPartSet("confirmDelete")) { soneRequest.core.deleteReply(reply) - throw RedirectException(returnPage) + redirectTo(returnPage) } if (soneRequest.httpRequest.isPartSet("abortDelete")) { - throw RedirectException(returnPage) + redirectTo(returnPage) } templateContext["reply"] = replyId templateContext["returnPage"] = returnPage diff --git a/src/main/kotlin/net/pterodactylus/sone/web/pages/DeleteSonePage.kt b/src/main/kotlin/net/pterodactylus/sone/web/pages/DeleteSonePage.kt index f65e378..3f99730 100644 --- a/src/main/kotlin/net/pterodactylus/sone/web/pages/DeleteSonePage.kt +++ b/src/main/kotlin/net/pterodactylus/sone/web/pages/DeleteSonePage.kt @@ -24,7 +24,7 @@ class DeleteSonePage @Inject constructor(webInterface: WebInterface, loaders: Lo if (soneRequest.httpRequest.isPartSet("deleteSone")) { soneRequest.core.deleteSone(currentSone) } - throw RedirectException("index.html") + redirectTo("index.html") } } diff --git a/src/main/kotlin/net/pterodactylus/sone/web/pages/DismissNotificationPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/pages/DismissNotificationPage.kt index 862db67..cd348e7 100644 --- a/src/main/kotlin/net/pterodactylus/sone/web/pages/DismissNotificationPage.kt +++ b/src/main/kotlin/net/pterodactylus/sone/web/pages/DismissNotificationPage.kt @@ -17,7 +17,7 @@ class DismissNotificationPage @Inject constructor(webInterface: WebInterface, lo val returnPage = soneRequest.httpRequest.getPartAsStringFailsafe("returnPage", 256) val notificationId = soneRequest.httpRequest.getPartAsStringFailsafe("notification", 36) soneRequest.webInterface.getNotification(notificationId).orNull()?.takeIf { it.isDismissable }?.dismiss() - throw RedirectException(returnPage) + redirectTo(returnPage) } } diff --git a/src/main/kotlin/net/pterodactylus/sone/web/pages/DistrustPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/pages/DistrustPage.kt deleted file mode 100644 index f2115e5..0000000 --- a/src/main/kotlin/net/pterodactylus/sone/web/pages/DistrustPage.kt +++ /dev/null @@ -1,29 +0,0 @@ -package net.pterodactylus.sone.web.pages - -import net.pterodactylus.sone.data.* -import net.pterodactylus.sone.main.* -import net.pterodactylus.sone.utils.* -import net.pterodactylus.sone.web.* -import net.pterodactylus.sone.web.page.* -import net.pterodactylus.util.template.* -import javax.inject.* - -/** - * Page that lets the user distrust another Sone. This will assign a - * configurable (negative) amount of trust to an identity. - * - * @see net.pterodactylus.sone.core.Core#distrustSone(Sone, Sone) - */ -@ToadletPath("distrust.html") -class DistrustPage @Inject constructor(webInterface: WebInterface, loaders: Loaders, templateRenderer: TemplateRenderer) : - LoggedInPage("Page.Distrust.Title", webInterface, loaders, templateRenderer) { - - override fun handleRequest(soneRequest: SoneRequest, currentSone: Sone, templateContext: TemplateContext) { - if (soneRequest.isPOST) { - soneRequest.core.getSone(soneRequest.httpRequest.getPartAsStringFailsafe("sone", 44)) - ?.run { soneRequest.core.distrustSone(currentSone, this) } - throw RedirectException(soneRequest.httpRequest.getPartAsStringFailsafe("returnPage", 256)) - } - } - -} diff --git a/src/main/kotlin/net/pterodactylus/sone/web/pages/EditAlbumPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/pages/EditAlbumPage.kt index aeaf14e..4569e9a 100644 --- a/src/main/kotlin/net/pterodactylus/sone/web/pages/EditAlbumPage.kt +++ b/src/main/kotlin/net/pterodactylus/sone/web/pages/EditAlbumPage.kt @@ -18,16 +18,16 @@ class EditAlbumPage @Inject constructor(webInterface: WebInterface, loaders: Loa override fun handleRequest(soneRequest: SoneRequest, currentSone: Sone, templateContext: TemplateContext) { if (soneRequest.isPOST) { - val album = soneRequest.core.getAlbum(soneRequest.httpRequest.getPartAsStringFailsafe("album", 36)) ?: throw RedirectException("invalid.html") - album.takeUnless { it.sone.isLocal }?.run { throw RedirectException("noPermission.html") } + val album = soneRequest.core.getAlbum(soneRequest.httpRequest.getPartAsStringFailsafe("album", 36)) ?: redirectTo("invalid.html") + album.takeUnless { it.sone.isLocal }?.run { redirectTo("noPermission.html") } if (soneRequest.httpRequest.getPartAsStringFailsafe("moveLeft", 4) == "true") { album.parent?.moveAlbumUp(album) soneRequest.core.touchConfiguration() - throw RedirectException("imageBrowser.html?album=${album.parent?.id}") + redirectTo("imageBrowser.html?album=${album.parent?.id}") } else if (soneRequest.httpRequest.getPartAsStringFailsafe("moveRight", 4) == "true") { album.parent?.moveAlbumDown(album) soneRequest.core.touchConfiguration() - throw RedirectException("imageBrowser.html?album=${album.parent?.id}") + redirectTo("imageBrowser.html?album=${album.parent?.id}") } else { try { album.modify() @@ -35,10 +35,10 @@ class EditAlbumPage @Inject constructor(webInterface: WebInterface, loaders: Loa .setDescription(soneRequest.httpRequest.getPartAsStringFailsafe("description", 1000)) .update() } catch (e: AlbumTitleMustNotBeEmpty) { - throw RedirectException("emptyAlbumTitle.html") + redirectTo("emptyAlbumTitle.html") } soneRequest.core.touchConfiguration() - throw RedirectException("imageBrowser.html?album=${album.id}") + redirectTo("imageBrowser.html?album=${album.id}") } } } diff --git a/src/main/kotlin/net/pterodactylus/sone/web/pages/EditImagePage.kt b/src/main/kotlin/net/pterodactylus/sone/web/pages/EditImagePage.kt index 76f6e23..5443d28 100644 --- a/src/main/kotlin/net/pterodactylus/sone/web/pages/EditImagePage.kt +++ b/src/main/kotlin/net/pterodactylus/sone/web/pages/EditImagePage.kt @@ -19,9 +19,9 @@ class EditImagePage @Inject constructor(webInterface: WebInterface, loaders: Loa override fun handleRequest(soneRequest: SoneRequest, currentSone: Sone, templateContext: TemplateContext) { if (soneRequest.isPOST) { - val image = soneRequest.core.getImage(soneRequest.httpRequest.getPartAsStringFailsafe("image", 36)) ?: throw RedirectException("invalid.html") + val image = soneRequest.core.getImage(soneRequest.httpRequest.getPartAsStringFailsafe("image", 36)) ?: redirectTo("invalid.html") if (!image.sone.isLocal) { - throw RedirectException("noPermission.html") + redirectTo("noPermission.html") } soneRequest.httpRequest.getPartAsStringFailsafe("returnPage", 256).let { returnPage -> if (soneRequest.httpRequest.getPartAsStringFailsafe("moveLeft", 4) == "true") { @@ -38,10 +38,10 @@ class EditImagePage @Inject constructor(webInterface: WebInterface, loaders: Loa .update() soneRequest.core.touchConfiguration() } catch (e: ImageTitleMustNotBeEmpty) { - throw RedirectException("emptyImageTitle.html") + redirectTo("emptyImageTitle.html") } } - throw RedirectException(returnPage) + redirectTo(returnPage) } } } diff --git a/src/main/kotlin/net/pterodactylus/sone/web/pages/EditProfileFieldPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/pages/EditProfileFieldPage.kt index 386f66b..be97e80 100644 --- a/src/main/kotlin/net/pterodactylus/sone/web/pages/EditProfileFieldPage.kt +++ b/src/main/kotlin/net/pterodactylus/sone/web/pages/EditProfileFieldPage.kt @@ -20,23 +20,23 @@ class EditProfileFieldPage @Inject constructor(webInterface: WebInterface, loade currentSone.profile.let { profile -> if (soneRequest.isPOST) { if (soneRequest.httpRequest.getPartAsStringFailsafe("cancel", 4) == "true") { - throw RedirectException("editProfile.html#profile-fields") + redirectTo("editProfile.html#profile-fields") } - val field = profile.getFieldById(soneRequest.httpRequest.getPartAsStringFailsafe("field", 36)) ?: throw RedirectException("invalid.html") + val field = profile.getFieldById(soneRequest.httpRequest.getPartAsStringFailsafe("field", 36)) ?: redirectTo("invalid.html") soneRequest.httpRequest.getPartAsStringFailsafe("name", 256).let { name -> try { if (name != field.name) { field.name = name currentSone.profile = profile } - throw RedirectException("editProfile.html#profile-fields") + redirectTo("editProfile.html#profile-fields") } catch (e: IllegalArgumentException) { templateContext["duplicateFieldName"] = true return } } } - templateContext["field"] = profile.getFieldById(soneRequest.httpRequest.getParam("field")) ?: throw RedirectException("invalid.html") + templateContext["field"] = profile.getFieldById(soneRequest.httpRequest.getParam("field")) ?: redirectTo("invalid.html") } } diff --git a/src/main/kotlin/net/pterodactylus/sone/web/pages/EditProfilePage.kt b/src/main/kotlin/net/pterodactylus/sone/web/pages/EditProfilePage.kt index 54b7efd..93393ba 100644 --- a/src/main/kotlin/net/pterodactylus/sone/web/pages/EditProfilePage.kt +++ b/src/main/kotlin/net/pterodactylus/sone/web/pages/EditProfilePage.kt @@ -43,31 +43,31 @@ class EditProfilePage @Inject constructor(webInterface: WebInterface, loaders: L } currentSone.profile = profile soneRequest.core.touchConfiguration() - throw RedirectException("editProfile.html") + redirectTo("editProfile.html") } else if (soneRequest.httpRequest.getPartAsStringFailsafe("add-field", 4) == "true") { val fieldName = soneRequest.httpRequest.getPartAsStringFailsafe("field-name", 100) try { profile.addField(fieldName) currentSone.profile = profile soneRequest.core.touchConfiguration() - throw RedirectException("editProfile.html#profile-fields") + redirectTo("editProfile.html#profile-fields") } catch (e: DuplicateField) { templateContext["fieldName"] = fieldName templateContext["duplicateFieldName"] = true } } else profile.fields.forEach { field -> if (soneRequest.httpRequest.getPartAsStringFailsafe("delete-field-${field.id}", 4) == "true") { - throw RedirectException("deleteProfileField.html?field=${field.id}") + redirectTo("deleteProfileField.html?field=${field.id}") } else if (soneRequest.httpRequest.getPartAsStringFailsafe("edit-field-${field.id}", 4) == "true") { - throw RedirectException("editProfileField.html?field=${field.id}") + redirectTo("editProfileField.html?field=${field.id}") } else if (soneRequest.httpRequest.getPartAsStringFailsafe("move-down-field-${field.id}", 4) == "true") { profile.moveFieldDown(field) currentSone.profile = profile - throw RedirectException("editProfile.html#profile-fields") + redirectTo("editProfile.html#profile-fields") } else if (soneRequest.httpRequest.getPartAsStringFailsafe("move-up-field-${field.id}", 4) == "true") { profile.moveFieldUp(field) currentSone.profile = profile - throw RedirectException("editProfile.html#profile-fields") + redirectTo("editProfile.html#profile-fields") } } } diff --git a/src/main/kotlin/net/pterodactylus/sone/web/pages/FollowSonePage.kt b/src/main/kotlin/net/pterodactylus/sone/web/pages/FollowSonePage.kt index 244cb52..5fd06ce 100644 --- a/src/main/kotlin/net/pterodactylus/sone/web/pages/FollowSonePage.kt +++ b/src/main/kotlin/net/pterodactylus/sone/web/pages/FollowSonePage.kt @@ -24,7 +24,7 @@ class FollowSonePage @Inject constructor(webInterface: WebInterface, loaders: Lo soneRequest.core.followSone(currentSone, sone.first) soneRequest.core.markSoneKnown(sone.second) } - throw RedirectException(soneRequest.httpRequest.getPartAsStringFailsafe("returnPage", 256)) + redirectTo(soneRequest.httpRequest.getPartAsStringFailsafe("returnPage", 256)) } } diff --git a/src/main/kotlin/net/pterodactylus/sone/web/pages/LikePage.kt b/src/main/kotlin/net/pterodactylus/sone/web/pages/LikePage.kt index cc5043f..c8369e9 100644 --- a/src/main/kotlin/net/pterodactylus/sone/web/pages/LikePage.kt +++ b/src/main/kotlin/net/pterodactylus/sone/web/pages/LikePage.kt @@ -23,7 +23,7 @@ class LikePage @Inject constructor(webInterface: WebInterface, loaders: Loaders, "reply" -> currentSone.addLikedReplyId(soneRequest.parameters["reply", 36]!!) } } - throw RedirectException(soneRequest.parameters["returnPage", 256]!!) + redirectTo(soneRequest.parameters["returnPage", 256]!!) } } diff --git a/src/main/kotlin/net/pterodactylus/sone/web/pages/LockSonePage.kt b/src/main/kotlin/net/pterodactylus/sone/web/pages/LockSonePage.kt index 2a62a7d..9f6f30e 100644 --- a/src/main/kotlin/net/pterodactylus/sone/web/pages/LockSonePage.kt +++ b/src/main/kotlin/net/pterodactylus/sone/web/pages/LockSonePage.kt @@ -19,7 +19,7 @@ class LockSonePage @Inject constructor(webInterface: WebInterface, loaders: Load soneRequest.parameters["sone", 44]!! .let { soneRequest.core.getLocalSone(it) } ?.let { soneRequest.core.lockSone(it) } - throw RedirectException(returnPage) + redirectTo(returnPage) } } diff --git a/src/main/kotlin/net/pterodactylus/sone/web/pages/LoginPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/pages/LoginPage.kt index 9e47049..5be34ac 100644 --- a/src/main/kotlin/net/pterodactylus/sone/web/pages/LoginPage.kt +++ b/src/main/kotlin/net/pterodactylus/sone/web/pages/LoginPage.kt @@ -23,7 +23,7 @@ class LoginPage @Inject constructor(webInterface: WebInterface, loaders: Loaders soneRequest.core.getLocalSone(soneId)?.let { sone -> setCurrentSone(soneRequest.toadletContext, sone) val target = soneRequest.httpRequest.getParam("target").emptyToNull ?: "index.html" - throw RedirectException(target) + redirectTo(target) } } templateContext["sones"] = soneRequest.core.localSones.sortedWith(Sone.NICE_NAME_COMPARATOR) diff --git a/src/main/kotlin/net/pterodactylus/sone/web/pages/LogoutPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/pages/LogoutPage.kt index 325d876..a7722d4 100644 --- a/src/main/kotlin/net/pterodactylus/sone/web/pages/LogoutPage.kt +++ b/src/main/kotlin/net/pterodactylus/sone/web/pages/LogoutPage.kt @@ -17,7 +17,7 @@ class LogoutPage @Inject constructor(webInterface: WebInterface, loaders: Loader override fun handleRequest(soneRequest: SoneRequest, currentSone: Sone, templateContext: TemplateContext) { setCurrentSone(soneRequest.toadletContext, null) - throw RedirectException("index.html") + redirectTo("index.html") } override fun isEnabled(soneRequest: SoneRequest): Boolean = diff --git a/src/main/kotlin/net/pterodactylus/sone/web/pages/MarkAsKnownPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/pages/MarkAsKnownPage.kt index 652881d..f919318 100644 --- a/src/main/kotlin/net/pterodactylus/sone/web/pages/MarkAsKnownPage.kt +++ b/src/main/kotlin/net/pterodactylus/sone/web/pages/MarkAsKnownPage.kt @@ -22,9 +22,9 @@ class MarkAsKnownPage @Inject constructor(webInterface: WebInterface, loaders: L "sone" -> ids.mapNotNull(soneRequest.core::getSone).forEach(soneRequest.core::markSoneKnown) "post" -> ids.mapNotNull(soneRequest.core::getPost).forEach(soneRequest.core::markPostKnown) "reply" -> ids.mapNotNull(soneRequest.core::getPostReply).forEach(soneRequest.core::markReplyKnown) - else -> throw RedirectException("invalid.html") + else -> redirectTo("invalid.html") } - throw RedirectException(soneRequest.parameters["returnPage", 256]!!) + redirectTo(soneRequest.parameters["returnPage", 256]!!) } } diff --git a/src/main/kotlin/net/pterodactylus/sone/web/pages/MetricsPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/pages/MetricsPage.kt new file mode 100644 index 0000000..837e2c8 --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/web/pages/MetricsPage.kt @@ -0,0 +1,19 @@ +package net.pterodactylus.sone.web.pages + +import com.codahale.metrics.* +import net.pterodactylus.sone.main.* +import net.pterodactylus.sone.web.* +import net.pterodactylus.sone.web.page.* +import net.pterodactylus.util.template.* +import javax.inject.* + +@MenuName("Metrics") +@TemplatePath("/templates/metrics.html") +@ToadletPath("metrics.html") +class MetricsPage @Inject constructor(webInterface: WebInterface, loaders: Loaders, templateRenderer: TemplateRenderer, private val metricsRegistry: MetricRegistry) : SoneTemplatePage(webInterface, loaders, templateRenderer, "Page.Metrics.Title") { + + override fun handleRequest(soneRequest: SoneRequest, templateContext: TemplateContext) { + templateContext["histograms"] = metricsRegistry.histograms + } + +} diff --git a/src/main/kotlin/net/pterodactylus/sone/web/pages/OptionsPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/pages/OptionsPage.kt index 8829030..412258d 100644 --- a/src/main/kotlin/net/pterodactylus/sone/web/pages/OptionsPage.kt +++ b/src/main/kotlin/net/pterodactylus/sone/web/pages/OptionsPage.kt @@ -50,9 +50,6 @@ class OptionsPage @Inject constructor(webInterface: WebInterface, loaders: Loade val imagesPerPage = soneRequest.parameters["images-per-page"]?.toIntOrNull() val insertionDelay = soneRequest.parameters["insertion-delay"]?.toIntOrNull() val fcpFullAccessRequired = soneRequest.parameters["fcp-full-access-required"]?.toIntOrNull() - val negativeTrust = soneRequest.parameters["negative-trust"]?.toIntOrNull() - val positiveTrust = soneRequest.parameters["positive-trust"]?.toIntOrNull() - val trustComment = soneRequest.parameters["trust-comment"]?.emptyToNull if (cantSetOption { soneRequest.core.preferences.newPostsPerPage = postsPerPage }) fieldsWithErrors += "posts-per-page" if (cantSetOption { soneRequest.core.preferences.newCharactersPerPost = charactersPerPost }) fieldsWithErrors += "characters-per-post" @@ -60,13 +57,10 @@ class OptionsPage @Inject constructor(webInterface: WebInterface, loaders: Loade if (cantSetOption { soneRequest.core.preferences.newImagesPerPage = imagesPerPage }) fieldsWithErrors += "images-per-page" if (cantSetOption { soneRequest.core.preferences.newInsertionDelay = insertionDelay }) fieldsWithErrors += "insertion-delay" fcpFullAccessRequired?.also { if (cantSetOption { soneRequest.core.preferences.newFcpFullAccessRequired = FullAccessRequired.values()[fcpFullAccessRequired] }) fieldsWithErrors += "fcp-full-access-required" } - if (cantSetOption { soneRequest.core.preferences.newNegativeTrust = negativeTrust }) fieldsWithErrors += "negative-trust" - if (cantSetOption { soneRequest.core.preferences.newPositiveTrust = positiveTrust }) fieldsWithErrors += "positive-trust" - if (cantSetOption { soneRequest.core.preferences.newTrustComment = trustComment }) fieldsWithErrors += "trust-comment" if (fieldsWithErrors.isEmpty()) { soneRequest.core.touchConfiguration() - throw RedirectException("options.html") + redirectTo("options.html") } templateContext["fieldErrors"] = fieldsWithErrors } @@ -86,11 +80,8 @@ class OptionsPage @Inject constructor(webInterface: WebInterface, loaders: Loade templateContext["images-per-page"] = preferences.imagesPerPage templateContext["fcp-interface-active"] = preferences.fcpInterfaceActive templateContext["require-full-access"] = preferences.requireFullAccess - templateContext["negative-trust"] = preferences.negativeTrust - templateContext["positive-trust"] = preferences.positiveTrust templateContext["post-cut-off-length"] = preferences.postCutOffLength templateContext["posts-per-page"] = preferences.postsPerPage - templateContext["trust-comment"] = preferences.trustComment } } diff --git a/src/main/kotlin/net/pterodactylus/sone/web/pages/SearchPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/pages/SearchPage.kt index 3c14604..19eba1c 100644 --- a/src/main/kotlin/net/pterodactylus/sone/web/pages/SearchPage.kt +++ b/src/main/kotlin/net/pterodactylus/sone/web/pages/SearchPage.kt @@ -125,7 +125,7 @@ class SearchPage(webInterface: WebInterface, loaders: Loaders, templateRenderer: } } - private fun redirect(target: String): Nothing = throw RedirectException(target) + private fun redirect(target: String): Nothing = redirectTo(target) enum class Optionality { OPTIONAL, diff --git a/src/main/kotlin/net/pterodactylus/sone/web/pages/SoneTemplatePage.kt b/src/main/kotlin/net/pterodactylus/sone/web/pages/SoneTemplatePage.kt index c905ed2..9734a03 100644 --- a/src/main/kotlin/net/pterodactylus/sone/web/pages/SoneTemplatePage.kt +++ b/src/main/kotlin/net/pterodactylus/sone/web/pages/SoneTemplatePage.kt @@ -2,6 +2,7 @@ package net.pterodactylus.sone.web.pages import freenet.clients.http.* import net.pterodactylus.sone.data.* +import net.pterodactylus.sone.freenet.* import net.pterodactylus.sone.main.* import net.pterodactylus.sone.utils.* import net.pterodactylus.sone.web.* @@ -20,11 +21,12 @@ open class SoneTemplatePage( templateRenderer: TemplateRenderer, private val pageTitleKey: String? = null, private val requiresLogin: Boolean = false, - private val pageTitle: (FreenetRequest) -> String = { pageTitleKey?.let(webInterface.l10n::getString) ?: "" } + private val pageTitle: (FreenetRequest) -> String = { pageTitleKey?.let(webInterface.translation::translate) ?: "" } ) : FreenetTemplatePage(templateRenderer, loaders, "noPermission.html") { private val core = webInterface.core private val sessionProvider: SessionProvider = webInterface + protected val translation: Translation = webInterface.translation protected fun getCurrentSone(toadletContext: ToadletContext, createSession: Boolean = true) = sessionProvider.getCurrentSone(toadletContext, createSession) @@ -89,7 +91,7 @@ open class SoneTemplatePage( private val String.urlEncode: String get() = URLEncoder.encode(this, "UTF-8") override fun isEnabled(toadletContext: ToadletContext) = - isEnabled(SoneRequest(toadletContext.uri, Method.GET, HTTPRequestImpl(toadletContext.uri, "GET"), toadletContext, webInterface.l10n, webInterface.sessionManager, core, webInterface)) + isEnabled(SoneRequest(toadletContext.uri, Method.GET, HTTPRequestImpl(toadletContext.uri, "GET"), toadletContext, webInterface.sessionManager, core, webInterface)) open fun isEnabled(soneRequest: SoneRequest) = when { requiresLogin && getCurrentSone(soneRequest.toadletContext) == null -> false diff --git a/src/main/kotlin/net/pterodactylus/sone/web/pages/TrustPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/pages/TrustPage.kt deleted file mode 100644 index 9a4c02f..0000000 --- a/src/main/kotlin/net/pterodactylus/sone/web/pages/TrustPage.kt +++ /dev/null @@ -1,28 +0,0 @@ -package net.pterodactylus.sone.web.pages - -import net.pterodactylus.sone.data.* -import net.pterodactylus.sone.main.* -import net.pterodactylus.sone.utils.* -import net.pterodactylus.sone.web.* -import net.pterodactylus.sone.web.page.* -import net.pterodactylus.util.template.* -import javax.inject.* - -/** - * Page that lets the user trust another Sone. This will assign a configurable - * amount of trust to an identity. - */ -@ToadletPath("trust.html") -class TrustPage @Inject constructor(webInterface: WebInterface, loaders: Loaders, templateRenderer: TemplateRenderer) : - LoggedInPage("Page.Trust.Title", webInterface, loaders, templateRenderer) { - - override fun handleRequest(soneRequest: SoneRequest, currentSone: Sone, templateContext: TemplateContext) { - if (soneRequest.isPOST) { - soneRequest.core.getSone(soneRequest.parameters["sone"]!!)?.let { sone -> - soneRequest.core.trustSone(currentSone, sone) - } - throw RedirectException(soneRequest.parameters["returnPage", 256]) - } - } - -} diff --git a/src/main/kotlin/net/pterodactylus/sone/web/pages/UnbookmarkPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/pages/UnbookmarkPage.kt index 9c226d4..aa7bbf6 100644 --- a/src/main/kotlin/net/pterodactylus/sone/web/pages/UnbookmarkPage.kt +++ b/src/main/kotlin/net/pterodactylus/sone/web/pages/UnbookmarkPage.kt @@ -21,13 +21,13 @@ class UnbookmarkPage @Inject constructor(webInterface: WebInterface, loaders: Lo soneRequest.core.bookmarkedPosts .filterNot(Post::isLoaded) .forEach(soneRequest.core::unbookmarkPost) - throw RedirectException("bookmarks.html") + redirectTo("bookmarks.html") } soneRequest.isPOST -> { soneRequest.parameters["post", 36] ?.let(soneRequest.core::getPost) ?.also(soneRequest.core::unbookmarkPost) - throw RedirectException(soneRequest.parameters["returnPage", 256]) + redirectTo(soneRequest.parameters["returnPage", 256]) } } } diff --git a/src/main/kotlin/net/pterodactylus/sone/web/pages/UnfollowSonePage.kt b/src/main/kotlin/net/pterodactylus/sone/web/pages/UnfollowSonePage.kt index 79fdef5..9dbe9f1 100644 --- a/src/main/kotlin/net/pterodactylus/sone/web/pages/UnfollowSonePage.kt +++ b/src/main/kotlin/net/pterodactylus/sone/web/pages/UnfollowSonePage.kt @@ -19,7 +19,7 @@ class UnfollowSonePage @Inject constructor(webInterface: WebInterface, loaders: if (soneRequest.isPOST) { soneRequest.parameters["sone"]!!.split(Regex("[ ,]+")) .forEach { soneRequest.core.unfollowSone(currentSone, it) } - throw RedirectException(soneRequest.parameters["returnPage", 256]) + redirectTo(soneRequest.parameters["returnPage", 256]) } } diff --git a/src/main/kotlin/net/pterodactylus/sone/web/pages/UnlikePage.kt b/src/main/kotlin/net/pterodactylus/sone/web/pages/UnlikePage.kt index 82147a9..46bd3ab 100644 --- a/src/main/kotlin/net/pterodactylus/sone/web/pages/UnlikePage.kt +++ b/src/main/kotlin/net/pterodactylus/sone/web/pages/UnlikePage.kt @@ -21,7 +21,7 @@ class UnlikePage @Inject constructor(webInterface: WebInterface, loaders: Loader "post" -> currentSone.removeLikedPostId(soneRequest.parameters["post"]!!) "reply" -> currentSone.removeLikedReplyId(soneRequest.parameters["reply"]!!) } - throw RedirectException(soneRequest.parameters["returnPage", 256]) + redirectTo(soneRequest.parameters["returnPage", 256]) } } diff --git a/src/main/kotlin/net/pterodactylus/sone/web/pages/UnlockSonePage.kt b/src/main/kotlin/net/pterodactylus/sone/web/pages/UnlockSonePage.kt index 2c5735b..450a466 100644 --- a/src/main/kotlin/net/pterodactylus/sone/web/pages/UnlockSonePage.kt +++ b/src/main/kotlin/net/pterodactylus/sone/web/pages/UnlockSonePage.kt @@ -19,7 +19,7 @@ class UnlockSonePage @Inject constructor(webInterface: WebInterface, loaders: Lo soneRequest.parameters["sone", 44] .let(soneRequest.core::getLocalSone) ?.also(soneRequest.core::unlockSone) - throw RedirectException(soneRequest.parameters["returnPage", 256]) + redirectTo(soneRequest.parameters["returnPage", 256]) } } diff --git a/src/main/kotlin/net/pterodactylus/sone/web/pages/UntrustPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/pages/UntrustPage.kt deleted file mode 100644 index 7dd342e..0000000 --- a/src/main/kotlin/net/pterodactylus/sone/web/pages/UntrustPage.kt +++ /dev/null @@ -1,28 +0,0 @@ -package net.pterodactylus.sone.web.pages - -import net.pterodactylus.sone.data.* -import net.pterodactylus.sone.main.* -import net.pterodactylus.sone.utils.* -import net.pterodactylus.sone.web.* -import net.pterodactylus.sone.web.page.* -import net.pterodactylus.util.template.* -import javax.inject.* - -/** - * Page that lets the user untrust another Sone. This will remove all trust - * assignments for an identity. - */ -@ToadletPath("untrust.html") -class UntrustPage @Inject constructor(webInterface: WebInterface, loaders: Loaders, templateRenderer: TemplateRenderer) : - LoggedInPage("Page.Untrust.Title", webInterface, loaders, templateRenderer) { - - override fun handleRequest(soneRequest: SoneRequest, currentSone: Sone, templateContext: TemplateContext) { - if (soneRequest.isPOST) { - soneRequest.parameters["sone", 44]!! - .let(soneRequest.core::getSone) - ?.also { soneRequest.core.untrustSone(currentSone, it) } - throw RedirectException(soneRequest.parameters["returnPage", 256]) - } - } - -} diff --git a/src/main/kotlin/net/pterodactylus/sone/web/pages/UploadImagePage.kt b/src/main/kotlin/net/pterodactylus/sone/web/pages/UploadImagePage.kt index de61835..0021c6b 100644 --- a/src/main/kotlin/net/pterodactylus/sone/web/pages/UploadImagePage.kt +++ b/src/main/kotlin/net/pterodactylus/sone/web/pages/UploadImagePage.kt @@ -23,17 +23,17 @@ class UploadImagePage @Inject constructor(webInterface: WebInterface, loaders: L override fun handleRequest(soneRequest: SoneRequest, currentSone: Sone, templateContext: TemplateContext) { if (soneRequest.isPOST) { - val parentAlbum = soneRequest.parameters["parent"]!!.let(soneRequest.core::getAlbum) ?: throw RedirectException("noPermission.html") + val parentAlbum = soneRequest.parameters["parent"]!!.let(soneRequest.core::getAlbum) ?: redirectTo("noPermission.html") if (parentAlbum.sone != currentSone) { - throw RedirectException("noPermission.html") + redirectTo("noPermission.html") } - val title = soneRequest.parameters["title", 200].emptyToNull ?: throw RedirectException("emptyImageTitle.html") + val title = soneRequest.parameters["title", 200].emptyToNull ?: redirectTo("emptyImageTitle.html") val uploadedFile = soneRequest.httpRequest.getUploadedFile("image") val bytes = uploadedFile.data.use { it.toByteArray() } val bufferedImage = bytes.toImage() if (bufferedImage == null) { - templateContext["messages"] = soneRequest.l10n.getString("Page.UploadImage.Error.InvalidImage") + templateContext["messages"] = translation.translate("Page.UploadImage.Error.InvalidImage") return } @@ -44,7 +44,7 @@ class UploadImagePage @Inject constructor(webInterface: WebInterface, loaders: L setTitle(title) setDescription(TextFilter.filter(soneRequest.headers["Host"], soneRequest.parameters["description", 4000])) }.update() - throw RedirectException("imageBrowser.html?album=${parentAlbum.id}") + redirectTo("imageBrowser.html?album=${parentAlbum.id}") } } diff --git a/src/main/kotlin/net/pterodactylus/sone/web/pages/ViewSonePage.kt b/src/main/kotlin/net/pterodactylus/sone/web/pages/ViewSonePage.kt index a498d24..efaee3f 100644 --- a/src/main/kotlin/net/pterodactylus/sone/web/pages/ViewSonePage.kt +++ b/src/main/kotlin/net/pterodactylus/sone/web/pages/ViewSonePage.kt @@ -51,7 +51,7 @@ class ViewSonePage @Inject constructor(webInterface: WebInterface, loaders: Load override fun getPageTitle(soneRequest: SoneRequest): String = soneRequest.parameters["sone"]!!.let(soneRequest.core::getSone)?.let { sone -> - "${SoneAccessor.getNiceName(sone)} - ${soneRequest.l10n.getString("Page.ViewSone.Title")}" - } ?: soneRequest.l10n.getString("Page.ViewSone.Page.TitleWithoutSone") + "${SoneAccessor.getNiceName(sone)} - ${translation.translate("Page.ViewSone.Title")}" + } ?: translation.translate("Page.ViewSone.Page.TitleWithoutSone") } diff --git a/src/main/resources/i18n/sone.de.properties b/src/main/resources/i18n/sone.de.properties index 7e42167..e17e995 100644 --- a/src/main/resources/i18n/sone.de.properties +++ b/src/main/resources/i18n/sone.de.properties @@ -26,6 +26,8 @@ Navigation.Menu.Sone.Item.Rescue.Name=Sonerettung Navigation.Menu.Sone.Item.Rescue.Tooltip=Rettet Ihre Sone Navigation.Menu.Sone.Item.About.Name=Über Sone Navigation.Menu.Sone.Item.About.Tooltip=Informationen über Sone +Navigation.Menu.Sone.Item.Metrics.Name=Metriken +Navigation.Menu.Sone.Item.Metrics.Tooltip=Von Sone gesammelte Metriken Page.About.Title=Über - Sone Page.About.Page.Title=Über @@ -66,10 +68,6 @@ Page.Options.Option.ImagesPerPage.Description=Anzahl der Bilder pro Seite. Page.Options.Option.CharactersPerPost.Description=Die Anzahl der Zeichen, die eine Nachricht enthalten muss, damit sie gekürzt angezeigt wird (-1 für „nie kürzen“). Die Anzahl der tatsächlich angezeigten Zeichen wird in der nächsten Option konfiguriert. Page.Options.Option.PostCutOffLength.Description=Die Anzahl der Zeichen, die von einer gekürzten Nachricht sichtbar sind (siehe Option hierüber). Wird ignoriert, wenn die Option hierüber deaktiviert ist, bzw. auf -1 steht. Page.Options.Option.RequireFullAccess.Description=Zugriff auf Sone für alle Rechner, die keinen vollen Zugriff haben, unterbinden. -Page.Options.Section.TrustOptions.Title=Vertrauenseinstellungen -Page.Options.Option.PositiveTrust.Description=Die Menge an positivem Vertrauen, die bei einem Klick auf den Haken unter einer Nachricht zugewiesen werden soll. -Page.Options.Option.NegativeTrust.Description=Die Menge an negativem Vertrauen, die bei einem Klick auf das rote X unter einer Nachricht zugewiesen werden soll. Dieser Wert sollte negativ sein. -Page.Options.Option.TrustComment.Description=Der Kommentar, der im Web of Trust für Ihre Zuweisung angezeigt werden soll. Page.Options.Section.FcpOptions.Title=FCP-Schnittstellenoptionen Page.Options.Option.FcpInterfaceActive.Description=Die FCP-Schnittstelle aktivieren, um anderen Plugins und Programmen den Zugriff auf Ihr Sone-Plugin zu ermöglichen. Page.Options.Option.FcpFullAccessRequired.Description=FCP-Verbindungen nur von „erlaubten Hosts“ erlauben (siehe {link}Knoten-Konfiguration, Abschnitt “FCP”{/link}). @@ -163,7 +161,7 @@ Page.EditProfileField.Page.Title=Benutzerdefiniertes Feld bearbeiten Page.EditProfileField.Text=Bitte geben Sie einen neuen Namen für dieses benutzerdefinierte Feld ein. Page.EditProfileField.Error.DuplicateFieldName=Der von Ihnen eingegebene Feldname existiert bereits. Page.EditProfileField.Button.Save=Ändern -Page.EditProfileField.Button.Reset=Aten Namen wiederherstellen +Page.EditProfileField.Button.Reset=Alten Namen wiederherstellen Page.EditProfileField.Button.Cancel=Namen nicht ändern Page.DeleteProfileField.Title=Benutzerdefiniertes Feld löschen - Sone @@ -276,12 +274,6 @@ Page.DeleteAlbum.Text.AlbumWillBeGone=Ihr Album „{title}“ wird gelöscht. M Page.DeleteAlbum.Button.Yes=Ja, Album löschen. Page.DeleteAlbum.Button.No=Nein, Album nicht löschen. -Page.Trust.Title=Sone vertrauen - Sone - -Page.Distrust.Title=Sone misstrauen - Sone - -Page.Untrust.Title=Sone nicht vertrauen - Sone - Page.MarkAsKnown.Title=Als gelesen markieren - Sone Page.Bookmark.Title=Als Favorit markieren - Sone @@ -305,7 +297,7 @@ Page.Rescue.Text.Fetching=Die Sonerettung versucht gerade, Version {0} Ihrer Son Page.Rescue.Text.Fetched=Die Sonerettung hat Version {0} Ihrer Sone herunter geladen. Bitte überprüfen Sie Ihre Nachrichten, Antworten und Ihr Profile. Bei Gefallen können Sie die Sone einfach entsperren. Page.Rescue.Text.FetchedLast=Die Sonerettung hat die letzte verfügbare Version Ihrer Sone herunter geladen. Wenn bis jetzt keine Version dabei war, die Sie wiederherstellen möchten, haben Sie jetzt kein Glück. Page.Rescue.Text.NotFetched=Die Sonerettung konnte Version {0} Ihrer Sone nicht herunter laden. Bitte versuchen Sie erneut, Version {0} herunter zu laden, oder versuchen Sie die nächstältere Version. -Page.Rescue.Label.NextEdition=Nächste Version: +Page.Rescue.Label.NextEdition=Nächste Version Page.Rescue.Button.Fetch=Version herunter laden Page.NoPermission.Title=Unberechtigter Zugriff - Sone @@ -331,6 +323,12 @@ Page.Invalid.Title=Ungültige Aktion ausgeführt - Sone Page.Invalid.Page.Title=Ungültige Aktion ausgeführt Page.Invalid.Text=Eine ungültige Aktion wurde ausgeführt, oder eine gültige Aktion hatte ungültige Parameter. Bitte kehren Sie zur {link}Hauptseite{/link} zurück und versuchen Sie Ihre Aktion erneut. Wenn der Fehler weiterhin besteht, haben Sie wahrscheinlich einen Programmierfehler gefunden. +Page.Metrics.Title=Metriken +Page.Metrics.Page.Title=Metriken +Page.Metrics.SoneInsertDuration.Title=Hochladedauer einer Sone +Page.Metrics.SoneParseDuration.Title=Parsdauer einer Sone +Page.Metrics.ConfigurationSaveDuration.Title=Konfigurationsspeicherdauer + View.Search.Button.Search=Suchen View.CreateSone.Text.WotIdentityRequired=Um eine Sone anzulegen, brauchen Sie eine Identität aus dem {link}„Web of Trust“ Plugin{/link}. @@ -358,9 +356,10 @@ View.Sone.Status.Downloading=Diese Sone wird gerade herunter geladen. View.Sone.Status.Inserting=Diese Sone wird gerade hoch geladen. View.SoneMenu.Link.AllAlbums=alle Alben +View.SoneMenu.WebOfTrustLink=„Web of Trust“ Profil View.Post.UnknownAuthor=(unbekannt) -View.Post.WebOfTrustLink=„Web of Trust“ Profil +View.Post.WebOfTrustLink=WoT-Profil View.Post.Permalink=Nachrichtenlink View.Post.PermalinkAuthor=Autorenlink View.Post.Bookmarks.PostIsBookmarked=Nachricht ist ein Favorit, klicken, um Favoritenmarkierung zu entfernen @@ -377,10 +376,6 @@ View.Post.ShowLess=weniger zeigen View.UpdateStatus.Text.ChooseSenderIdentity=Absender wählen -View.Trust.Tooltip.Trust=Dieser Sone vertrauen -View.Trust.Tooltip.Distrust=Dieser Sone misstrauen -View.Trust.Tooltip.Untrust=Dieser Sone kein Vertrauen zuweisen - View.CreateAlbum.Title=Album anlegen View.CreateAlbum.Label.Name=Name: View.CreateAlbum.Label.Description=Beschreibung: @@ -431,9 +426,6 @@ WebInterface.DefaultText.Option.PostsPerPage=Anzahl der Nachrichten pro Seite WebInterface.DefaultText.Option.ImagesPerPage=Anzahl der Bilder pro Seite WebInterface.DefaultText.Option.CharactersPerPost=Anzahl der Zeichen, die eine Nachricht haben muss, damit er gekürzt wird WebInterface.DefaultText.Option.PostCutOffLength=Anzahl der Zeichen, die von einer gekürzten Nachricht angezeigt werden -WebInterface.DefaultText.Option.PositiveTrust=Der positive Vertrauenswert -WebInterface.DefaultText.Option.NegativeTrust=Der negative Vertrauenswert -WebInterface.DefaultText.Option.TrustComment=Der Kommentar für die Vertrauenszuweisung WebInterface.Button.Comment=Antworten WebInterface.Confirmation.DeletePostButton=Ja, löschen! WebInterface.Confirmation.DeleteReplyButton=Ja, löschen! @@ -471,3 +463,4 @@ Notification.Mention.Text=Sie wurden in diesen Nachrichten erwähnt: Notification.SoneIsInserting.Text=Ihre Sone sone://{0} wird jetzt hoch geladen. Notification.SoneIsInserted.Text=Ihre Sone sone://{0} wurde in {1,number} {1,choice,0#Sekunden|1#Sekunde|1)[^>]*$|^#([\w-]+)$/, - - // Is it a simple selector - isSimple = /^.[^:#\[\.,]*$/, - - // Check if a string has a non-whitespace character in it - rnotwhite = /\S/, - - // Used for trimming whitespace - rtrim = /^(\s|\u00A0)+|(\s|\u00A0)+$/g, - - // Match a standalone tag - rsingleTag = /^<(\w+)\s*\/?>(?:<\/\1>)?$/, - - // Keep a UserAgent string for use with jQuery.browser - userAgent = navigator.userAgent, - - // For matching the engine and version of the browser - browserMatch, - - // Has the ready events already been bound? - readyBound = false, - - // The functions to execute on DOM ready - readyList = [], - - // The ready event handler - DOMContentLoaded, - - // Save a reference to some core methods - toString = Object.prototype.toString, - hasOwnProperty = Object.prototype.hasOwnProperty, - push = Array.prototype.push, - slice = Array.prototype.slice, - indexOf = Array.prototype.indexOf; - -jQuery.fn = jQuery.prototype = { - init: function( selector, context ) { - var match, elem, ret, doc; - - // Handle $(""), $(null), or $(undefined) - if ( !selector ) { - return this; - } - - // Handle $(DOMElement) - if ( selector.nodeType ) { - this.context = this[0] = selector; - this.length = 1; - return this; - } - - // The body element only exists once, optimize finding it - if ( selector === "body" && !context ) { - this.context = document; - this[0] = document.body; - this.selector = "body"; - this.length = 1; - return this; - } - - // Handle HTML strings - if ( typeof selector === "string" ) { - // Are we dealing with HTML string or an ID? - match = quickExpr.exec( selector ); - - // Verify a match, and that no context was specified for #id - if ( match && (match[1] || !context) ) { - - // HANDLE: $(html) -> $(array) - if ( match[1] ) { - doc = (context ? context.ownerDocument || context : document); - - // If a single string is passed in and it's a single tag - // just do a createElement and skip the rest - ret = rsingleTag.exec( selector ); - - if ( ret ) { - if ( jQuery.isPlainObject( context ) ) { - selector = [ document.createElement( ret[1] ) ]; - jQuery.fn.attr.call( selector, context, true ); - - } else { - selector = [ doc.createElement( ret[1] ) ]; - } - - } else { - ret = buildFragment( [ match[1] ], [ doc ] ); - selector = (ret.cacheable ? ret.fragment.cloneNode(true) : ret.fragment).childNodes; - } - - return jQuery.merge( this, selector ); - - // HANDLE: $("#id") - } else { - elem = document.getElementById( match[2] ); - - if ( elem ) { - // Handle the case where IE and Opera return items - // by name instead of ID - if ( elem.id !== match[2] ) { - return rootjQuery.find( selector ); - } - - // Otherwise, we inject the element directly into the jQuery object - this.length = 1; - this[0] = elem; - } - - this.context = document; - this.selector = selector; - return this; - } - - // HANDLE: $("TAG") - } else if ( !context && /^\w+$/.test( selector ) ) { - this.selector = selector; - this.context = document; - selector = document.getElementsByTagName( selector ); - return jQuery.merge( this, selector ); - - // HANDLE: $(expr, $(...)) - } else if ( !context || context.jquery ) { - return (context || rootjQuery).find( selector ); - - // HANDLE: $(expr, context) - // (which is just equivalent to: $(context).find(expr) - } else { - return jQuery( context ).find( selector ); - } - - // HANDLE: $(function) - // Shortcut for document ready - } else if ( jQuery.isFunction( selector ) ) { - return rootjQuery.ready( selector ); - } - - if (selector.selector !== undefined) { - this.selector = selector.selector; - this.context = selector.context; - } - - return jQuery.makeArray( selector, this ); - }, - - // Start with an empty selector - selector: "", - - // The current version of jQuery being used - jquery: "1.4.2", - - // The default length of a jQuery object is 0 - length: 0, - - // The number of elements contained in the matched element set - size: function() { - return this.length; - }, - - toArray: function() { - return slice.call( this, 0 ); - }, - - // Get the Nth element in the matched element set OR - // Get the whole matched element set as a clean array - get: function( num ) { - return num == null ? - - // Return a 'clean' array - this.toArray() : - - // Return just the object - ( num < 0 ? this.slice(num)[ 0 ] : this[ num ] ); - }, - - // Take an array of elements and push it onto the stack - // (returning the new matched element set) - pushStack: function( elems, name, selector ) { - // Build a new jQuery matched element set - var ret = jQuery(); - - if ( jQuery.isArray( elems ) ) { - push.apply( ret, elems ); - - } else { - jQuery.merge( ret, elems ); - } - - // Add the old object onto the stack (as a reference) - ret.prevObject = this; - - ret.context = this.context; - - if ( name === "find" ) { - ret.selector = this.selector + (this.selector ? " " : "") + selector; - } else if ( name ) { - ret.selector = this.selector + "." + name + "(" + selector + ")"; - } - - // Return the newly-formed element set - return ret; - }, - - // Execute a callback for every element in the matched set. - // (You can seed the arguments with an array of args, but this is - // only used internally.) - each: function( callback, args ) { - return jQuery.each( this, callback, args ); - }, - - ready: function( fn ) { - // Attach the listeners - jQuery.bindReady(); - - // If the DOM is already ready - if ( jQuery.isReady ) { - // Execute the function immediately - fn.call( document, jQuery ); - - // Otherwise, remember the function for later - } else if ( readyList ) { - // Add the function to the wait list - readyList.push( fn ); - } - - return this; - }, - - eq: function( i ) { - return i === -1 ? - this.slice( i ) : - this.slice( i, +i + 1 ); - }, - - first: function() { - return this.eq( 0 ); - }, - - last: function() { - return this.eq( -1 ); - }, - - slice: function() { - return this.pushStack( slice.apply( this, arguments ), - "slice", slice.call(arguments).join(",") ); - }, - - map: function( callback ) { - return this.pushStack( jQuery.map(this, function( elem, i ) { - return callback.call( elem, i, elem ); - })); - }, - - end: function() { - return this.prevObject || jQuery(null); - }, - - // For internal use only. - // Behaves like an Array's method, not like a jQuery method. - push: push, - sort: [].sort, - splice: [].splice -}; - -// Give the init function the jQuery prototype for later instantiation -jQuery.fn.init.prototype = jQuery.fn; - -jQuery.extend = jQuery.fn.extend = function() { - // copy reference to target object - var target = arguments[0] || {}, i = 1, length = arguments.length, deep = false, options, name, src, copy; - - // Handle a deep copy situation - if ( typeof target === "boolean" ) { - deep = target; - target = arguments[1] || {}; - // skip the boolean and the target - i = 2; - } - - // Handle case when target is a string or something (possible in deep copy) - if ( typeof target !== "object" && !jQuery.isFunction(target) ) { - target = {}; - } - - // extend jQuery itself if only one argument is passed - if ( length === i ) { - target = this; - --i; - } - - for ( ; i < length; i++ ) { - // Only deal with non-null/undefined values - if ( (options = arguments[ i ]) != null ) { - // Extend the base object - for ( name in options ) { - src = target[ name ]; - copy = options[ name ]; - - // Prevent never-ending loop - if ( target === copy ) { - continue; - } - - // Recurse if we're merging object literal values or arrays - if ( deep && copy && ( jQuery.isPlainObject(copy) || jQuery.isArray(copy) ) ) { - var clone = src && ( jQuery.isPlainObject(src) || jQuery.isArray(src) ) ? src - : jQuery.isArray(copy) ? [] : {}; - - // Never move original objects, clone them - target[ name ] = jQuery.extend( deep, clone, copy ); - - // Don't bring in undefined values - } else if ( copy !== undefined ) { - target[ name ] = copy; - } - } - } - } - - // Return the modified object - return target; -}; - -jQuery.extend({ - noConflict: function( deep ) { - window.$ = _$; - - if ( deep ) { - window.jQuery = _jQuery; - } - - return jQuery; - }, - - // Is the DOM ready to be used? Set to true once it occurs. - isReady: false, - - // Handle when the DOM is ready - ready: function() { - // Make sure that the DOM is not already loaded - if ( !jQuery.isReady ) { - // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443). - if ( !document.body ) { - return setTimeout( jQuery.ready, 13 ); - } - - // Remember that the DOM is ready - jQuery.isReady = true; - - // If there are functions bound, to execute - if ( readyList ) { - // Execute all of them - var fn, i = 0; - while ( (fn = readyList[ i++ ]) ) { - fn.call( document, jQuery ); - } - - // Reset the list of functions - readyList = null; - } - - // Trigger any bound ready events - if ( jQuery.fn.triggerHandler ) { - jQuery( document ).triggerHandler( "ready" ); - } - } - }, - - bindReady: function() { - if ( readyBound ) { - return; - } - - readyBound = true; - - // Catch cases where $(document).ready() is called after the - // browser event has already occurred. - if ( document.readyState === "complete" ) { - return jQuery.ready(); - } - - // Mozilla, Opera and webkit nightlies currently support this event - if ( document.addEventListener ) { - // Use the handy event callback - document.addEventListener( "DOMContentLoaded", DOMContentLoaded, false ); - - // A fallback to window.onload, that will always work - window.addEventListener( "load", jQuery.ready, false ); - - // If IE event model is used - } else if ( document.attachEvent ) { - // ensure firing before onload, - // maybe late but safe also for iframes - document.attachEvent("onreadystatechange", DOMContentLoaded); - - // A fallback to window.onload, that will always work - window.attachEvent( "onload", jQuery.ready ); - - // If IE and not a frame - // continually check to see if the document is ready - var toplevel = false; - - try { - toplevel = window.frameElement == null; - } catch(e) {} - - if ( document.documentElement.doScroll && toplevel ) { - doScrollCheck(); - } - } - }, - - // See test/unit/core.js for details concerning isFunction. - // Since version 1.3, DOM methods and functions like alert - // aren't supported. They return false on IE (#2968). - isFunction: function( obj ) { - return toString.call(obj) === "[object Function]"; - }, - - isArray: function( obj ) { - return toString.call(obj) === "[object Array]"; - }, - - isPlainObject: function( obj ) { - // Must be an Object. - // Because of IE, we also have to check the presence of the constructor property. - // Make sure that DOM nodes and window objects don't pass through, as well - if ( !obj || toString.call(obj) !== "[object Object]" || obj.nodeType || obj.setInterval ) { - return false; - } - - // Not own constructor property must be Object - if ( obj.constructor - && !hasOwnProperty.call(obj, "constructor") - && !hasOwnProperty.call(obj.constructor.prototype, "isPrototypeOf") ) { - return false; - } - - // Own properties are enumerated firstly, so to speed up, - // if last one is own, then all properties are own. - - var key; - for ( key in obj ) {} - - return key === undefined || hasOwnProperty.call( obj, key ); - }, - - isEmptyObject: function( obj ) { - for ( var name in obj ) { - return false; - } - return true; - }, - - error: function( msg ) { - throw msg; - }, - - parseJSON: function( data ) { - if ( typeof data !== "string" || !data ) { - return null; - } - - // Make sure leading/trailing whitespace is removed (IE can't handle it) - data = jQuery.trim( data ); - - // Make sure the incoming data is actual JSON - // Logic borrowed from http://json.org/json2.js - if ( /^[\],:{}\s]*$/.test(data.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, "@") - .replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, "]") - .replace(/(?:^|:|,)(?:\s*\[)+/g, "")) ) { - - // Try to use the native JSON parser first - return window.JSON && window.JSON.parse ? - window.JSON.parse( data ) : - (new Function("return " + data))(); - - } else { - jQuery.error( "Invalid JSON: " + data ); - } - }, - - noop: function() {}, - - // Evalulates a script in a global context - globalEval: function( data ) { - if ( data && rnotwhite.test(data) ) { - // Inspired by code by Andrea Giammarchi - // http://webreflection.blogspot.com/2007/08/global-scope-evaluation-and-dom.html - var head = document.getElementsByTagName("head")[0] || document.documentElement, - script = document.createElement("script"); - - script.type = "text/javascript"; - - if ( jQuery.support.scriptEval ) { - script.appendChild( document.createTextNode( data ) ); - } else { - script.text = data; - } - - // Use insertBefore instead of appendChild to circumvent an IE6 bug. - // This arises when a base node is used (#2709). - head.insertBefore( script, head.firstChild ); - head.removeChild( script ); - } - }, - - nodeName: function( elem, name ) { - return elem.nodeName && elem.nodeName.toUpperCase() === name.toUpperCase(); - }, - - // args is for internal usage only - each: function( object, callback, args ) { - var name, i = 0, - length = object.length, - isObj = length === undefined || jQuery.isFunction(object); - - if ( args ) { - if ( isObj ) { - for ( name in object ) { - if ( callback.apply( object[ name ], args ) === false ) { - break; - } - } - } else { - for ( ; i < length; ) { - if ( callback.apply( object[ i++ ], args ) === false ) { - break; - } - } - } - - // A special, fast, case for the most common use of each - } else { - if ( isObj ) { - for ( name in object ) { - if ( callback.call( object[ name ], name, object[ name ] ) === false ) { - break; - } - } - } else { - for ( var value = object[0]; - i < length && callback.call( value, i, value ) !== false; value = object[++i] ) {} - } - } - - return object; - }, - - trim: function( text ) { - return (text || "").replace( rtrim, "" ); - }, - - // results is for internal usage only - makeArray: function( array, results ) { - var ret = results || []; - - if ( array != null ) { - // The window, strings (and functions) also have 'length' - // The extra typeof function check is to prevent crashes - // in Safari 2 (See: #3039) - if ( array.length == null || typeof array === "string" || jQuery.isFunction(array) || (typeof array !== "function" && array.setInterval) ) { - push.call( ret, array ); - } else { - jQuery.merge( ret, array ); - } - } - - return ret; - }, - - inArray: function( elem, array ) { - if ( array.indexOf ) { - return array.indexOf( elem ); - } - - for ( var i = 0, length = array.length; i < length; i++ ) { - if ( array[ i ] === elem ) { - return i; - } - } - - return -1; - }, - - merge: function( first, second ) { - var i = first.length, j = 0; - - if ( typeof second.length === "number" ) { - for ( var l = second.length; j < l; j++ ) { - first[ i++ ] = second[ j ]; - } - - } else { - while ( second[j] !== undefined ) { - first[ i++ ] = second[ j++ ]; - } - } - - first.length = i; - - return first; - }, - - grep: function( elems, callback, inv ) { - var ret = []; - - // Go through the array, only saving the items - // that pass the validator function - for ( var i = 0, length = elems.length; i < length; i++ ) { - if ( !inv !== !callback( elems[ i ], i ) ) { - ret.push( elems[ i ] ); - } - } - - return ret; - }, - - // arg is for internal usage only - map: function( elems, callback, arg ) { - var ret = [], value; - - // Go through the array, translating each of the items to their - // new value (or values). - for ( var i = 0, length = elems.length; i < length; i++ ) { - value = callback( elems[ i ], i, arg ); - - if ( value != null ) { - ret[ ret.length ] = value; - } - } - - return ret.concat.apply( [], ret ); - }, - - // A global GUID counter for objects - guid: 1, - - proxy: function( fn, proxy, thisObject ) { - if ( arguments.length === 2 ) { - if ( typeof proxy === "string" ) { - thisObject = fn; - fn = thisObject[ proxy ]; - proxy = undefined; - - } else if ( proxy && !jQuery.isFunction( proxy ) ) { - thisObject = proxy; - proxy = undefined; - } - } - - if ( !proxy && fn ) { - proxy = function() { - return fn.apply( thisObject || this, arguments ); - }; - } - - // Set the guid of unique handler to the same of original handler, so it can be removed - if ( fn ) { - proxy.guid = fn.guid = fn.guid || proxy.guid || jQuery.guid++; - } - - // So proxy can be declared as an argument - return proxy; - }, - - // Use of jQuery.browser is frowned upon. - // More details: http://docs.jquery.com/Utilities/jQuery.browser - uaMatch: function( ua ) { - ua = ua.toLowerCase(); - - var match = /(webkit)[ \/]([\w.]+)/.exec( ua ) || - /(opera)(?:.*version)?[ \/]([\w.]+)/.exec( ua ) || - /(msie) ([\w.]+)/.exec( ua ) || - !/compatible/.test( ua ) && /(mozilla)(?:.*? rv:([\w.]+))?/.exec( ua ) || - []; - - return { browser: match[1] || "", version: match[2] || "0" }; - }, - - browser: {} -}); - -browserMatch = jQuery.uaMatch( userAgent ); -if ( browserMatch.browser ) { - jQuery.browser[ browserMatch.browser ] = true; - jQuery.browser.version = browserMatch.version; -} - -// Deprecated, use jQuery.browser.webkit instead -if ( jQuery.browser.webkit ) { - jQuery.browser.safari = true; -} - -if ( indexOf ) { - jQuery.inArray = function( elem, array ) { - return indexOf.call( array, elem ); - }; -} - -// All jQuery objects should point back to these -rootjQuery = jQuery(document); - -// Cleanup functions for the document ready method -if ( document.addEventListener ) { - DOMContentLoaded = function() { - document.removeEventListener( "DOMContentLoaded", DOMContentLoaded, false ); - jQuery.ready(); - }; - -} else if ( document.attachEvent ) { - DOMContentLoaded = function() { - // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443). - if ( document.readyState === "complete" ) { - document.detachEvent( "onreadystatechange", DOMContentLoaded ); - jQuery.ready(); - } - }; -} - -// The DOM ready check for Internet Explorer -function doScrollCheck() { - if ( jQuery.isReady ) { - return; - } - - try { - // If IE is used, use the trick by Diego Perini - // http://javascript.nwbox.com/IEContentLoaded/ - document.documentElement.doScroll("left"); - } catch( error ) { - setTimeout( doScrollCheck, 1 ); - return; - } - - // and execute any waiting functions - jQuery.ready(); -} - -function evalScript( i, elem ) { - if ( elem.src ) { - jQuery.ajax({ - url: elem.src, - async: false, - dataType: "script" - }); - } else { - jQuery.globalEval( elem.text || elem.textContent || elem.innerHTML || "" ); - } - - if ( elem.parentNode ) { - elem.parentNode.removeChild( elem ); - } -} - -// Mutifunctional method to get and set values to a collection -// The value/s can be optionally by executed if its a function -function access( elems, key, value, exec, fn, pass ) { - var length = elems.length; - - // Setting many attributes - if ( typeof key === "object" ) { - for ( var k in key ) { - access( elems, k, key[k], exec, fn, value ); - } - return elems; - } - - // Setting one attribute - if ( value !== undefined ) { - // Optionally, function values get executed if exec is true - exec = !pass && exec && jQuery.isFunction(value); - - for ( var i = 0; i < length; i++ ) { - fn( elems[i], key, exec ? value.call( elems[i], i, fn( elems[i], key ) ) : value, pass ); - } - - return elems; - } - - // Getting an attribute - return length ? fn( elems[0], key ) : undefined; -} - -function now() { - return (new Date).getTime(); -} -(function() { - - jQuery.support = {}; - - var root = document.documentElement, - script = document.createElement("script"), - div = document.createElement("div"), - id = "script" + now(); - - div.style.display = "none"; - div.innerHTML = "
a"; - - var all = div.getElementsByTagName("*"), - a = div.getElementsByTagName("a")[0]; - - // Can't get basic test support - if ( !all || !all.length || !a ) { - return; - } - - jQuery.support = { - // IE strips leading whitespace when .innerHTML is used - leadingWhitespace: div.firstChild.nodeType === 3, - - // Make sure that tbody elements aren't automatically inserted - // IE will insert them into empty tables - tbody: !div.getElementsByTagName("tbody").length, - - // Make sure that link elements get serialized correctly by innerHTML - // This requires a wrapper element in IE - htmlSerialize: !!div.getElementsByTagName("link").length, - - // Get the style information from getAttribute - // (IE uses .cssText insted) - style: /red/.test( a.getAttribute("style") ), - - // Make sure that URLs aren't manipulated - // (IE normalizes it by default) - hrefNormalized: a.getAttribute("href") === "/a", - - // Make sure that element opacity exists - // (IE uses filter instead) - // Use a regex to work around a WebKit issue. See #5145 - opacity: /^0.55$/.test( a.style.opacity ), - - // Verify style float existence - // (IE uses styleFloat instead of cssFloat) - cssFloat: !!a.style.cssFloat, - - // Make sure that if no value is specified for a checkbox - // that it defaults to "on". - // (WebKit defaults to "" instead) - checkOn: div.getElementsByTagName("input")[0].value === "on", - - // Make sure that a selected-by-default option has a working selected property. - // (WebKit defaults to false instead of true, IE too, if it's in an optgroup) - optSelected: document.createElement("select").appendChild( document.createElement("option") ).selected, - - parentNode: div.removeChild( div.appendChild( document.createElement("div") ) ).parentNode === null, - - // Will be defined later - deleteExpando: true, - checkClone: false, - scriptEval: false, - noCloneEvent: true, - boxModel: null - }; - - script.type = "text/javascript"; - try { - script.appendChild( document.createTextNode( "window." + id + "=1;" ) ); - } catch(e) {} - - root.insertBefore( script, root.firstChild ); - - // Make sure that the execution of code works by injecting a script - // tag with appendChild/createTextNode - // (IE doesn't support this, fails, and uses .text instead) - if ( window[ id ] ) { - jQuery.support.scriptEval = true; - delete window[ id ]; - } - - // Test to see if it's possible to delete an expando from an element - // Fails in Internet Explorer - try { - delete script.test; - - } catch(e) { - jQuery.support.deleteExpando = false; - } - - root.removeChild( script ); - - if ( div.attachEvent && div.fireEvent ) { - div.attachEvent("onclick", function click() { - // Cloning a node shouldn't copy over any - // bound event handlers (IE does this) - jQuery.support.noCloneEvent = false; - div.detachEvent("onclick", click); - }); - div.cloneNode(true).fireEvent("onclick"); - } - - div = document.createElement("div"); - div.innerHTML = ""; - - var fragment = document.createDocumentFragment(); - fragment.appendChild( div.firstChild ); - - // WebKit doesn't clone checked state correctly in fragments - jQuery.support.checkClone = fragment.cloneNode(true).cloneNode(true).lastChild.checked; - - // Figure out if the W3C box model works as expected - // document.body must exist before we can do this - jQuery(function() { - var div = document.createElement("div"); - div.style.width = div.style.paddingLeft = "1px"; - - document.body.appendChild( div ); - jQuery.boxModel = jQuery.support.boxModel = div.offsetWidth === 2; - document.body.removeChild( div ).style.display = 'none'; - - div = null; - }); - - // Technique from Juriy Zaytsev - // http://thinkweb2.com/projects/prototype/detecting-event-support-without-browser-sniffing/ - var eventSupported = function( eventName ) { - var el = document.createElement("div"); - eventName = "on" + eventName; - - var isSupported = (eventName in el); - if ( !isSupported ) { - el.setAttribute(eventName, "return;"); - isSupported = typeof el[eventName] === "function"; - } - el = null; - - return isSupported; - }; - - jQuery.support.submitBubbles = eventSupported("submit"); - jQuery.support.changeBubbles = eventSupported("change"); - - // release memory in IE - root = script = div = all = a = null; -})(); - -jQuery.props = { - "for": "htmlFor", - "class": "className", - readonly: "readOnly", - maxlength: "maxLength", - cellspacing: "cellSpacing", - rowspan: "rowSpan", - colspan: "colSpan", - tabindex: "tabIndex", - usemap: "useMap", - frameborder: "frameBorder" -}; -var expando = "jQuery" + now(), uuid = 0, windowData = {}; - -jQuery.extend({ - cache: {}, - - expando:expando, - - // The following elements throw uncatchable exceptions if you - // attempt to add expando properties to them. - noData: { - "embed": true, - "object": true, - "applet": true - }, - - data: function( elem, name, data ) { - if ( elem.nodeName && jQuery.noData[elem.nodeName.toLowerCase()] ) { - return; - } - - elem = elem == window ? - windowData : - elem; - - var id = elem[ expando ], cache = jQuery.cache, thisCache; - - if ( !id && typeof name === "string" && data === undefined ) { - return null; - } - - // Compute a unique ID for the element - if ( !id ) { - id = ++uuid; - } - - // Avoid generating a new cache unless none exists and we - // want to manipulate it. - if ( typeof name === "object" ) { - elem[ expando ] = id; - thisCache = cache[ id ] = jQuery.extend(true, {}, name); - - } else if ( !cache[ id ] ) { - elem[ expando ] = id; - cache[ id ] = {}; - } - - thisCache = cache[ id ]; - - // Prevent overriding the named cache with undefined values - if ( data !== undefined ) { - thisCache[ name ] = data; - } - - return typeof name === "string" ? thisCache[ name ] : thisCache; - }, - - removeData: function( elem, name ) { - if ( elem.nodeName && jQuery.noData[elem.nodeName.toLowerCase()] ) { - return; - } - - elem = elem == window ? - windowData : - elem; - - var id = elem[ expando ], cache = jQuery.cache, thisCache = cache[ id ]; - - // If we want to remove a specific section of the element's data - if ( name ) { - if ( thisCache ) { - // Remove the section of cache data - delete thisCache[ name ]; - - // If we've removed all the data, remove the element's cache - if ( jQuery.isEmptyObject(thisCache) ) { - jQuery.removeData( elem ); - } - } - - // Otherwise, we want to remove all of the element's data - } else { - if ( jQuery.support.deleteExpando ) { - delete elem[ jQuery.expando ]; - - } else if ( elem.removeAttribute ) { - elem.removeAttribute( jQuery.expando ); - } - - // Completely remove the data cache - delete cache[ id ]; - } - } -}); - -jQuery.fn.extend({ - data: function( key, value ) { - if ( typeof key === "undefined" && this.length ) { - return jQuery.data( this[0] ); - - } else if ( typeof key === "object" ) { - return this.each(function() { - jQuery.data( this, key ); - }); - } - - var parts = key.split("."); - parts[1] = parts[1] ? "." + parts[1] : ""; - - if ( value === undefined ) { - var data = this.triggerHandler("getData" + parts[1] + "!", [parts[0]]); - - if ( data === undefined && this.length ) { - data = jQuery.data( this[0], key ); - } - return data === undefined && parts[1] ? - this.data( parts[0] ) : - data; - } else { - return this.trigger("setData" + parts[1] + "!", [parts[0], value]).each(function() { - jQuery.data( this, key, value ); - }); - } - }, - - removeData: function( key ) { - return this.each(function() { - jQuery.removeData( this, key ); - }); - } -}); -jQuery.extend({ - queue: function( elem, type, data ) { - if ( !elem ) { - return; - } - - type = (type || "fx") + "queue"; - var q = jQuery.data( elem, type ); - - // Speed up dequeue by getting out quickly if this is just a lookup - if ( !data ) { - return q || []; - } - - if ( !q || jQuery.isArray(data) ) { - q = jQuery.data( elem, type, jQuery.makeArray(data) ); - - } else { - q.push( data ); - } - - return q; - }, - - dequeue: function( elem, type ) { - type = type || "fx"; - - var queue = jQuery.queue( elem, type ), fn = queue.shift(); - - // If the fx queue is dequeued, always remove the progress sentinel - if ( fn === "inprogress" ) { - fn = queue.shift(); - } - - if ( fn ) { - // Add a progress sentinel to prevent the fx queue from being - // automatically dequeued - if ( type === "fx" ) { - queue.unshift("inprogress"); - } - - fn.call(elem, function() { - jQuery.dequeue(elem, type); - }); - } - } -}); - -jQuery.fn.extend({ - queue: function( type, data ) { - if ( typeof type !== "string" ) { - data = type; - type = "fx"; - } - - if ( data === undefined ) { - return jQuery.queue( this[0], type ); - } - return this.each(function( i, elem ) { - var queue = jQuery.queue( this, type, data ); - - if ( type === "fx" && queue[0] !== "inprogress" ) { - jQuery.dequeue( this, type ); - } - }); - }, - dequeue: function( type ) { - return this.each(function() { - jQuery.dequeue( this, type ); - }); - }, - - // Based off of the plugin by Clint Helfers, with permission. - // http://blindsignals.com/index.php/2009/07/jquery-delay/ - delay: function( time, type ) { - time = jQuery.fx ? jQuery.fx.speeds[time] || time : time; - type = type || "fx"; - - return this.queue( type, function() { - var elem = this; - setTimeout(function() { - jQuery.dequeue( elem, type ); - }, time ); - }); - }, - - clearQueue: function( type ) { - return this.queue( type || "fx", [] ); - } -}); -var rclass = /[\n\t]/g, - rspace = /\s+/, - rreturn = /\r/g, - rspecialurl = /href|src|style/, - rtype = /(button|input)/i, - rfocusable = /(button|input|object|select|textarea)/i, - rclickable = /^(a|area)$/i, - rradiocheck = /radio|checkbox/; - -jQuery.fn.extend({ - attr: function( name, value ) { - return access( this, name, value, true, jQuery.attr ); - }, - - removeAttr: function( name, fn ) { - return this.each(function(){ - jQuery.attr( this, name, "" ); - if ( this.nodeType === 1 ) { - this.removeAttribute( name ); - } - }); - }, - - addClass: function( value ) { - if ( jQuery.isFunction(value) ) { - return this.each(function(i) { - var self = jQuery(this); - self.addClass( value.call(this, i, self.attr("class")) ); - }); - } - - if ( value && typeof value === "string" ) { - var classNames = (value || "").split( rspace ); - - for ( var i = 0, l = this.length; i < l; i++ ) { - var elem = this[i]; - - if ( elem.nodeType === 1 ) { - if ( !elem.className ) { - elem.className = value; - - } else { - var className = " " + elem.className + " ", setClass = elem.className; - for ( var c = 0, cl = classNames.length; c < cl; c++ ) { - if ( className.indexOf( " " + classNames[c] + " " ) < 0 ) { - setClass += " " + classNames[c]; - } - } - elem.className = jQuery.trim( setClass ); - } - } - } - } - - return this; - }, - - removeClass: function( value ) { - if ( jQuery.isFunction(value) ) { - return this.each(function(i) { - var self = jQuery(this); - self.removeClass( value.call(this, i, self.attr("class")) ); - }); - } - - if ( (value && typeof value === "string") || value === undefined ) { - var classNames = (value || "").split(rspace); - - for ( var i = 0, l = this.length; i < l; i++ ) { - var elem = this[i]; - - if ( elem.nodeType === 1 && elem.className ) { - if ( value ) { - var className = (" " + elem.className + " ").replace(rclass, " "); - for ( var c = 0, cl = classNames.length; c < cl; c++ ) { - className = className.replace(" " + classNames[c] + " ", " "); - } - elem.className = jQuery.trim( className ); - - } else { - elem.className = ""; - } - } - } - } - - return this; - }, - - toggleClass: function( value, stateVal ) { - var type = typeof value, isBool = typeof stateVal === "boolean"; - - if ( jQuery.isFunction( value ) ) { - return this.each(function(i) { - var self = jQuery(this); - self.toggleClass( value.call(this, i, self.attr("class"), stateVal), stateVal ); - }); - } - - return this.each(function() { - if ( type === "string" ) { - // toggle individual class names - var className, i = 0, self = jQuery(this), - state = stateVal, - classNames = value.split( rspace ); - - while ( (className = classNames[ i++ ]) ) { - // check each className given, space seperated list - state = isBool ? state : !self.hasClass( className ); - self[ state ? "addClass" : "removeClass" ]( className ); - } - - } else if ( type === "undefined" || type === "boolean" ) { - if ( this.className ) { - // store className if set - jQuery.data( this, "__className__", this.className ); - } - - // toggle whole className - this.className = this.className || value === false ? "" : jQuery.data( this, "__className__" ) || ""; - } - }); - }, - - hasClass: function( selector ) { - var className = " " + selector + " "; - for ( var i = 0, l = this.length; i < l; i++ ) { - if ( (" " + this[i].className + " ").replace(rclass, " ").indexOf( className ) > -1 ) { - return true; - } - } - - return false; - }, - - val: function( value ) { - if ( value === undefined ) { - var elem = this[0]; - - if ( elem ) { - if ( jQuery.nodeName( elem, "option" ) ) { - return (elem.attributes.value || {}).specified ? elem.value : elem.text; - } - - // We need to handle select boxes special - if ( jQuery.nodeName( elem, "select" ) ) { - var index = elem.selectedIndex, - values = [], - options = elem.options, - one = elem.type === "select-one"; - - // Nothing was selected - if ( index < 0 ) { - return null; - } - - // Loop through all the selected options - for ( var i = one ? index : 0, max = one ? index + 1 : options.length; i < max; i++ ) { - var option = options[ i ]; - - if ( option.selected ) { - // Get the specifc value for the option - value = jQuery(option).val(); - - // We don't need an array for one selects - if ( one ) { - return value; - } - - // Multi-Selects return an array - values.push( value ); - } - } - - return values; - } - - // Handle the case where in Webkit "" is returned instead of "on" if a value isn't specified - if ( rradiocheck.test( elem.type ) && !jQuery.support.checkOn ) { - return elem.getAttribute("value") === null ? "on" : elem.value; - } - - - // Everything else, we just grab the value - return (elem.value || "").replace(rreturn, ""); - - } - - return undefined; - } - - var isFunction = jQuery.isFunction(value); - - return this.each(function(i) { - var self = jQuery(this), val = value; - - if ( this.nodeType !== 1 ) { - return; - } - - if ( isFunction ) { - val = value.call(this, i, self.val()); - } - - // Typecast each time if the value is a Function and the appended - // value is therefore different each time. - if ( typeof val === "number" ) { - val += ""; - } - - if ( jQuery.isArray(val) && rradiocheck.test( this.type ) ) { - this.checked = jQuery.inArray( self.val(), val ) >= 0; - - } else if ( jQuery.nodeName( this, "select" ) ) { - var values = jQuery.makeArray(val); - - jQuery( "option", this ).each(function() { - this.selected = jQuery.inArray( jQuery(this).val(), values ) >= 0; - }); - - if ( !values.length ) { - this.selectedIndex = -1; - } - - } else { - this.value = val; - } - }); - } -}); - -jQuery.extend({ - attrFn: { - val: true, - css: true, - html: true, - text: true, - data: true, - width: true, - height: true, - offset: true - }, - - attr: function( elem, name, value, pass ) { - // don't set attributes on text and comment nodes - if ( !elem || elem.nodeType === 3 || elem.nodeType === 8 ) { - return undefined; - } - - if ( pass && name in jQuery.attrFn ) { - return jQuery(elem)[name](value); - } - - var notxml = elem.nodeType !== 1 || !jQuery.isXMLDoc( elem ), - // Whether we are setting (or getting) - set = value !== undefined; - - // Try to normalize/fix the name - name = notxml && jQuery.props[ name ] || name; - - // Only do all the following if this is a node (faster for style) - if ( elem.nodeType === 1 ) { - // These attributes require special treatment - var special = rspecialurl.test( name ); - - // Safari mis-reports the default selected property of an option - // Accessing the parent's selectedIndex property fixes it - if ( name === "selected" && !jQuery.support.optSelected ) { - var parent = elem.parentNode; - if ( parent ) { - parent.selectedIndex; - - // Make sure that it also works with optgroups, see #5701 - if ( parent.parentNode ) { - parent.parentNode.selectedIndex; - } - } - } - - // If applicable, access the attribute via the DOM 0 way - if ( name in elem && notxml && !special ) { - if ( set ) { - // We can't allow the type property to be changed (since it causes problems in IE) - if ( name === "type" && rtype.test( elem.nodeName ) && elem.parentNode ) { - jQuery.error( "type property can't be changed" ); - } - - elem[ name ] = value; - } - - // browsers index elements by id/name on forms, give priority to attributes. - if ( jQuery.nodeName( elem, "form" ) && elem.getAttributeNode(name) ) { - return elem.getAttributeNode( name ).nodeValue; - } - - // elem.tabIndex doesn't always return the correct value when it hasn't been explicitly set - // http://fluidproject.org/blog/2008/01/09/getting-setting-and-removing-tabindex-values-with-javascript/ - if ( name === "tabIndex" ) { - var attributeNode = elem.getAttributeNode( "tabIndex" ); - - return attributeNode && attributeNode.specified ? - attributeNode.value : - rfocusable.test( elem.nodeName ) || rclickable.test( elem.nodeName ) && elem.href ? - 0 : - undefined; - } - - return elem[ name ]; - } - - if ( !jQuery.support.style && notxml && name === "style" ) { - if ( set ) { - elem.style.cssText = "" + value; - } - - return elem.style.cssText; - } - - if ( set ) { - // convert the value to a string (all browsers do this but IE) see #1070 - elem.setAttribute( name, "" + value ); - } - - var attr = !jQuery.support.hrefNormalized && notxml && special ? - // Some attributes require a special call on IE - elem.getAttribute( name, 2 ) : - elem.getAttribute( name ); - - // Non-existent attributes return null, we normalize to undefined - return attr === null ? undefined : attr; - } - - // elem is actually elem.style ... set the style - // Using attr for specific style information is now deprecated. Use style instead. - return jQuery.style( elem, name, value ); - } -}); -var rnamespaces = /\.(.*)$/, - fcleanup = function( nm ) { - return nm.replace(/[^\w\s\.\|`]/g, function( ch ) { - return "\\" + ch; - }); - }; - -/* - * A number of helper functions used for managing events. - * Many of the ideas behind this code originated from - * Dean Edwards' addEvent library. - */ -jQuery.event = { - - // Bind an event to an element - // Original by Dean Edwards - add: function( elem, types, handler, data ) { - if ( elem.nodeType === 3 || elem.nodeType === 8 ) { - return; - } - - // For whatever reason, IE has trouble passing the window object - // around, causing it to be cloned in the process - if ( elem.setInterval && ( elem !== window && !elem.frameElement ) ) { - elem = window; - } - - var handleObjIn, handleObj; - - if ( handler.handler ) { - handleObjIn = handler; - handler = handleObjIn.handler; - } - - // Make sure that the function being executed has a unique ID - if ( !handler.guid ) { - handler.guid = jQuery.guid++; - } - - // Init the element's event structure - var elemData = jQuery.data( elem ); - - // If no elemData is found then we must be trying to bind to one of the - // banned noData elements - if ( !elemData ) { - return; - } - - var events = elemData.events = elemData.events || {}, - eventHandle = elemData.handle, eventHandle; - - if ( !eventHandle ) { - elemData.handle = eventHandle = function() { - // Handle the second event of a trigger and when - // an event is called after a page has unloaded - return typeof jQuery !== "undefined" && !jQuery.event.triggered ? - jQuery.event.handle.apply( eventHandle.elem, arguments ) : - undefined; - }; - } - - // Add elem as a property of the handle function - // This is to prevent a memory leak with non-native events in IE. - eventHandle.elem = elem; - - // Handle multiple events separated by a space - // jQuery(...).bind("mouseover mouseout", fn); - types = types.split(" "); - - var type, i = 0, namespaces; - - while ( (type = types[ i++ ]) ) { - handleObj = handleObjIn ? - jQuery.extend({}, handleObjIn) : - { handler: handler, data: data }; - - // Namespaced event handlers - if ( type.indexOf(".") > -1 ) { - namespaces = type.split("."); - type = namespaces.shift(); - handleObj.namespace = namespaces.slice(0).sort().join("."); - - } else { - namespaces = []; - handleObj.namespace = ""; - } - - handleObj.type = type; - handleObj.guid = handler.guid; - - // Get the current list of functions bound to this event - var handlers = events[ type ], - special = jQuery.event.special[ type ] || {}; - - // Init the event handler queue - if ( !handlers ) { - handlers = events[ type ] = []; - - // Check for a special event handler - // Only use addEventListener/attachEvent if the special - // events handler returns false - if ( !special.setup || special.setup.call( elem, data, namespaces, eventHandle ) === false ) { - // Bind the global event handler to the element - if ( elem.addEventListener ) { - elem.addEventListener( type, eventHandle, false ); - - } else if ( elem.attachEvent ) { - elem.attachEvent( "on" + type, eventHandle ); - } - } - } - - if ( special.add ) { - special.add.call( elem, handleObj ); - - if ( !handleObj.handler.guid ) { - handleObj.handler.guid = handler.guid; - } - } - - // Add the function to the element's handler list - handlers.push( handleObj ); - - // Keep track of which events have been used, for global triggering - jQuery.event.global[ type ] = true; - } - - // Nullify elem to prevent memory leaks in IE - elem = null; - }, - - global: {}, - - // Detach an event or set of events from an element - remove: function( elem, types, handler, pos ) { - // don't do events on text and comment nodes - if ( elem.nodeType === 3 || elem.nodeType === 8 ) { - return; - } - - var ret, type, fn, i = 0, all, namespaces, namespace, special, eventType, handleObj, origType, - elemData = jQuery.data( elem ), - events = elemData && elemData.events; - - if ( !elemData || !events ) { - return; - } - - // types is actually an event object here - if ( types && types.type ) { - handler = types.handler; - types = types.type; - } - - // Unbind all events for the element - if ( !types || typeof types === "string" && types.charAt(0) === "." ) { - types = types || ""; - - for ( type in events ) { - jQuery.event.remove( elem, type + types ); - } - - return; - } - - // Handle multiple events separated by a space - // jQuery(...).unbind("mouseover mouseout", fn); - types = types.split(" "); - - while ( (type = types[ i++ ]) ) { - origType = type; - handleObj = null; - all = type.indexOf(".") < 0; - namespaces = []; - - if ( !all ) { - // Namespaced event handlers - namespaces = type.split("."); - type = namespaces.shift(); - - namespace = new RegExp("(^|\\.)" + - jQuery.map( namespaces.slice(0).sort(), fcleanup ).join("\\.(?:.*\\.)?") + "(\\.|$)") - } - - eventType = events[ type ]; - - if ( !eventType ) { - continue; - } - - if ( !handler ) { - for ( var j = 0; j < eventType.length; j++ ) { - handleObj = eventType[ j ]; - - if ( all || namespace.test( handleObj.namespace ) ) { - jQuery.event.remove( elem, origType, handleObj.handler, j ); - eventType.splice( j--, 1 ); - } - } - - continue; - } - - special = jQuery.event.special[ type ] || {}; - - for ( var j = pos || 0; j < eventType.length; j++ ) { - handleObj = eventType[ j ]; - - if ( handler.guid === handleObj.guid ) { - // remove the given handler for the given type - if ( all || namespace.test( handleObj.namespace ) ) { - if ( pos == null ) { - eventType.splice( j--, 1 ); - } - - if ( special.remove ) { - special.remove.call( elem, handleObj ); - } - } - - if ( pos != null ) { - break; - } - } - } - - // remove generic event handler if no more handlers exist - if ( eventType.length === 0 || pos != null && eventType.length === 1 ) { - if ( !special.teardown || special.teardown.call( elem, namespaces ) === false ) { - removeEvent( elem, type, elemData.handle ); - } - - ret = null; - delete events[ type ]; - } - } - - // Remove the expando if it's no longer used - if ( jQuery.isEmptyObject( events ) ) { - var handle = elemData.handle; - if ( handle ) { - handle.elem = null; - } - - delete elemData.events; - delete elemData.handle; - - if ( jQuery.isEmptyObject( elemData ) ) { - jQuery.removeData( elem ); - } - } - }, - - // bubbling is internal - trigger: function( event, data, elem /*, bubbling */ ) { - // Event object or event type - var type = event.type || event, - bubbling = arguments[3]; - - if ( !bubbling ) { - event = typeof event === "object" ? - // jQuery.Event object - event[expando] ? event : - // Object literal - jQuery.extend( jQuery.Event(type), event ) : - // Just the event type (string) - jQuery.Event(type); - - if ( type.indexOf("!") >= 0 ) { - event.type = type = type.slice(0, -1); - event.exclusive = true; - } - - // Handle a global trigger - if ( !elem ) { - // Don't bubble custom events when global (to avoid too much overhead) - event.stopPropagation(); - - // Only trigger if we've ever bound an event for it - if ( jQuery.event.global[ type ] ) { - jQuery.each( jQuery.cache, function() { - if ( this.events && this.events[type] ) { - jQuery.event.trigger( event, data, this.handle.elem ); - } - }); - } - } - - // Handle triggering a single element - - // don't do events on text and comment nodes - if ( !elem || elem.nodeType === 3 || elem.nodeType === 8 ) { - return undefined; - } - - // Clean up in case it is reused - event.result = undefined; - event.target = elem; - - // Clone the incoming data, if any - data = jQuery.makeArray( data ); - data.unshift( event ); - } - - event.currentTarget = elem; - - // Trigger the event, it is assumed that "handle" is a function - var handle = jQuery.data( elem, "handle" ); - if ( handle ) { - handle.apply( elem, data ); - } - - var parent = elem.parentNode || elem.ownerDocument; - - // Trigger an inline bound script - try { - if ( !(elem && elem.nodeName && jQuery.noData[elem.nodeName.toLowerCase()]) ) { - if ( elem[ "on" + type ] && elem[ "on" + type ].apply( elem, data ) === false ) { - event.result = false; - } - } - - // prevent IE from throwing an error for some elements with some event types, see #3533 - } catch (e) {} - - if ( !event.isPropagationStopped() && parent ) { - jQuery.event.trigger( event, data, parent, true ); - - } else if ( !event.isDefaultPrevented() ) { - var target = event.target, old, - isClick = jQuery.nodeName(target, "a") && type === "click", - special = jQuery.event.special[ type ] || {}; - - if ( (!special._default || special._default.call( elem, event ) === false) && - !isClick && !(target && target.nodeName && jQuery.noData[target.nodeName.toLowerCase()]) ) { - - try { - if ( target[ type ] ) { - // Make sure that we don't accidentally re-trigger the onFOO events - old = target[ "on" + type ]; - - if ( old ) { - target[ "on" + type ] = null; - } - - jQuery.event.triggered = true; - target[ type ](); - } - - // prevent IE from throwing an error for some elements with some event types, see #3533 - } catch (e) {} - - if ( old ) { - target[ "on" + type ] = old; - } - - jQuery.event.triggered = false; - } - } - }, - - handle: function( event ) { - var all, handlers, namespaces, namespace, events; - - event = arguments[0] = jQuery.event.fix( event || window.event ); - event.currentTarget = this; - - // Namespaced event handlers - all = event.type.indexOf(".") < 0 && !event.exclusive; - - if ( !all ) { - namespaces = event.type.split("."); - event.type = namespaces.shift(); - namespace = new RegExp("(^|\\.)" + namespaces.slice(0).sort().join("\\.(?:.*\\.)?") + "(\\.|$)"); - } - - var events = jQuery.data(this, "events"), handlers = events[ event.type ]; - - if ( events && handlers ) { - // Clone the handlers to prevent manipulation - handlers = handlers.slice(0); - - for ( var j = 0, l = handlers.length; j < l; j++ ) { - var handleObj = handlers[ j ]; - - // Filter the functions by class - if ( all || namespace.test( handleObj.namespace ) ) { - // Pass in a reference to the handler function itself - // So that we can later remove it - event.handler = handleObj.handler; - event.data = handleObj.data; - event.handleObj = handleObj; - - var ret = handleObj.handler.apply( this, arguments ); - - if ( ret !== undefined ) { - event.result = ret; - if ( ret === false ) { - event.preventDefault(); - event.stopPropagation(); - } - } - - if ( event.isImmediatePropagationStopped() ) { - break; - } - } - } - } - - return event.result; - }, - - props: "altKey attrChange attrName bubbles button cancelable charCode clientX clientY ctrlKey currentTarget data detail eventPhase fromElement handler keyCode layerX layerY metaKey newValue offsetX offsetY originalTarget pageX pageY prevValue relatedNode relatedTarget screenX screenY shiftKey srcElement target toElement view wheelDelta which".split(" "), - - fix: function( event ) { - if ( event[ expando ] ) { - return event; - } - - // store a copy of the original event object - // and "clone" to set read-only properties - var originalEvent = event; - event = jQuery.Event( originalEvent ); - - for ( var i = this.props.length, prop; i; ) { - prop = this.props[ --i ]; - event[ prop ] = originalEvent[ prop ]; - } - - // Fix target property, if necessary - if ( !event.target ) { - event.target = event.srcElement || document; // Fixes #1925 where srcElement might not be defined either - } - - // check if target is a textnode (safari) - if ( event.target.nodeType === 3 ) { - event.target = event.target.parentNode; - } - - // Add relatedTarget, if necessary - if ( !event.relatedTarget && event.fromElement ) { - event.relatedTarget = event.fromElement === event.target ? event.toElement : event.fromElement; - } - - // Calculate pageX/Y if missing and clientX/Y available - if ( event.pageX == null && event.clientX != null ) { - var doc = document.documentElement, body = document.body; - event.pageX = event.clientX + (doc && doc.scrollLeft || body && body.scrollLeft || 0) - (doc && doc.clientLeft || body && body.clientLeft || 0); - event.pageY = event.clientY + (doc && doc.scrollTop || body && body.scrollTop || 0) - (doc && doc.clientTop || body && body.clientTop || 0); - } - - // Add which for key events - if ( !event.which && ((event.charCode || event.charCode === 0) ? event.charCode : event.keyCode) ) { - event.which = event.charCode || event.keyCode; - } - - // Add metaKey to non-Mac browsers (use ctrl for PC's and Meta for Macs) - if ( !event.metaKey && event.ctrlKey ) { - event.metaKey = event.ctrlKey; - } - - // Add which for click: 1 === left; 2 === middle; 3 === right - // Note: button is not normalized, so don't use it - if ( !event.which && event.button !== undefined ) { - event.which = (event.button & 1 ? 1 : ( event.button & 2 ? 3 : ( event.button & 4 ? 2 : 0 ) )); - } - - return event; - }, - - // Deprecated, use jQuery.guid instead - guid: 1E8, - - // Deprecated, use jQuery.proxy instead - proxy: jQuery.proxy, - - special: { - ready: { - // Make sure the ready event is setup - setup: jQuery.bindReady, - teardown: jQuery.noop - }, - - live: { - add: function( handleObj ) { - jQuery.event.add( this, handleObj.origType, jQuery.extend({}, handleObj, {handler: liveHandler}) ); - }, - - remove: function( handleObj ) { - var remove = true, - type = handleObj.origType.replace(rnamespaces, ""); - - jQuery.each( jQuery.data(this, "events").live || [], function() { - if ( type === this.origType.replace(rnamespaces, "") ) { - remove = false; - return false; - } - }); - - if ( remove ) { - jQuery.event.remove( this, handleObj.origType, liveHandler ); - } - } - - }, - - beforeunload: { - setup: function( data, namespaces, eventHandle ) { - // We only want to do this special case on windows - if ( this.setInterval ) { - this.onbeforeunload = eventHandle; - } - - return false; - }, - teardown: function( namespaces, eventHandle ) { - if ( this.onbeforeunload === eventHandle ) { - this.onbeforeunload = null; - } - } - } - } -}; - -var removeEvent = document.removeEventListener ? - function( elem, type, handle ) { - elem.removeEventListener( type, handle, false ); - } : - function( elem, type, handle ) { - elem.detachEvent( "on" + type, handle ); - }; - -jQuery.Event = function( src ) { - // Allow instantiation without the 'new' keyword - if ( !this.preventDefault ) { - return new jQuery.Event( src ); - } - - // Event object - if ( src && src.type ) { - this.originalEvent = src; - this.type = src.type; - // Event type - } else { - this.type = src; - } - - // timeStamp is buggy for some events on Firefox(#3843) - // So we won't rely on the native value - this.timeStamp = now(); - - // Mark it as fixed - this[ expando ] = true; -}; - -function returnFalse() { - return false; -} -function returnTrue() { - return true; -} - -// jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding -// http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html -jQuery.Event.prototype = { - preventDefault: function() { - this.isDefaultPrevented = returnTrue; - - var e = this.originalEvent; - if ( !e ) { - return; - } - - // if preventDefault exists run it on the original event - if ( e.preventDefault ) { - e.preventDefault(); - } - // otherwise set the returnValue property of the original event to false (IE) - e.returnValue = false; - }, - stopPropagation: function() { - this.isPropagationStopped = returnTrue; - - var e = this.originalEvent; - if ( !e ) { - return; - } - // if stopPropagation exists run it on the original event - if ( e.stopPropagation ) { - e.stopPropagation(); - } - // otherwise set the cancelBubble property of the original event to true (IE) - e.cancelBubble = true; - }, - stopImmediatePropagation: function() { - this.isImmediatePropagationStopped = returnTrue; - this.stopPropagation(); - }, - isDefaultPrevented: returnFalse, - isPropagationStopped: returnFalse, - isImmediatePropagationStopped: returnFalse -}; - -// Checks if an event happened on an element within another element -// Used in jQuery.event.special.mouseenter and mouseleave handlers -var withinElement = function( event ) { - // Check if mouse(over|out) are still within the same parent element - var parent = event.relatedTarget; - - // Firefox sometimes assigns relatedTarget a XUL element - // which we cannot access the parentNode property of - try { - // Traverse up the tree - while ( parent && parent !== this ) { - parent = parent.parentNode; - } - - if ( parent !== this ) { - // set the correct event type - event.type = event.data; - - // handle event if we actually just moused on to a non sub-element - jQuery.event.handle.apply( this, arguments ); - } - - // assuming we've left the element since we most likely mousedover a xul element - } catch(e) { } -}, - -// In case of event delegation, we only need to rename the event.type, -// liveHandler will take care of the rest. -delegate = function( event ) { - event.type = event.data; - jQuery.event.handle.apply( this, arguments ); -}; - -// Create mouseenter and mouseleave events -jQuery.each({ - mouseenter: "mouseover", - mouseleave: "mouseout" -}, function( orig, fix ) { - jQuery.event.special[ orig ] = { - setup: function( data ) { - jQuery.event.add( this, fix, data && data.selector ? delegate : withinElement, orig ); - }, - teardown: function( data ) { - jQuery.event.remove( this, fix, data && data.selector ? delegate : withinElement ); - } - }; -}); - -// submit delegation -if ( !jQuery.support.submitBubbles ) { - - jQuery.event.special.submit = { - setup: function( data, namespaces ) { - if ( this.nodeName.toLowerCase() !== "form" ) { - jQuery.event.add(this, "click.specialSubmit", function( e ) { - var elem = e.target, type = elem.type; - - if ( (type === "submit" || type === "image") && jQuery( elem ).closest("form").length ) { - return trigger( "submit", this, arguments ); - } - }); - - jQuery.event.add(this, "keypress.specialSubmit", function( e ) { - var elem = e.target, type = elem.type; - - if ( (type === "text" || type === "password") && jQuery( elem ).closest("form").length && e.keyCode === 13 ) { - return trigger( "submit", this, arguments ); - } - }); - - } else { - return false; - } - }, - - teardown: function( namespaces ) { - jQuery.event.remove( this, ".specialSubmit" ); - } - }; - -} - -// change delegation, happens here so we have bind. -if ( !jQuery.support.changeBubbles ) { - - var formElems = /textarea|input|select/i, - - changeFilters, - - getVal = function( elem ) { - var type = elem.type, val = elem.value; - - if ( type === "radio" || type === "checkbox" ) { - val = elem.checked; - - } else if ( type === "select-multiple" ) { - val = elem.selectedIndex > -1 ? - jQuery.map( elem.options, function( elem ) { - return elem.selected; - }).join("-") : - ""; - - } else if ( elem.nodeName.toLowerCase() === "select" ) { - val = elem.selectedIndex; - } - - return val; - }, - - testChange = function testChange( e ) { - var elem = e.target, data, val; - - if ( !formElems.test( elem.nodeName ) || elem.readOnly ) { - return; - } - - data = jQuery.data( elem, "_change_data" ); - val = getVal(elem); - - // the current data will be also retrieved by beforeactivate - if ( e.type !== "focusout" || elem.type !== "radio" ) { - jQuery.data( elem, "_change_data", val ); - } - - if ( data === undefined || val === data ) { - return; - } - - if ( data != null || val ) { - e.type = "change"; - return jQuery.event.trigger( e, arguments[1], elem ); - } - }; - - jQuery.event.special.change = { - filters: { - focusout: testChange, - - click: function( e ) { - var elem = e.target, type = elem.type; - - if ( type === "radio" || type === "checkbox" || elem.nodeName.toLowerCase() === "select" ) { - return testChange.call( this, e ); - } - }, - - // Change has to be called before submit - // Keydown will be called before keypress, which is used in submit-event delegation - keydown: function( e ) { - var elem = e.target, type = elem.type; - - if ( (e.keyCode === 13 && elem.nodeName.toLowerCase() !== "textarea") || - (e.keyCode === 32 && (type === "checkbox" || type === "radio")) || - type === "select-multiple" ) { - return testChange.call( this, e ); - } - }, - - // Beforeactivate happens also before the previous element is blurred - // with this event you can't trigger a change event, but you can store - // information/focus[in] is not needed anymore - beforeactivate: function( e ) { - var elem = e.target; - jQuery.data( elem, "_change_data", getVal(elem) ); - } - }, - - setup: function( data, namespaces ) { - if ( this.type === "file" ) { - return false; - } - - for ( var type in changeFilters ) { - jQuery.event.add( this, type + ".specialChange", changeFilters[type] ); - } - - return formElems.test( this.nodeName ); - }, - - teardown: function( namespaces ) { - jQuery.event.remove( this, ".specialChange" ); - - return formElems.test( this.nodeName ); - } - }; - - changeFilters = jQuery.event.special.change.filters; -} - -function trigger( type, elem, args ) { - args[0].type = type; - return jQuery.event.handle.apply( elem, args ); -} - -// Create "bubbling" focus and blur events -if ( document.addEventListener ) { - jQuery.each({ focus: "focusin", blur: "focusout" }, function( orig, fix ) { - jQuery.event.special[ fix ] = { - setup: function() { - this.addEventListener( orig, handler, true ); - }, - teardown: function() { - this.removeEventListener( orig, handler, true ); - } - }; - - function handler( e ) { - e = jQuery.event.fix( e ); - e.type = fix; - return jQuery.event.handle.call( this, e ); - } - }); -} - -jQuery.each(["bind", "one"], function( i, name ) { - jQuery.fn[ name ] = function( type, data, fn ) { - // Handle object literals - if ( typeof type === "object" ) { - for ( var key in type ) { - this[ name ](key, data, type[key], fn); - } - return this; - } - - if ( jQuery.isFunction( data ) ) { - fn = data; - data = undefined; - } - - var handler = name === "one" ? jQuery.proxy( fn, function( event ) { - jQuery( this ).unbind( event, handler ); - return fn.apply( this, arguments ); - }) : fn; - - if ( type === "unload" && name !== "one" ) { - this.one( type, data, fn ); - - } else { - for ( var i = 0, l = this.length; i < l; i++ ) { - jQuery.event.add( this[i], type, handler, data ); - } - } - - return this; - }; -}); - -jQuery.fn.extend({ - unbind: function( type, fn ) { - // Handle object literals - if ( typeof type === "object" && !type.preventDefault ) { - for ( var key in type ) { - this.unbind(key, type[key]); - } - - } else { - for ( var i = 0, l = this.length; i < l; i++ ) { - jQuery.event.remove( this[i], type, fn ); - } - } - - return this; - }, - - delegate: function( selector, types, data, fn ) { - return this.live( types, data, fn, selector ); - }, - - undelegate: function( selector, types, fn ) { - if ( arguments.length === 0 ) { - return this.unbind( "live" ); - - } else { - return this.die( types, null, fn, selector ); - } - }, - - trigger: function( type, data ) { - return this.each(function() { - jQuery.event.trigger( type, data, this ); - }); - }, - - triggerHandler: function( type, data ) { - if ( this[0] ) { - var event = jQuery.Event( type ); - event.preventDefault(); - event.stopPropagation(); - jQuery.event.trigger( event, data, this[0] ); - return event.result; - } - }, - - toggle: function( fn ) { - // Save reference to arguments for access in closure - var args = arguments, i = 1; - - // link all the functions, so any of them can unbind this click handler - while ( i < args.length ) { - jQuery.proxy( fn, args[ i++ ] ); - } - - return this.click( jQuery.proxy( fn, function( event ) { - // Figure out which function to execute - var lastToggle = ( jQuery.data( this, "lastToggle" + fn.guid ) || 0 ) % i; - jQuery.data( this, "lastToggle" + fn.guid, lastToggle + 1 ); - - // Make sure that clicks stop - event.preventDefault(); - - // and execute the function - return args[ lastToggle ].apply( this, arguments ) || false; - })); - }, - - hover: function( fnOver, fnOut ) { - return this.mouseenter( fnOver ).mouseleave( fnOut || fnOver ); - } -}); - -var liveMap = { - focus: "focusin", - blur: "focusout", - mouseenter: "mouseover", - mouseleave: "mouseout" -}; - -jQuery.each(["live", "die"], function( i, name ) { - jQuery.fn[ name ] = function( types, data, fn, origSelector /* Internal Use Only */ ) { - var type, i = 0, match, namespaces, preType, - selector = origSelector || this.selector, - context = origSelector ? this : jQuery( this.context ); - - if ( jQuery.isFunction( data ) ) { - fn = data; - data = undefined; - } - - types = (types || "").split(" "); - - while ( (type = types[ i++ ]) != null ) { - match = rnamespaces.exec( type ); - namespaces = ""; - - if ( match ) { - namespaces = match[0]; - type = type.replace( rnamespaces, "" ); - } - - if ( type === "hover" ) { - types.push( "mouseenter" + namespaces, "mouseleave" + namespaces ); - continue; - } - - preType = type; - - if ( type === "focus" || type === "blur" ) { - types.push( liveMap[ type ] + namespaces ); - type = type + namespaces; - - } else { - type = (liveMap[ type ] || type) + namespaces; - } - - if ( name === "live" ) { - // bind live handler - context.each(function(){ - jQuery.event.add( this, liveConvert( type, selector ), - { data: data, selector: selector, handler: fn, origType: type, origHandler: fn, preType: preType } ); - }); - - } else { - // unbind live handler - context.unbind( liveConvert( type, selector ), fn ); - } - } - - return this; - } -}); - -function liveHandler( event ) { - var stop, elems = [], selectors = [], args = arguments, - related, match, handleObj, elem, j, i, l, data, - events = jQuery.data( this, "events" ); - - // Make sure we avoid non-left-click bubbling in Firefox (#3861) - if ( event.liveFired === this || !events || !events.live || event.button && event.type === "click" ) { - return; - } - - event.liveFired = this; - - var live = events.live.slice(0); - - for ( j = 0; j < live.length; j++ ) { - handleObj = live[j]; - - if ( handleObj.origType.replace( rnamespaces, "" ) === event.type ) { - selectors.push( handleObj.selector ); - - } else { - live.splice( j--, 1 ); - } - } - - match = jQuery( event.target ).closest( selectors, event.currentTarget ); - - for ( i = 0, l = match.length; i < l; i++ ) { - for ( j = 0; j < live.length; j++ ) { - handleObj = live[j]; - - if ( match[i].selector === handleObj.selector ) { - elem = match[i].elem; - related = null; - - // Those two events require additional checking - if ( handleObj.preType === "mouseenter" || handleObj.preType === "mouseleave" ) { - related = jQuery( event.relatedTarget ).closest( handleObj.selector )[0]; - } - - if ( !related || related !== elem ) { - elems.push({ elem: elem, handleObj: handleObj }); - } - } - } - } - - for ( i = 0, l = elems.length; i < l; i++ ) { - match = elems[i]; - event.currentTarget = match.elem; - event.data = match.handleObj.data; - event.handleObj = match.handleObj; - - if ( match.handleObj.origHandler.apply( match.elem, args ) === false ) { - stop = false; - break; - } - } - - return stop; -} - -function liveConvert( type, selector ) { - return "live." + (type && type !== "*" ? type + "." : "") + selector.replace(/\./g, "`").replace(/ /g, "&"); -} - -jQuery.each( ("blur focus focusin focusout load resize scroll unload click dblclick " + - "mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave " + - "change select submit keydown keypress keyup error").split(" "), function( i, name ) { - - // Handle event binding - jQuery.fn[ name ] = function( fn ) { - return fn ? this.bind( name, fn ) : this.trigger( name ); - }; - - if ( jQuery.attrFn ) { - jQuery.attrFn[ name ] = true; - } -}); - -// Prevent memory leaks in IE -// Window isn't included so as not to unbind existing unload events -// More info: -// - http://isaacschlueter.com/2006/10/msie-memory-leaks/ -if ( window.attachEvent && !window.addEventListener ) { - window.attachEvent("onunload", function() { - for ( var id in jQuery.cache ) { - if ( jQuery.cache[ id ].handle ) { - // Try/Catch is to handle iframes being unloaded, see #4280 - try { - jQuery.event.remove( jQuery.cache[ id ].handle.elem ); - } catch(e) {} - } - } - }); -} -/*! - * Sizzle CSS Selector Engine - v1.0 - * Copyright 2009, The Dojo Foundation - * Released under the MIT, BSD, and GPL Licenses. - * More information: http://sizzlejs.com/ - */ -(function(){ - -var chunker = /((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^[\]]*\]|['"][^'"]*['"]|[^[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g, - done = 0, - toString = Object.prototype.toString, - hasDuplicate = false, - baseHasDuplicate = true; - -// Here we check if the JavaScript engine is using some sort of -// optimization where it does not always call our comparision -// function. If that is the case, discard the hasDuplicate value. -// Thus far that includes Google Chrome. -[0, 0].sort(function(){ - baseHasDuplicate = false; - return 0; -}); - -var Sizzle = function(selector, context, results, seed) { - results = results || []; - var origContext = context = context || document; - - if ( context.nodeType !== 1 && context.nodeType !== 9 ) { - return []; - } - - if ( !selector || typeof selector !== "string" ) { - return results; - } - - var parts = [], m, set, checkSet, extra, prune = true, contextXML = isXML(context), - soFar = selector; - - // Reset the position of the chunker regexp (start from head) - while ( (chunker.exec(""), m = chunker.exec(soFar)) !== null ) { - soFar = m[3]; - - parts.push( m[1] ); - - if ( m[2] ) { - extra = m[3]; - break; - } - } - - if ( parts.length > 1 && origPOS.exec( selector ) ) { - if ( parts.length === 2 && Expr.relative[ parts[0] ] ) { - set = posProcess( parts[0] + parts[1], context ); - } else { - set = Expr.relative[ parts[0] ] ? - [ context ] : - Sizzle( parts.shift(), context ); - - while ( parts.length ) { - selector = parts.shift(); - - if ( Expr.relative[ selector ] ) { - selector += parts.shift(); - } - - set = posProcess( selector, set ); - } - } - } else { - // Take a shortcut and set the context if the root selector is an ID - // (but not if it'll be faster if the inner selector is an ID) - if ( !seed && parts.length > 1 && context.nodeType === 9 && !contextXML && - Expr.match.ID.test(parts[0]) && !Expr.match.ID.test(parts[parts.length - 1]) ) { - var ret = Sizzle.find( parts.shift(), context, contextXML ); - context = ret.expr ? Sizzle.filter( ret.expr, ret.set )[0] : ret.set[0]; - } - - if ( context ) { - var ret = seed ? - { expr: parts.pop(), set: makeArray(seed) } : - Sizzle.find( parts.pop(), parts.length === 1 && (parts[0] === "~" || parts[0] === "+") && context.parentNode ? context.parentNode : context, contextXML ); - set = ret.expr ? Sizzle.filter( ret.expr, ret.set ) : ret.set; - - if ( parts.length > 0 ) { - checkSet = makeArray(set); - } else { - prune = false; - } - - while ( parts.length ) { - var cur = parts.pop(), pop = cur; - - if ( !Expr.relative[ cur ] ) { - cur = ""; - } else { - pop = parts.pop(); - } - - if ( pop == null ) { - pop = context; - } - - Expr.relative[ cur ]( checkSet, pop, contextXML ); - } - } else { - checkSet = parts = []; - } - } - - if ( !checkSet ) { - checkSet = set; - } - - if ( !checkSet ) { - Sizzle.error( cur || selector ); - } - - if ( toString.call(checkSet) === "[object Array]" ) { - if ( !prune ) { - results.push.apply( results, checkSet ); - } else if ( context && context.nodeType === 1 ) { - for ( var i = 0; checkSet[i] != null; i++ ) { - if ( checkSet[i] && (checkSet[i] === true || checkSet[i].nodeType === 1 && contains(context, checkSet[i])) ) { - results.push( set[i] ); - } - } - } else { - for ( var i = 0; checkSet[i] != null; i++ ) { - if ( checkSet[i] && checkSet[i].nodeType === 1 ) { - results.push( set[i] ); - } - } - } - } else { - makeArray( checkSet, results ); - } - - if ( extra ) { - Sizzle( extra, origContext, results, seed ); - Sizzle.uniqueSort( results ); - } - - return results; -}; - -Sizzle.uniqueSort = function(results){ - if ( sortOrder ) { - hasDuplicate = baseHasDuplicate; - results.sort(sortOrder); - - if ( hasDuplicate ) { - for ( var i = 1; i < results.length; i++ ) { - if ( results[i] === results[i-1] ) { - results.splice(i--, 1); - } - } - } - } - - return results; -}; - -Sizzle.matches = function(expr, set){ - return Sizzle(expr, null, null, set); -}; - -Sizzle.find = function(expr, context, isXML){ - var set, match; - - if ( !expr ) { - return []; - } - - for ( var i = 0, l = Expr.order.length; i < l; i++ ) { - var type = Expr.order[i], match; - - if ( (match = Expr.leftMatch[ type ].exec( expr )) ) { - var left = match[1]; - match.splice(1,1); - - if ( left.substr( left.length - 1 ) !== "\\" ) { - match[1] = (match[1] || "").replace(/\\/g, ""); - set = Expr.find[ type ]( match, context, isXML ); - if ( set != null ) { - expr = expr.replace( Expr.match[ type ], "" ); - break; - } - } - } - } - - if ( !set ) { - set = context.getElementsByTagName("*"); - } - - return {set: set, expr: expr}; -}; - -Sizzle.filter = function(expr, set, inplace, not){ - var old = expr, result = [], curLoop = set, match, anyFound, - isXMLFilter = set && set[0] && isXML(set[0]); - - while ( expr && set.length ) { - for ( var type in Expr.filter ) { - if ( (match = Expr.leftMatch[ type ].exec( expr )) != null && match[2] ) { - var filter = Expr.filter[ type ], found, item, left = match[1]; - anyFound = false; - - match.splice(1,1); - - if ( left.substr( left.length - 1 ) === "\\" ) { - continue; - } - - if ( curLoop === result ) { - result = []; - } - - if ( Expr.preFilter[ type ] ) { - match = Expr.preFilter[ type ]( match, curLoop, inplace, result, not, isXMLFilter ); - - if ( !match ) { - anyFound = found = true; - } else if ( match === true ) { - continue; - } - } - - if ( match ) { - for ( var i = 0; (item = curLoop[i]) != null; i++ ) { - if ( item ) { - found = filter( item, match, i, curLoop ); - var pass = not ^ !!found; - - if ( inplace && found != null ) { - if ( pass ) { - anyFound = true; - } else { - curLoop[i] = false; - } - } else if ( pass ) { - result.push( item ); - anyFound = true; - } - } - } - } - - if ( found !== undefined ) { - if ( !inplace ) { - curLoop = result; - } - - expr = expr.replace( Expr.match[ type ], "" ); - - if ( !anyFound ) { - return []; - } - - break; - } - } - } - - // Improper expression - if ( expr === old ) { - if ( anyFound == null ) { - Sizzle.error( expr ); - } else { - break; - } - } - - old = expr; - } - - return curLoop; -}; - -Sizzle.error = function( msg ) { - throw "Syntax error, unrecognized expression: " + msg; -}; - -var Expr = Sizzle.selectors = { - order: [ "ID", "NAME", "TAG" ], - match: { - ID: /#((?:[\w\u00c0-\uFFFF-]|\\.)+)/, - CLASS: /\.((?:[\w\u00c0-\uFFFF-]|\\.)+)/, - NAME: /\[name=['"]*((?:[\w\u00c0-\uFFFF-]|\\.)+)['"]*\]/, - ATTR: /\[\s*((?:[\w\u00c0-\uFFFF-]|\\.)+)\s*(?:(\S?=)\s*(['"]*)(.*?)\3|)\s*\]/, - TAG: /^((?:[\w\u00c0-\uFFFF\*-]|\\.)+)/, - CHILD: /:(only|nth|last|first)-child(?:\((even|odd|[\dn+-]*)\))?/, - POS: /:(nth|eq|gt|lt|first|last|even|odd)(?:\((\d*)\))?(?=[^-]|$)/, - PSEUDO: /:((?:[\w\u00c0-\uFFFF-]|\\.)+)(?:\((['"]?)((?:\([^\)]+\)|[^\(\)]*)+)\2\))?/ - }, - leftMatch: {}, - attrMap: { - "class": "className", - "for": "htmlFor" - }, - attrHandle: { - href: function(elem){ - return elem.getAttribute("href"); - } - }, - relative: { - "+": function(checkSet, part){ - var isPartStr = typeof part === "string", - isTag = isPartStr && !/\W/.test(part), - isPartStrNotTag = isPartStr && !isTag; - - if ( isTag ) { - part = part.toLowerCase(); - } - - for ( var i = 0, l = checkSet.length, elem; i < l; i++ ) { - if ( (elem = checkSet[i]) ) { - while ( (elem = elem.previousSibling) && elem.nodeType !== 1 ) {} - - checkSet[i] = isPartStrNotTag || elem && elem.nodeName.toLowerCase() === part ? - elem || false : - elem === part; - } - } - - if ( isPartStrNotTag ) { - Sizzle.filter( part, checkSet, true ); - } - }, - ">": function(checkSet, part){ - var isPartStr = typeof part === "string"; - - if ( isPartStr && !/\W/.test(part) ) { - part = part.toLowerCase(); - - for ( var i = 0, l = checkSet.length; i < l; i++ ) { - var elem = checkSet[i]; - if ( elem ) { - var parent = elem.parentNode; - checkSet[i] = parent.nodeName.toLowerCase() === part ? parent : false; - } - } - } else { - for ( var i = 0, l = checkSet.length; i < l; i++ ) { - var elem = checkSet[i]; - if ( elem ) { - checkSet[i] = isPartStr ? - elem.parentNode : - elem.parentNode === part; - } - } - - if ( isPartStr ) { - Sizzle.filter( part, checkSet, true ); - } - } - }, - "": function(checkSet, part, isXML){ - var doneName = done++, checkFn = dirCheck; - - if ( typeof part === "string" && !/\W/.test(part) ) { - var nodeCheck = part = part.toLowerCase(); - checkFn = dirNodeCheck; - } - - checkFn("parentNode", part, doneName, checkSet, nodeCheck, isXML); - }, - "~": function(checkSet, part, isXML){ - var doneName = done++, checkFn = dirCheck; - - if ( typeof part === "string" && !/\W/.test(part) ) { - var nodeCheck = part = part.toLowerCase(); - checkFn = dirNodeCheck; - } - - checkFn("previousSibling", part, doneName, checkSet, nodeCheck, isXML); - } - }, - find: { - ID: function(match, context, isXML){ - if ( typeof context.getElementById !== "undefined" && !isXML ) { - var m = context.getElementById(match[1]); - return m ? [m] : []; - } - }, - NAME: function(match, context){ - if ( typeof context.getElementsByName !== "undefined" ) { - var ret = [], results = context.getElementsByName(match[1]); - - for ( var i = 0, l = results.length; i < l; i++ ) { - if ( results[i].getAttribute("name") === match[1] ) { - ret.push( results[i] ); - } - } - - return ret.length === 0 ? null : ret; - } - }, - TAG: function(match, context){ - return context.getElementsByTagName(match[1]); - } - }, - preFilter: { - CLASS: function(match, curLoop, inplace, result, not, isXML){ - match = " " + match[1].replace(/\\/g, "") + " "; - - if ( isXML ) { - return match; - } - - for ( var i = 0, elem; (elem = curLoop[i]) != null; i++ ) { - if ( elem ) { - if ( not ^ (elem.className && (" " + elem.className + " ").replace(/[\t\n]/g, " ").indexOf(match) >= 0) ) { - if ( !inplace ) { - result.push( elem ); - } - } else if ( inplace ) { - curLoop[i] = false; - } - } - } - - return false; - }, - ID: function(match){ - return match[1].replace(/\\/g, ""); - }, - TAG: function(match, curLoop){ - return match[1].toLowerCase(); - }, - CHILD: function(match){ - if ( match[1] === "nth" ) { - // parse equations like 'even', 'odd', '5', '2n', '3n+2', '4n-1', '-n+6' - var test = /(-?)(\d*)n((?:\+|-)?\d*)/.exec( - match[2] === "even" && "2n" || match[2] === "odd" && "2n+1" || - !/\D/.test( match[2] ) && "0n+" + match[2] || match[2]); - - // calculate the numbers (first)n+(last) including if they are negative - match[2] = (test[1] + (test[2] || 1)) - 0; - match[3] = test[3] - 0; - } - - // TODO: Move to normal caching system - match[0] = done++; - - return match; - }, - ATTR: function(match, curLoop, inplace, result, not, isXML){ - var name = match[1].replace(/\\/g, ""); - - if ( !isXML && Expr.attrMap[name] ) { - match[1] = Expr.attrMap[name]; - } - - if ( match[2] === "~=" ) { - match[4] = " " + match[4] + " "; - } - - return match; - }, - PSEUDO: function(match, curLoop, inplace, result, not){ - if ( match[1] === "not" ) { - // If we're dealing with a complex expression, or a simple one - if ( ( chunker.exec(match[3]) || "" ).length > 1 || /^\w/.test(match[3]) ) { - match[3] = Sizzle(match[3], null, null, curLoop); - } else { - var ret = Sizzle.filter(match[3], curLoop, inplace, true ^ not); - if ( !inplace ) { - result.push.apply( result, ret ); - } - return false; - } - } else if ( Expr.match.POS.test( match[0] ) || Expr.match.CHILD.test( match[0] ) ) { - return true; - } - - return match; - }, - POS: function(match){ - match.unshift( true ); - return match; - } - }, - filters: { - enabled: function(elem){ - return elem.disabled === false && elem.type !== "hidden"; - }, - disabled: function(elem){ - return elem.disabled === true; - }, - checked: function(elem){ - return elem.checked === true; - }, - selected: function(elem){ - // Accessing this property makes selected-by-default - // options in Safari work properly - elem.parentNode.selectedIndex; - return elem.selected === true; - }, - parent: function(elem){ - return !!elem.firstChild; - }, - empty: function(elem){ - return !elem.firstChild; - }, - has: function(elem, i, match){ - return !!Sizzle( match[3], elem ).length; - }, - header: function(elem){ - return /h\d/i.test( elem.nodeName ); - }, - text: function(elem){ - return "text" === elem.type; - }, - radio: function(elem){ - return "radio" === elem.type; - }, - checkbox: function(elem){ - return "checkbox" === elem.type; - }, - file: function(elem){ - return "file" === elem.type; - }, - password: function(elem){ - return "password" === elem.type; - }, - submit: function(elem){ - return "submit" === elem.type; - }, - image: function(elem){ - return "image" === elem.type; - }, - reset: function(elem){ - return "reset" === elem.type; - }, - button: function(elem){ - return "button" === elem.type || elem.nodeName.toLowerCase() === "button"; - }, - input: function(elem){ - return /input|select|textarea|button/i.test(elem.nodeName); - } - }, - setFilters: { - first: function(elem, i){ - return i === 0; - }, - last: function(elem, i, match, array){ - return i === array.length - 1; - }, - even: function(elem, i){ - return i % 2 === 0; - }, - odd: function(elem, i){ - return i % 2 === 1; - }, - lt: function(elem, i, match){ - return i < match[3] - 0; - }, - gt: function(elem, i, match){ - return i > match[3] - 0; - }, - nth: function(elem, i, match){ - return match[3] - 0 === i; - }, - eq: function(elem, i, match){ - return match[3] - 0 === i; - } - }, - filter: { - PSEUDO: function(elem, match, i, array){ - var name = match[1], filter = Expr.filters[ name ]; - - if ( filter ) { - return filter( elem, i, match, array ); - } else if ( name === "contains" ) { - return (elem.textContent || elem.innerText || getText([ elem ]) || "").indexOf(match[3]) >= 0; - } else if ( name === "not" ) { - var not = match[3]; - - for ( var i = 0, l = not.length; i < l; i++ ) { - if ( not[i] === elem ) { - return false; - } - } - - return true; - } else { - Sizzle.error( "Syntax error, unrecognized expression: " + name ); - } - }, - CHILD: function(elem, match){ - var type = match[1], node = elem; - switch (type) { - case 'only': - case 'first': - while ( (node = node.previousSibling) ) { - if ( node.nodeType === 1 ) { - return false; - } - } - if ( type === "first" ) { - return true; - } - node = elem; - case 'last': - while ( (node = node.nextSibling) ) { - if ( node.nodeType === 1 ) { - return false; - } - } - return true; - case 'nth': - var first = match[2], last = match[3]; - - if ( first === 1 && last === 0 ) { - return true; - } - - var doneName = match[0], - parent = elem.parentNode; - - if ( parent && (parent.sizcache !== doneName || !elem.nodeIndex) ) { - var count = 0; - for ( node = parent.firstChild; node; node = node.nextSibling ) { - if ( node.nodeType === 1 ) { - node.nodeIndex = ++count; - } - } - parent.sizcache = doneName; - } - - var diff = elem.nodeIndex - last; - if ( first === 0 ) { - return diff === 0; - } else { - return ( diff % first === 0 && diff / first >= 0 ); - } - } - }, - ID: function(elem, match){ - return elem.nodeType === 1 && elem.getAttribute("id") === match; - }, - TAG: function(elem, match){ - return (match === "*" && elem.nodeType === 1) || elem.nodeName.toLowerCase() === match; - }, - CLASS: function(elem, match){ - return (" " + (elem.className || elem.getAttribute("class")) + " ") - .indexOf( match ) > -1; - }, - ATTR: function(elem, match){ - var name = match[1], - result = Expr.attrHandle[ name ] ? - Expr.attrHandle[ name ]( elem ) : - elem[ name ] != null ? - elem[ name ] : - elem.getAttribute( name ), - value = result + "", - type = match[2], - check = match[4]; - - return result == null ? - type === "!=" : - type === "=" ? - value === check : - type === "*=" ? - value.indexOf(check) >= 0 : - type === "~=" ? - (" " + value + " ").indexOf(check) >= 0 : - !check ? - value && result !== false : - type === "!=" ? - value !== check : - type === "^=" ? - value.indexOf(check) === 0 : - type === "$=" ? - value.substr(value.length - check.length) === check : - type === "|=" ? - value === check || value.substr(0, check.length + 1) === check + "-" : - false; - }, - POS: function(elem, match, i, array){ - var name = match[2], filter = Expr.setFilters[ name ]; - - if ( filter ) { - return filter( elem, i, match, array ); - } - } - } -}; - -var origPOS = Expr.match.POS; - -for ( var type in Expr.match ) { - Expr.match[ type ] = new RegExp( Expr.match[ type ].source + /(?![^\[]*\])(?![^\(]*\))/.source ); - Expr.leftMatch[ type ] = new RegExp( /(^(?:.|\r|\n)*?)/.source + Expr.match[ type ].source.replace(/\\(\d+)/g, function(all, num){ - return "\\" + (num - 0 + 1); - })); -} - -var makeArray = function(array, results) { - array = Array.prototype.slice.call( array, 0 ); - - if ( results ) { - results.push.apply( results, array ); - return results; - } - - return array; -}; - -// Perform a simple check to determine if the browser is capable of -// converting a NodeList to an array using builtin methods. -// Also verifies that the returned array holds DOM nodes -// (which is not the case in the Blackberry browser) -try { - Array.prototype.slice.call( document.documentElement.childNodes, 0 )[0].nodeType; - -// Provide a fallback method if it does not work -} catch(e){ - makeArray = function(array, results) { - var ret = results || []; - - if ( toString.call(array) === "[object Array]" ) { - Array.prototype.push.apply( ret, array ); - } else { - if ( typeof array.length === "number" ) { - for ( var i = 0, l = array.length; i < l; i++ ) { - ret.push( array[i] ); - } - } else { - for ( var i = 0; array[i]; i++ ) { - ret.push( array[i] ); - } - } - } - - return ret; - }; -} - -var sortOrder; - -if ( document.documentElement.compareDocumentPosition ) { - sortOrder = function( a, b ) { - if ( !a.compareDocumentPosition || !b.compareDocumentPosition ) { - if ( a == b ) { - hasDuplicate = true; - } - return a.compareDocumentPosition ? -1 : 1; - } - - var ret = a.compareDocumentPosition(b) & 4 ? -1 : a === b ? 0 : 1; - if ( ret === 0 ) { - hasDuplicate = true; - } - return ret; - }; -} else if ( "sourceIndex" in document.documentElement ) { - sortOrder = function( a, b ) { - if ( !a.sourceIndex || !b.sourceIndex ) { - if ( a == b ) { - hasDuplicate = true; - } - return a.sourceIndex ? -1 : 1; - } - - var ret = a.sourceIndex - b.sourceIndex; - if ( ret === 0 ) { - hasDuplicate = true; - } - return ret; - }; -} else if ( document.createRange ) { - sortOrder = function( a, b ) { - if ( !a.ownerDocument || !b.ownerDocument ) { - if ( a == b ) { - hasDuplicate = true; - } - return a.ownerDocument ? -1 : 1; - } - - var aRange = a.ownerDocument.createRange(), bRange = b.ownerDocument.createRange(); - aRange.setStart(a, 0); - aRange.setEnd(a, 0); - bRange.setStart(b, 0); - bRange.setEnd(b, 0); - var ret = aRange.compareBoundaryPoints(Range.START_TO_END, bRange); - if ( ret === 0 ) { - hasDuplicate = true; - } - return ret; - }; -} - -// Utility function for retreiving the text value of an array of DOM nodes -function getText( elems ) { - var ret = "", elem; - - for ( var i = 0; elems[i]; i++ ) { - elem = elems[i]; - - // Get the text from text nodes and CDATA nodes - if ( elem.nodeType === 3 || elem.nodeType === 4 ) { - ret += elem.nodeValue; - - // Traverse everything else, except comment nodes - } else if ( elem.nodeType !== 8 ) { - ret += getText( elem.childNodes ); - } - } - - return ret; -} - -// Check to see if the browser returns elements by name when -// querying by getElementById (and provide a workaround) -(function(){ - // We're going to inject a fake input element with a specified name - var form = document.createElement("div"), - id = "script" + (new Date).getTime(); - form.innerHTML = ""; - - // Inject it into the root element, check its status, and remove it quickly - var root = document.documentElement; - root.insertBefore( form, root.firstChild ); - - // The workaround has to do additional checks after a getElementById - // Which slows things down for other browsers (hence the branching) - if ( document.getElementById( id ) ) { - Expr.find.ID = function(match, context, isXML){ - if ( typeof context.getElementById !== "undefined" && !isXML ) { - var m = context.getElementById(match[1]); - return m ? m.id === match[1] || typeof m.getAttributeNode !== "undefined" && m.getAttributeNode("id").nodeValue === match[1] ? [m] : undefined : []; - } - }; - - Expr.filter.ID = function(elem, match){ - var node = typeof elem.getAttributeNode !== "undefined" && elem.getAttributeNode("id"); - return elem.nodeType === 1 && node && node.nodeValue === match; - }; - } - - root.removeChild( form ); - root = form = null; // release memory in IE -})(); - -(function(){ - // Check to see if the browser returns only elements - // when doing getElementsByTagName("*") - - // Create a fake element - var div = document.createElement("div"); - div.appendChild( document.createComment("") ); - - // Make sure no comments are found - if ( div.getElementsByTagName("*").length > 0 ) { - Expr.find.TAG = function(match, context){ - var results = context.getElementsByTagName(match[1]); - - // Filter out possible comments - if ( match[1] === "*" ) { - var tmp = []; - - for ( var i = 0; results[i]; i++ ) { - if ( results[i].nodeType === 1 ) { - tmp.push( results[i] ); - } - } - - results = tmp; - } - - return results; - }; - } - - // Check to see if an attribute returns normalized href attributes - div.innerHTML = ""; - if ( div.firstChild && typeof div.firstChild.getAttribute !== "undefined" && - div.firstChild.getAttribute("href") !== "#" ) { - Expr.attrHandle.href = function(elem){ - return elem.getAttribute("href", 2); - }; - } - - div = null; // release memory in IE -})(); - -if ( document.querySelectorAll ) { - (function(){ - var oldSizzle = Sizzle, div = document.createElement("div"); - div.innerHTML = "

"; - - // Safari can't handle uppercase or unicode characters when - // in quirks mode. - if ( div.querySelectorAll && div.querySelectorAll(".TEST").length === 0 ) { - return; - } - - Sizzle = function(query, context, extra, seed){ - context = context || document; - - // Only use querySelectorAll on non-XML documents - // (ID selectors don't work in non-HTML documents) - if ( !seed && context.nodeType === 9 && !isXML(context) ) { - try { - return makeArray( context.querySelectorAll(query), extra ); - } catch(e){} - } - - return oldSizzle(query, context, extra, seed); - }; - - for ( var prop in oldSizzle ) { - Sizzle[ prop ] = oldSizzle[ prop ]; - } - - div = null; // release memory in IE - })(); -} - -(function(){ - var div = document.createElement("div"); - - div.innerHTML = "
"; - - // Opera can't find a second classname (in 9.6) - // Also, make sure that getElementsByClassName actually exists - if ( !div.getElementsByClassName || div.getElementsByClassName("e").length === 0 ) { - return; - } - - // Safari caches class attributes, doesn't catch changes (in 3.2) - div.lastChild.className = "e"; - - if ( div.getElementsByClassName("e").length === 1 ) { - return; - } - - Expr.order.splice(1, 0, "CLASS"); - Expr.find.CLASS = function(match, context, isXML) { - if ( typeof context.getElementsByClassName !== "undefined" && !isXML ) { - return context.getElementsByClassName(match[1]); - } - }; - - div = null; // release memory in IE -})(); - -function dirNodeCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) { - for ( var i = 0, l = checkSet.length; i < l; i++ ) { - var elem = checkSet[i]; - if ( elem ) { - elem = elem[dir]; - var match = false; - - while ( elem ) { - if ( elem.sizcache === doneName ) { - match = checkSet[elem.sizset]; - break; - } - - if ( elem.nodeType === 1 && !isXML ){ - elem.sizcache = doneName; - elem.sizset = i; - } - - if ( elem.nodeName.toLowerCase() === cur ) { - match = elem; - break; - } - - elem = elem[dir]; - } - - checkSet[i] = match; - } - } -} - -function dirCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) { - for ( var i = 0, l = checkSet.length; i < l; i++ ) { - var elem = checkSet[i]; - if ( elem ) { - elem = elem[dir]; - var match = false; - - while ( elem ) { - if ( elem.sizcache === doneName ) { - match = checkSet[elem.sizset]; - break; - } - - if ( elem.nodeType === 1 ) { - if ( !isXML ) { - elem.sizcache = doneName; - elem.sizset = i; - } - if ( typeof cur !== "string" ) { - if ( elem === cur ) { - match = true; - break; - } - - } else if ( Sizzle.filter( cur, [elem] ).length > 0 ) { - match = elem; - break; - } - } - - elem = elem[dir]; - } - - checkSet[i] = match; - } - } -} - -var contains = document.compareDocumentPosition ? function(a, b){ - return !!(a.compareDocumentPosition(b) & 16); -} : function(a, b){ - return a !== b && (a.contains ? a.contains(b) : true); -}; - -var isXML = function(elem){ - // documentElement is verified for cases where it doesn't yet exist - // (such as loading iframes in IE - #4833) - var documentElement = (elem ? elem.ownerDocument || elem : 0).documentElement; - return documentElement ? documentElement.nodeName !== "HTML" : false; -}; - -var posProcess = function(selector, context){ - var tmpSet = [], later = "", match, - root = context.nodeType ? [context] : context; - - // Position selectors must be done after the filter - // And so must :not(positional) so we move all PSEUDOs to the end - while ( (match = Expr.match.PSEUDO.exec( selector )) ) { - later += match[0]; - selector = selector.replace( Expr.match.PSEUDO, "" ); - } - - selector = Expr.relative[selector] ? selector + "*" : selector; - - for ( var i = 0, l = root.length; i < l; i++ ) { - Sizzle( selector, root[i], tmpSet ); - } - - return Sizzle.filter( later, tmpSet ); -}; - -// EXPOSE -jQuery.find = Sizzle; -jQuery.expr = Sizzle.selectors; -jQuery.expr[":"] = jQuery.expr.filters; -jQuery.unique = Sizzle.uniqueSort; -jQuery.text = getText; -jQuery.isXMLDoc = isXML; -jQuery.contains = contains; - -return; - -window.Sizzle = Sizzle; - -})(); -var runtil = /Until$/, - rparentsprev = /^(?:parents|prevUntil|prevAll)/, - // Note: This RegExp should be improved, or likely pulled from Sizzle - rmultiselector = /,/, - slice = Array.prototype.slice; - -// Implement the identical functionality for filter and not -var winnow = function( elements, qualifier, keep ) { - if ( jQuery.isFunction( qualifier ) ) { - return jQuery.grep(elements, function( elem, i ) { - return !!qualifier.call( elem, i, elem ) === keep; - }); - - } else if ( qualifier.nodeType ) { - return jQuery.grep(elements, function( elem, i ) { - return (elem === qualifier) === keep; - }); - - } else if ( typeof qualifier === "string" ) { - var filtered = jQuery.grep(elements, function( elem ) { - return elem.nodeType === 1; - }); - - if ( isSimple.test( qualifier ) ) { - return jQuery.filter(qualifier, filtered, !keep); - } else { - qualifier = jQuery.filter( qualifier, filtered ); - } - } - - return jQuery.grep(elements, function( elem, i ) { - return (jQuery.inArray( elem, qualifier ) >= 0) === keep; - }); -}; - -jQuery.fn.extend({ - find: function( selector ) { - var ret = this.pushStack( "", "find", selector ), length = 0; - - for ( var i = 0, l = this.length; i < l; i++ ) { - length = ret.length; - jQuery.find( selector, this[i], ret ); - - if ( i > 0 ) { - // Make sure that the results are unique - for ( var n = length; n < ret.length; n++ ) { - for ( var r = 0; r < length; r++ ) { - if ( ret[r] === ret[n] ) { - ret.splice(n--, 1); - break; - } - } - } - } - } - - return ret; - }, - - has: function( target ) { - var targets = jQuery( target ); - return this.filter(function() { - for ( var i = 0, l = targets.length; i < l; i++ ) { - if ( jQuery.contains( this, targets[i] ) ) { - return true; - } - } - }); - }, - - not: function( selector ) { - return this.pushStack( winnow(this, selector, false), "not", selector); - }, - - filter: function( selector ) { - return this.pushStack( winnow(this, selector, true), "filter", selector ); - }, - - is: function( selector ) { - return !!selector && jQuery.filter( selector, this ).length > 0; - }, - - closest: function( selectors, context ) { - if ( jQuery.isArray( selectors ) ) { - var ret = [], cur = this[0], match, matches = {}, selector; - - if ( cur && selectors.length ) { - for ( var i = 0, l = selectors.length; i < l; i++ ) { - selector = selectors[i]; - - if ( !matches[selector] ) { - matches[selector] = jQuery.expr.match.POS.test( selector ) ? - jQuery( selector, context || this.context ) : - selector; - } - } - - while ( cur && cur.ownerDocument && cur !== context ) { - for ( selector in matches ) { - match = matches[selector]; - - if ( match.jquery ? match.index(cur) > -1 : jQuery(cur).is(match) ) { - ret.push({ selector: selector, elem: cur }); - delete matches[selector]; - } - } - cur = cur.parentNode; - } - } - - return ret; - } - - var pos = jQuery.expr.match.POS.test( selectors ) ? - jQuery( selectors, context || this.context ) : null; - - return this.map(function( i, cur ) { - while ( cur && cur.ownerDocument && cur !== context ) { - if ( pos ? pos.index(cur) > -1 : jQuery(cur).is(selectors) ) { - return cur; - } - cur = cur.parentNode; - } - return null; - }); - }, - - // Determine the position of an element within - // the matched set of elements - index: function( elem ) { - if ( !elem || typeof elem === "string" ) { - return jQuery.inArray( this[0], - // If it receives a string, the selector is used - // If it receives nothing, the siblings are used - elem ? jQuery( elem ) : this.parent().children() ); - } - // Locate the position of the desired element - return jQuery.inArray( - // If it receives a jQuery object, the first element is used - elem.jquery ? elem[0] : elem, this ); - }, - - add: function( selector, context ) { - var set = typeof selector === "string" ? - jQuery( selector, context || this.context ) : - jQuery.makeArray( selector ), - all = jQuery.merge( this.get(), set ); - - return this.pushStack( isDisconnected( set[0] ) || isDisconnected( all[0] ) ? - all : - jQuery.unique( all ) ); - }, - - andSelf: function() { - return this.add( this.prevObject ); - } -}); - -// A painfully simple check to see if an element is disconnected -// from a document (should be improved, where feasible). -function isDisconnected( node ) { - return !node || !node.parentNode || node.parentNode.nodeType === 11; -} - -jQuery.each({ - parent: function( elem ) { - var parent = elem.parentNode; - return parent && parent.nodeType !== 11 ? parent : null; - }, - parents: function( elem ) { - return jQuery.dir( elem, "parentNode" ); - }, - parentsUntil: function( elem, i, until ) { - return jQuery.dir( elem, "parentNode", until ); - }, - next: function( elem ) { - return jQuery.nth( elem, 2, "nextSibling" ); - }, - prev: function( elem ) { - return jQuery.nth( elem, 2, "previousSibling" ); - }, - nextAll: function( elem ) { - return jQuery.dir( elem, "nextSibling" ); - }, - prevAll: function( elem ) { - return jQuery.dir( elem, "previousSibling" ); - }, - nextUntil: function( elem, i, until ) { - return jQuery.dir( elem, "nextSibling", until ); - }, - prevUntil: function( elem, i, until ) { - return jQuery.dir( elem, "previousSibling", until ); - }, - siblings: function( elem ) { - return jQuery.sibling( elem.parentNode.firstChild, elem ); - }, - children: function( elem ) { - return jQuery.sibling( elem.firstChild ); - }, - contents: function( elem ) { - return jQuery.nodeName( elem, "iframe" ) ? - elem.contentDocument || elem.contentWindow.document : - jQuery.makeArray( elem.childNodes ); - } -}, function( name, fn ) { - jQuery.fn[ name ] = function( until, selector ) { - var ret = jQuery.map( this, fn, until ); - - if ( !runtil.test( name ) ) { - selector = until; - } - - if ( selector && typeof selector === "string" ) { - ret = jQuery.filter( selector, ret ); - } - - ret = this.length > 1 ? jQuery.unique( ret ) : ret; - - if ( (this.length > 1 || rmultiselector.test( selector )) && rparentsprev.test( name ) ) { - ret = ret.reverse(); - } - - return this.pushStack( ret, name, slice.call(arguments).join(",") ); - }; -}); - -jQuery.extend({ - filter: function( expr, elems, not ) { - if ( not ) { - expr = ":not(" + expr + ")"; - } - - return jQuery.find.matches(expr, elems); - }, - - dir: function( elem, dir, until ) { - var matched = [], cur = elem[dir]; - while ( cur && cur.nodeType !== 9 && (until === undefined || cur.nodeType !== 1 || !jQuery( cur ).is( until )) ) { - if ( cur.nodeType === 1 ) { - matched.push( cur ); - } - cur = cur[dir]; - } - return matched; - }, - - nth: function( cur, result, dir, elem ) { - result = result || 1; - var num = 0; - - for ( ; cur; cur = cur[dir] ) { - if ( cur.nodeType === 1 && ++num === result ) { - break; - } - } - - return cur; - }, - - sibling: function( n, elem ) { - var r = []; - - for ( ; n; n = n.nextSibling ) { - if ( n.nodeType === 1 && n !== elem ) { - r.push( n ); - } - } - - return r; - } -}); -var rinlinejQuery = / jQuery\d+="(?:\d+|null)"/g, - rleadingWhitespace = /^\s+/, - rxhtmlTag = /(<([\w:]+)[^>]*?)\/>/g, - rselfClosing = /^(?:area|br|col|embed|hr|img|input|link|meta|param)$/i, - rtagName = /<([\w:]+)/, - rtbody = /"; - }, - wrapMap = { - option: [ 1, "" ], - legend: [ 1, "
", "
" ], - thead: [ 1, "", "
" ], - tr: [ 2, "", "
" ], - td: [ 3, "", "
" ], - col: [ 2, "", "
" ], - area: [ 1, "", "" ], - _default: [ 0, "", "" ] - }; - -wrapMap.optgroup = wrapMap.option; -wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; -wrapMap.th = wrapMap.td; - -// IE can't serialize and + diff --git a/src/main/resources/templates/include/soneMenu.html b/src/main/resources/templates/include/soneMenu.html index fc51e79..a2b2532 100644 --- a/src/main/resources/templates/include/soneMenu.html +++ b/src/main/resources/templates/include/soneMenu.html @@ -10,9 +10,9 @@
<%sone.niceName|html> - (<%= View.Sone.Stats.Posts|l10n 0=sone.posts.size>, <%= View.Sone.Stats.Replies|l10n 0=sone.replies.size><%if ! sone.allImages.size|match value==0>, <%= View.Sone.Stats.Images|l10n 0=sone.allImages.size><%/if>) + (<%= View.Sone.Stats.Posts|l10n 0=sone.posts.size>, <%= View.Sone.Stats.Replies|l10n 0=sone.replies.size><%if ! sone.allImages.size|match value==0>, <%= View.Sone.Stats.Images|l10n 0=sone.allImages.size><%/if><%if core.debug>, <% sone.client|html><%/if>)
- + <%foreach sone.albums album> <%first> diff --git a/src/main/resources/templates/include/viewPost.html b/src/main/resources/templates/include/viewPost.html index 81683e4..edcabce 100644 --- a/src/main/resources/templates/include/viewPost.html +++ b/src/main/resources/templates/include/viewPost.html @@ -93,30 +93,9 @@ - <%if !post.sone.current> - <%ifnull !post.sone.trust> - · -
- - - - -
-
- - - - -
-
- - - - -
- <%/if> - <%/if> <%/if> + · + <%= View.Post.WebOfTrustLink|l10n|html> <%if post.sone.local> ·
@@ -143,7 +122,7 @@
diff --git a/src/main/resources/templates/include/viewReply.html b/src/main/resources/templates/include/viewReply.html index 3a5e59a..7462b2b 100644 --- a/src/main/resources/templates/include/viewReply.html +++ b/src/main/resources/templates/include/viewReply.html @@ -64,30 +64,9 @@
- <%if !reply.sone.current> - <%ifnull !reply.sone.trust> - · -
- - - - -
-
- - - - -
-
- - - - -
- <%/if> - <%/if> <%/if> + · + <%= View.Post.WebOfTrustLink|l10n|html> <%if reply.sone.local> ·
diff --git a/src/main/resources/templates/knownSones.html b/src/main/resources/templates/knownSones.html index c113748..21c6be2 100644 --- a/src/main/resources/templates/knownSones.html +++ b/src/main/resources/templates/knownSones.html @@ -2,14 +2,14 @@ - @@ -160,23 +151,6 @@ <%= Page.Options.Option.RequireFullAccess.Description|l10n|html>

-

<%= Page.Options.Section.TrustOptions.Title|l10n|html>

- -

<%= Page.Options.Option.PositiveTrust.Description|l10n|html>

- <%if =positive-trust|in collection=fieldErrors> -

<%= Page.Options.Warnings.ValueNotChanged|l10n|html>

- <%/if> -

- -

<%= Page.Options.Option.NegativeTrust.Description|l10n|html>

- <%if =negative-trust|in collection=fieldErrors> -

<%= Page.Options.Warnings.ValueNotChanged|l10n|html>

- <%/if> -

- -

<%= Page.Options.Option.TrustComment.Description|l10n|html>

-

-

<%= Page.Options.Section.FcpOptions.Title|l10n|html>

checked="checked"<%/if> /> <%= Page.Options.Option.FcpInterfaceActive.Description|l10n|html>

diff --git a/src/test/java/net/pterodactylus/sone/core/ConfigurationSoneParserTest.java b/src/test/java/net/pterodactylus/sone/core/ConfigurationSoneParserTest.java deleted file mode 100644 index 4edc1b7..0000000 --- a/src/test/java/net/pterodactylus/sone/core/ConfigurationSoneParserTest.java +++ /dev/null @@ -1,522 +0,0 @@ -package net.pterodactylus.sone.core; - -import static com.google.common.base.Optional.of; -import static net.pterodactylus.sone.test.Matchers.isAlbum; -import static net.pterodactylus.sone.test.Matchers.isImage; -import static net.pterodactylus.sone.test.Matchers.isPost; -import static net.pterodactylus.sone.test.Matchers.isPostReply; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.contains; -import static org.hamcrest.Matchers.containsInAnyOrder; -import static org.hamcrest.Matchers.emptyIterable; -import static org.hamcrest.Matchers.hasSize; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.notNullValue; -import static org.hamcrest.Matchers.nullValue; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import java.util.Collection; -import java.util.List; -import java.util.Map; -import java.util.Set; - -import net.pterodactylus.sone.core.ConfigurationSoneParser.InvalidAlbumFound; -import net.pterodactylus.sone.core.ConfigurationSoneParser.InvalidImageFound; -import net.pterodactylus.sone.core.ConfigurationSoneParser.InvalidParentAlbumFound; -import net.pterodactylus.sone.core.ConfigurationSoneParser.InvalidPostFound; -import net.pterodactylus.sone.core.ConfigurationSoneParser.InvalidPostReplyFound; -import net.pterodactylus.sone.data.Album; -import net.pterodactylus.sone.data.Post; -import net.pterodactylus.sone.data.PostReply; -import net.pterodactylus.sone.data.Profile; -import net.pterodactylus.sone.data.Profile.Field; -import net.pterodactylus.sone.data.Sone; -import net.pterodactylus.sone.database.AlbumBuilder; -import net.pterodactylus.sone.database.AlbumBuilderFactory; -import net.pterodactylus.sone.database.ImageBuilder; -import net.pterodactylus.sone.database.ImageBuilderFactory; -import net.pterodactylus.sone.database.PostBuilder; -import net.pterodactylus.sone.database.PostBuilderFactory; -import net.pterodactylus.sone.database.PostReplyBuilder; -import net.pterodactylus.sone.database.PostReplyBuilderFactory; -import net.pterodactylus.sone.test.TestAlbumBuilder; -import net.pterodactylus.sone.test.TestImageBuilder; -import net.pterodactylus.sone.test.TestPostBuilder; -import net.pterodactylus.sone.test.TestPostReplyBuilder; -import net.pterodactylus.sone.test.TestValue; -import net.pterodactylus.util.config.Configuration; - -import com.google.common.base.Optional; -import org.hamcrest.Matchers; -import org.junit.Test; -import org.mockito.invocation.InvocationOnMock; -import org.mockito.stubbing.Answer; - -/** - * Unit test for {@link ConfigurationSoneParser}. - */ -public class ConfigurationSoneParserTest { - - private final Configuration configuration = mock(Configuration.class); - private final Sone sone = mock(Sone.class); - private final ConfigurationSoneParser configurationSoneParser; - - public ConfigurationSoneParserTest() { - when(sone.getId()).thenReturn("1"); - configurationSoneParser = - new ConfigurationSoneParser(configuration, sone); - } - - @Test - public void emptyProfileIsLoadedCorrectly() { - setupEmptyProfile(); - Profile profile = configurationSoneParser.parseProfile(); - assertThat(profile, notNullValue()); - assertThat(profile.getFirstName(), nullValue()); - assertThat(profile.getMiddleName(), nullValue()); - assertThat(profile.getLastName(), nullValue()); - assertThat(profile.getBirthDay(), nullValue()); - assertThat(profile.getBirthMonth(), nullValue()); - assertThat(profile.getBirthYear(), nullValue()); - assertThat(profile.getFields(), emptyIterable()); - } - - private void setupEmptyProfile() { - when(configuration.getStringValue(anyString())).thenReturn( - TestValue.from(null)); - when(configuration.getIntValue(anyString())).thenReturn( - TestValue.from(null)); - } - - @Test - public void filledProfileWithFieldsIsParsedCorrectly() { - setupFilledProfile(); - Profile profile = configurationSoneParser.parseProfile(); - assertThat(profile, notNullValue()); - assertThat(profile.getFirstName(), is("First")); - assertThat(profile.getMiddleName(), is("M.")); - assertThat(profile.getLastName(), is("Last")); - assertThat(profile.getBirthDay(), is(18)); - assertThat(profile.getBirthMonth(), is(12)); - assertThat(profile.getBirthYear(), is(1976)); - final List fields = profile.getFields(); - assertThat(fields, hasSize(2)); - assertThat(fields.get(0).getName(), is("Field1")); - assertThat(fields.get(0).getValue(), is("Value1")); - assertThat(fields.get(1).getName(), is("Field2")); - assertThat(fields.get(1).getValue(), is("Value2")); - } - - private void setupFilledProfile() { - setupString("Sone/1/Profile/FirstName", "First"); - setupString("Sone/1/Profile/MiddleName", "M."); - setupString("Sone/1/Profile/LastName", "Last"); - setupInteger("Sone/1/Profile/BirthDay", 18); - setupInteger("Sone/1/Profile/BirthMonth", 12); - setupInteger("Sone/1/Profile/BirthYear", 1976); - setupString("Sone/1/Profile/Fields/0/Name", "Field1"); - setupString("Sone/1/Profile/Fields/0/Value", "Value1"); - setupString("Sone/1/Profile/Fields/1/Name", "Field2"); - setupString("Sone/1/Profile/Fields/1/Value", "Value2"); - setupString("Sone/1/Profile/Fields/2/Name", null); - } - - private void setupString(String nodeName, String value) { - when(configuration.getStringValue(eq(nodeName))).thenReturn( - TestValue.from(value)); - } - - private void setupInteger(String nodeName, Integer value) { - when(configuration.getIntValue(eq(nodeName))).thenReturn( - TestValue.from(value)); - } - - @Test - public void postsAreParsedCorrectly() { - setupCompletePosts(); - PostBuilderFactory postBuilderFactory = createPostBuilderFactory(); - Collection posts = - configurationSoneParser.parsePosts(postBuilderFactory); - assertThat(posts, - Matchers.containsInAnyOrder( - isPost("P0", 1000L, "T0", Optional.absent()), - isPost("P1", 1001L, "T1", - of("1234567890123456789012345678901234567890123")))); - } - - private PostBuilderFactory createPostBuilderFactory() { - PostBuilderFactory postBuilderFactory = - mock(PostBuilderFactory.class); - when(postBuilderFactory.newPostBuilder()).thenAnswer( - new Answer() { - @Override - public PostBuilder answer(InvocationOnMock invocation) - throws Throwable { - return new TestPostBuilder(); - } - }); - return postBuilderFactory; - } - - private void setupCompletePosts() { - setupPost("0", "P0", 1000L, "T0", null); - setupPost("1", "P1", 1001L, "T1", - "1234567890123456789012345678901234567890123"); - setupPost("2", null, 0L, null, null); - } - - private void setupPost(String postNumber, String postId, long time, - String text, String recipientId) { - setupString("Sone/1/Posts/" + postNumber + "/ID", postId); - setupLong("Sone/1/Posts/" + postNumber + "/Time", time); - setupString("Sone/1/Posts/" + postNumber + "/Text", text); - setupString("Sone/1/Posts/" + postNumber + "/Recipient", recipientId); - } - - private void setupLong(String nodeName, Long value) { - when(configuration.getLongValue(eq(nodeName))).thenReturn( - TestValue.from(value)); - } - - @Test(expected = InvalidPostFound.class) - public void postWithoutTimeIsRecognized() { - setupPostWithoutTime(); - configurationSoneParser.parsePosts(createPostBuilderFactory()); - } - - private void setupPostWithoutTime() { - setupPost("0", "P0", 0L, "T0", null); - } - - @Test(expected = InvalidPostFound.class) - public void postWithoutTextIsRecognized() { - setupPostWithoutText(); - configurationSoneParser.parsePosts(createPostBuilderFactory()); - } - - private void setupPostWithoutText() { - setupPost("0", "P0", 1000L, null, null); - } - - @Test - public void postWithInvalidRecipientIdIsRecognized() { - setupPostWithInvalidRecipientId(); - Collection posts = configurationSoneParser.parsePosts( - createPostBuilderFactory()); - assertThat(posts, contains( - isPost("P0", 1000L, "T0", Optional.absent()))); - } - - private void setupPostWithInvalidRecipientId() { - setupPost("0", "P0", 1000L, "T0", "123"); - setupPost("1", null, 0L, null, null); - } - - @Test - public void postRepliesAreParsedCorrectly() { - setupPostReplies(); - PostReplyBuilderFactory postReplyBuilderFactory = - new PostReplyBuilderFactory() { - @Override - public PostReplyBuilder newPostReplyBuilder() { - return new TestPostReplyBuilder(); - } - }; - Collection postReplies = - configurationSoneParser.parsePostReplies( - postReplyBuilderFactory); - assertThat(postReplies, hasSize(2)); - assertThat(postReplies, - containsInAnyOrder(isPostReply("R0", "P0", 1000L, "T0"), - isPostReply("R1", "P1", 1001L, "T1"))); - } - - private void setupPostReplies() { - setupPostReply("0", "R0", "P0", 1000L, "T0"); - setupPostReply("1", "R1", "P1", 1001L, "T1"); - setupPostReply("2", null, null, 0L, null); - } - - private void setupPostReply(String postReplyNumber, String postReplyId, - String postId, long time, String text) { - setupString("Sone/1/Replies/" + postReplyNumber + "/ID", postReplyId); - setupString("Sone/1/Replies/" + postReplyNumber + "/Post/ID", postId); - setupLong("Sone/1/Replies/" + postReplyNumber + "/Time", time); - setupString("Sone/1/Replies/" + postReplyNumber + "/Text", text); - } - - @Test(expected = InvalidPostReplyFound.class) - public void missingPostIdIsRecognized() { - setupPostReplyWithMissingPostId(); - configurationSoneParser.parsePostReplies(null); - } - - private void setupPostReplyWithMissingPostId() { - setupPostReply("0", "R0", null, 1000L, "T0"); - } - - @Test(expected = InvalidPostReplyFound.class) - public void missingPostReplyTimeIsRecognized() { - setupPostReplyWithMissingPostReplyTime(); - configurationSoneParser.parsePostReplies(null); - } - - private void setupPostReplyWithMissingPostReplyTime() { - setupPostReply("0", "R0", "P0", 0L, "T0"); - } - - @Test(expected = InvalidPostReplyFound.class) - public void missingPostReplyTextIsRecognized() { - setupPostReplyWithMissingPostReplyText(); - configurationSoneParser.parsePostReplies(null); - } - - private void setupPostReplyWithMissingPostReplyText() { - setupPostReply("0", "R0", "P0", 1000L, null); - } - - @Test - public void likedPostIdsParsedCorrectly() { - setupLikedPostIds(); - Set likedPostIds = - configurationSoneParser.parseLikedPostIds(); - assertThat(likedPostIds, containsInAnyOrder("P1", "P2", "P3")); - } - - private void setupLikedPostIds() { - setupString("Sone/1/Likes/Post/0/ID", "P1"); - setupString("Sone/1/Likes/Post/1/ID", "P2"); - setupString("Sone/1/Likes/Post/2/ID", "P3"); - setupString("Sone/1/Likes/Post/3/ID", null); - } - - @Test - public void likedPostReplyIdsAreParsedCorrectly() { - setupLikedPostReplyIds(); - Set likedPostReplyIds = - configurationSoneParser.parseLikedPostReplyIds(); - assertThat(likedPostReplyIds, containsInAnyOrder("R1", "R2", "R3")); - } - - private void setupLikedPostReplyIds() { - setupString("Sone/1/Likes/Reply/0/ID", "R1"); - setupString("Sone/1/Likes/Reply/1/ID", "R2"); - setupString("Sone/1/Likes/Reply/2/ID", "R3"); - setupString("Sone/1/Likes/Reply/3/ID", null); - } - - @Test - public void friendsAreParsedCorrectly() { - setupFriends(); - Set friends = configurationSoneParser.parseFriends(); - assertThat(friends, containsInAnyOrder("F1", "F2", "F3")); - } - - private void setupFriends() { - setupString("Sone/1/Friends/0/ID", "F1"); - setupString("Sone/1/Friends/1/ID", "F2"); - setupString("Sone/1/Friends/2/ID", "F3"); - setupString("Sone/1/Friends/3/ID", null); - } - - @Test - public void topLevelAlbumsAreParsedCorrectly() { - setupTopLevelAlbums(); - AlbumBuilderFactory albumBuilderFactory = createAlbumBuilderFactory(); - List topLevelAlbums = - configurationSoneParser.parseTopLevelAlbums( - albumBuilderFactory); - assertThat(topLevelAlbums, hasSize(2)); - Album firstAlbum = topLevelAlbums.get(0); - assertThat(firstAlbum, isAlbum("A1", null, "T1", "D1")); - assertThat(firstAlbum.getAlbums(), emptyIterable()); - assertThat(firstAlbum.getImages(), emptyIterable()); - Album secondAlbum = topLevelAlbums.get(1); - assertThat(secondAlbum, isAlbum("A2", null, "T2", "D2")); - assertThat(secondAlbum.getAlbums(), hasSize(1)); - assertThat(secondAlbum.getImages(), emptyIterable()); - Album thirdAlbum = secondAlbum.getAlbums().get(0); - assertThat(thirdAlbum, isAlbum("A3", "A2", "T3", "D3")); - assertThat(thirdAlbum.getAlbums(), emptyIterable()); - assertThat(thirdAlbum.getImages(), emptyIterable()); - } - - private void setupTopLevelAlbums() { - setupAlbum(0, "A1", null, "T1", "D1", "I1"); - setupAlbum(1, "A2", null, "T2", "D2", null); - setupAlbum(2, "A3", "A2", "T3", "D3", "I3"); - setupAlbum(3, null, null, null, null, null); - } - - private void setupAlbum(int albumNumber, String albumId, - String parentAlbumId, - String title, String description, String imageId) { - final String albumPrefix = "Sone/1/Albums/" + albumNumber; - setupString(albumPrefix + "/ID", albumId); - setupString(albumPrefix + "/Title", title); - setupString(albumPrefix + "/Description", description); - setupString(albumPrefix + "/Parent", parentAlbumId); - setupString(albumPrefix + "/AlbumImage", imageId); - } - - private AlbumBuilderFactory createAlbumBuilderFactory() { - AlbumBuilderFactory albumBuilderFactory = - mock(AlbumBuilderFactory.class); - when(albumBuilderFactory.newAlbumBuilder()).thenAnswer( - new Answer() { - @Override - public AlbumBuilder answer(InvocationOnMock invocation) { - return new TestAlbumBuilder(); - } - }); - return albumBuilderFactory; - } - - @Test(expected = InvalidAlbumFound.class) - public void albumWithInvalidTitleIsRecognized() { - setupAlbum(0, "A1", null, null, "D1", "I1"); - configurationSoneParser.parseTopLevelAlbums( - createAlbumBuilderFactory()); - } - - @Test(expected = InvalidAlbumFound.class) - public void albumWithInvalidDescriptionIsRecognized() { - setupAlbum(0, "A1", null, "T1", null, "I1"); - configurationSoneParser.parseTopLevelAlbums( - createAlbumBuilderFactory()); - } - - @Test(expected = InvalidParentAlbumFound.class) - public void albumWithInvalidParentIsRecognized() { - setupAlbum(0, "A1", "A0", "T1", "D1", "I1"); - configurationSoneParser.parseTopLevelAlbums( - createAlbumBuilderFactory()); - } - - @Test - public void imagesAreParsedCorrectly() { - setupTopLevelAlbums(); - configurationSoneParser.parseTopLevelAlbums( - createAlbumBuilderFactory()); - setupImages(); - configurationSoneParser.parseImages(createImageBuilderFactory()); - Map albums = configurationSoneParser.getAlbums(); - assertThat(albums.get("A1").getImages(), - contains(isImage("I1", 1000L, "K1", "T1", "D1", 16, 9))); - assertThat(albums.get("A2").getImages(), contains( - isImage("I2", 2000L, "K2", "T2", "D2", 16 * 2, 9 * 2))); - assertThat(albums.get("A3").getImages(), contains( - isImage("I3", 3000L, "K3", "T3", "D3", 16 * 3, 9 * 3))); - } - - private void setupImages() { - setupImage(0, "I1", "A1", 1000L, "K1", "T1", "D1", 16, 9); - setupImage(1, "I2", "A2", 2000L, "K2", "T2", "D2", 16 * 2, 9 * 2); - setupImage(2, "I3", "A3", 3000L, "K3", "T3", "D3", 16 * 3, 9 * 3); - setupImage(3, null, null, 0L, null, null, null, 0, 0); - } - - private void setupImage(int imageNumber, String id, - String parentAlbumId, Long creationTime, String key, String title, - String description, Integer width, Integer height) { - final String imagePrefix = "Sone/1/Images/" + imageNumber; - setupString(imagePrefix + "/ID", id); - setupString(imagePrefix + "/Album", parentAlbumId); - setupLong(imagePrefix + "/CreationTime", creationTime); - setupString(imagePrefix + "/Key", key); - setupString(imagePrefix + "/Title", title); - setupString(imagePrefix + "/Description", description); - setupInteger(imagePrefix + "/Width", width); - setupInteger(imagePrefix + "/Height", height); - } - - private ImageBuilderFactory createImageBuilderFactory() { - ImageBuilderFactory imageBuilderFactory = - mock(ImageBuilderFactory.class); - when(imageBuilderFactory.newImageBuilder()).thenAnswer( - new Answer() { - @Override - public ImageBuilder answer(InvocationOnMock invocation) - throws Throwable { - return new TestImageBuilder(); - } - }); - return imageBuilderFactory; - } - - @Test(expected = InvalidImageFound.class) - public void missingAlbumIdIsRecognized() { - setupTopLevelAlbums(); - configurationSoneParser.parseTopLevelAlbums( - createAlbumBuilderFactory()); - setupImage(0, "I1", null, 1000L, "K1", "T1", "D1", 16, 9); - configurationSoneParser.parseImages(createImageBuilderFactory()); - } - - @Test(expected = InvalidParentAlbumFound.class) - public void invalidAlbumIdIsRecognized() { - setupTopLevelAlbums(); - configurationSoneParser.parseTopLevelAlbums( - createAlbumBuilderFactory()); - setupImage(0, "I1", "A4", 1000L, "K1", "T1", "D1", 16, 9); - configurationSoneParser.parseImages(createImageBuilderFactory()); - } - - @Test(expected = InvalidImageFound.class) - public void missingCreationTimeIsRecognized() { - setupTopLevelAlbums(); - configurationSoneParser.parseTopLevelAlbums( - createAlbumBuilderFactory()); - setupImage(0, "I1", "A1", null, "K1", "T1", "D1", 16, 9); - configurationSoneParser.parseImages(createImageBuilderFactory()); - } - - @Test(expected = InvalidImageFound.class) - public void missingKeyIsRecognized() { - setupTopLevelAlbums(); - configurationSoneParser.parseTopLevelAlbums( - createAlbumBuilderFactory()); - setupImage(0, "I1", "A1", 1000L, null, "T1", "D1", 16, 9); - configurationSoneParser.parseImages(createImageBuilderFactory()); - } - - @Test(expected = InvalidImageFound.class) - public void missingTitleIsRecognized() { - setupTopLevelAlbums(); - configurationSoneParser.parseTopLevelAlbums( - createAlbumBuilderFactory()); - setupImage(0, "I1", "A1", 1000L, "K1", null, "D1", 16, 9); - configurationSoneParser.parseImages(createImageBuilderFactory()); - } - - @Test(expected = InvalidImageFound.class) - public void missingDescriptionIsRecognized() { - setupTopLevelAlbums(); - configurationSoneParser.parseTopLevelAlbums( - createAlbumBuilderFactory()); - setupImage(0, "I1", "A1", 1000L, "K1", "T1", null, 16, 9); - configurationSoneParser.parseImages(createImageBuilderFactory()); - } - - @Test(expected = InvalidImageFound.class) - public void missingWidthIsRecognized() { - setupTopLevelAlbums(); - configurationSoneParser.parseTopLevelAlbums( - createAlbumBuilderFactory()); - setupImage(0, "I1", "A1", 1000L, "K1", "T1", "D1", null, 9); - configurationSoneParser.parseImages(createImageBuilderFactory()); - } - - @Test(expected = InvalidImageFound.class) - public void missingHeightIsRecognized() { - setupTopLevelAlbums(); - configurationSoneParser.parseTopLevelAlbums( - createAlbumBuilderFactory()); - setupImage(0, "I1", "A1", 1000L, "K1", "T1", "D1", 16, null); - configurationSoneParser.parseImages(createImageBuilderFactory()); - } - -} diff --git a/src/test/java/net/pterodactylus/sone/core/CoreTest.java b/src/test/java/net/pterodactylus/sone/core/CoreTest.java deleted file mode 100644 index 66ef743..0000000 --- a/src/test/java/net/pterodactylus/sone/core/CoreTest.java +++ /dev/null @@ -1,161 +0,0 @@ -package net.pterodactylus.sone.core; - -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.inOrder; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; -import static org.mockito.hamcrest.MockitoHamcrest.argThat; - -import net.pterodactylus.sone.core.Core.MarkPostKnown; -import net.pterodactylus.sone.core.Core.MarkReplyKnown; -import net.pterodactylus.sone.core.event.PostRemovedEvent; -import net.pterodactylus.sone.core.event.PostReplyRemovedEvent; -import net.pterodactylus.sone.core.event.SoneRemovedEvent; -import net.pterodactylus.sone.data.Post; -import net.pterodactylus.sone.data.PostReply; -import net.pterodactylus.sone.data.Sone; -import net.pterodactylus.sone.database.Database; -import net.pterodactylus.sone.freenet.wot.Identity; -import net.pterodactylus.sone.freenet.wot.IdentityManager; -import net.pterodactylus.sone.freenet.wot.OwnIdentity; -import net.pterodactylus.sone.freenet.wot.event.IdentityRemovedEvent; -import net.pterodactylus.util.config.Configuration; - -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableSet; -import com.google.common.eventbus.EventBus; -import org.hamcrest.Description; -import org.hamcrest.Matcher; -import org.hamcrest.TypeSafeDiagnosingMatcher; -import org.junit.Test; -import org.mockito.InOrder; - -/** - * Unit test for {@link Core} and its subclasses. - */ -public class CoreTest { - - @Test - public void markPostKnownMarksPostAsKnown() { - Core core = mock(Core.class); - Post post = mock(Post.class); - MarkPostKnown markPostKnown = core.new MarkPostKnown(post); - markPostKnown.run(); - verify(core).markPostKnown(eq(post)); - } - - @Test - public void markReplyKnownMarksReplyAsKnown() { - Core core = mock(Core.class); - PostReply postReply = mock(PostReply.class); - MarkReplyKnown markReplyKnown = core.new MarkReplyKnown(postReply); - markReplyKnown.run(); - verify(core).markReplyKnown(eq(postReply)); - } - - @Test - public void removingAnIdentitySendsRemovalEventsForAllSoneElements() { - // given - Configuration configuration = mock(Configuration.class); - FreenetInterface freenetInterface = mock(FreenetInterface.class); - IdentityManager identityManager = mock(IdentityManager.class); - SoneDownloader soneDownloader = mock(SoneDownloader.class); - ImageInserter imageInserter = mock(ImageInserter.class); - UpdateChecker updateChecker = mock(UpdateChecker.class); - WebOfTrustUpdater webOfTrustUpdater = mock(WebOfTrustUpdater.class); - EventBus eventBus = mock(EventBus.class); - Database database = mock(Database.class); - Core core = new Core(configuration, freenetInterface, identityManager, soneDownloader, imageInserter, updateChecker, webOfTrustUpdater, eventBus, database); - OwnIdentity ownIdentity = mock(OwnIdentity.class); - Identity identity = mock(Identity.class); - when(identity.getId()).thenReturn("sone-id"); - Sone sone = mock(Sone.class); - when(database.getSone("sone-id")).thenReturn(sone); - PostReply postReply1 = mock(PostReply.class); - PostReply postReply2 = mock(PostReply.class); - when(sone.getReplies()).thenReturn(ImmutableSet.of(postReply1, postReply2)); - Post post1 = mock(Post.class); - Post post2 = mock(Post.class); - when(sone.getPosts()).thenReturn(ImmutableList.of(post1, post2)); - - // when - core.identityRemoved(new IdentityRemovedEvent(ownIdentity, identity)); - - // then - InOrder inOrder = inOrder(eventBus, database); - inOrder.verify(eventBus).post(argThat(isPostReplyRemoved(postReply1))); - inOrder.verify(eventBus).post(argThat(isPostReplyRemoved(postReply2))); - inOrder.verify(eventBus).post(argThat(isPostRemoved(post1))); - inOrder.verify(eventBus).post(argThat(isPostRemoved(post2))); - inOrder.verify(eventBus).post(argThat(isSoneRemoved(sone))); - inOrder.verify(database).removeSone(sone); - } - - private Matcher isPostRemoved(final Post post) { - return new TypeSafeDiagnosingMatcher() { - @Override - protected boolean matchesSafely(Object item, Description mismatchDescription) { - if (!(item instanceof PostRemovedEvent)) { - mismatchDescription.appendText("is not PostRemovedEvent"); - return false; - } - if (((PostRemovedEvent) item).post() != post) { - mismatchDescription.appendText("post is ").appendValue(((PostRemovedEvent) item).post()); - return false; - } - return true; - } - - @Override - public void describeTo(Description description) { - description.appendText("is PostRemovedEvent and post is ").appendValue(post); - } - }; - } - - private Matcher isPostReplyRemoved(final PostReply postReply) { - return new TypeSafeDiagnosingMatcher() { - @Override - protected boolean matchesSafely(Object item, Description mismatchDescription) { - if (!(item instanceof PostReplyRemovedEvent)) { - mismatchDescription.appendText("is not PostReplyRemovedEvent"); - return false; - } - if (((PostReplyRemovedEvent) item).postReply() != postReply) { - mismatchDescription.appendText("post reply is ").appendValue(((PostReplyRemovedEvent) item).postReply()); - return false; - } - return true; - } - - @Override - public void describeTo(Description description) { - description.appendText("is PostReplyRemovedEvent and post is ").appendValue(postReply); - } - }; - } - - private Matcher isSoneRemoved(final Sone sone) { - return new TypeSafeDiagnosingMatcher() { - @Override - protected boolean matchesSafely(Object item, Description mismatchDescription) { - if (!(item instanceof SoneRemovedEvent)) { - mismatchDescription.appendText("is not SoneRemovedEvent"); - return false; - } - if (((SoneRemovedEvent) item).sone() != sone) { - mismatchDescription.appendText("sone is ").appendValue(((SoneRemovedEvent) item).sone()); - return false; - } - return true; - } - - @Override - public void describeTo(Description description) { - description.appendText("is SoneRemovedEvent and sone is ").appendValue(sone); - } - }; - } - -} diff --git a/src/test/java/net/pterodactylus/sone/core/FreenetInterfaceTest.java b/src/test/java/net/pterodactylus/sone/core/FreenetInterfaceTest.java deleted file mode 100644 index c91de68..0000000 --- a/src/test/java/net/pterodactylus/sone/core/FreenetInterfaceTest.java +++ /dev/null @@ -1,507 +0,0 @@ -package net.pterodactylus.sone.core; - -import static freenet.client.FetchException.FetchExceptionMode.ALL_DATA_NOT_FOUND; -import static freenet.keys.InsertableClientSSK.createRandom; -import static freenet.node.RequestStarter.INTERACTIVE_PRIORITY_CLASS; -import static freenet.node.RequestStarter.PREFETCH_PRIORITY_CLASS; -import static net.pterodactylus.sone.test.Matchers.delivers; -import static net.pterodactylus.sone.test.TestUtil.setFinalField; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.notNullValue; -import static org.hamcrest.Matchers.nullValue; -import static org.mockito.ArgumentCaptor.forClass; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.ArgumentMatchers.anyShort; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.doNothing; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoMoreInteractions; -import static org.mockito.Mockito.when; -import static org.mockito.Mockito.withSettings; - -import java.io.IOException; -import java.net.MalformedURLException; -import java.util.HashMap; - -import net.pterodactylus.sone.core.FreenetInterface.BackgroundFetchCallback; -import net.pterodactylus.sone.core.FreenetInterface.Callback; -import net.pterodactylus.sone.core.FreenetInterface.InsertToken; -import net.pterodactylus.sone.core.FreenetInterface.InsertTokenSupplier; -import net.pterodactylus.sone.core.event.ImageInsertAbortedEvent; -import net.pterodactylus.sone.core.event.ImageInsertFailedEvent; -import net.pterodactylus.sone.core.event.ImageInsertFinishedEvent; -import net.pterodactylus.sone.core.event.ImageInsertStartedEvent; -import net.pterodactylus.sone.data.Image; -import net.pterodactylus.sone.data.Sone; -import net.pterodactylus.sone.data.TemporaryImage; -import net.pterodactylus.sone.data.impl.ImageImpl; -import net.pterodactylus.sone.test.TestUtil; - -import freenet.client.ClientMetadata; -import freenet.client.FetchContext; -import freenet.client.FetchException; -import freenet.client.FetchException.FetchExceptionMode; -import freenet.client.FetchResult; -import freenet.client.HighLevelSimpleClient; -import freenet.client.InsertBlock; -import freenet.client.InsertContext; -import freenet.client.InsertException; -import freenet.client.InsertException.InsertExceptionMode; -import freenet.client.Metadata; -import freenet.client.async.ClientContext; -import freenet.client.async.ClientGetCallback; -import freenet.client.async.ClientGetter; -import freenet.client.async.ClientPutter; -import freenet.client.async.SnoopMetadata; -import freenet.client.async.USKCallback; -import freenet.client.async.USKManager; -import freenet.crypt.DummyRandomSource; -import freenet.crypt.RandomSource; -import freenet.keys.FreenetURI; -import freenet.keys.InsertableClientSSK; -import freenet.keys.USK; -import freenet.node.Node; -import freenet.node.NodeClientCore; -import freenet.node.RequestClient; -import freenet.support.Base64; -import freenet.support.api.Bucket; -import freenet.support.io.ArrayBucket; -import freenet.support.io.ResumeFailedException; - -import com.google.common.eventbus.EventBus; -import org.junit.Before; -import org.junit.Test; -import org.mockito.ArgumentCaptor; -import org.mockito.ArgumentMatchers; - -/** - * Unit test for {@link FreenetInterface}. - */ -public class FreenetInterfaceTest { - - private final EventBus eventBus = mock(EventBus.class); - private final Node node = mock(Node.class); - private final NodeClientCore nodeClientCore = mock(NodeClientCore.class); - private final HighLevelSimpleClient highLevelSimpleClient = mock(HighLevelSimpleClient.class, withSettings().extraInterfaces(RequestClient.class)); - private final RandomSource randomSource = new DummyRandomSource(); - private final USKManager uskManager = mock(USKManager.class); - private FreenetInterface freenetInterface; - private final Sone sone = mock(Sone.class); - private final ArgumentCaptor callbackCaptor = forClass(USKCallback.class); - private final Image image = mock(Image.class); - private InsertToken insertToken; - private final Bucket bucket = mock(Bucket.class); - private final ArgumentCaptor clientGetCallback = forClass(ClientGetCallback.class); - private final FreenetURI uri = new FreenetURI("KSK@pgl.png"); - private final FetchResult fetchResult = mock(FetchResult.class); - private final BackgroundFetchCallback backgroundFetchCallback = mock(BackgroundFetchCallback.class); - private final ClientGetter clientGetter = mock(ClientGetter.class); - - public FreenetInterfaceTest() throws MalformedURLException { - } - - @Before - public void setupHighLevelSimpleClient() throws Exception { - when(highLevelSimpleClient.getFetchContext()).thenReturn(mock(FetchContext.class)); - when(highLevelSimpleClient.fetch(eq(uri), anyLong(), any(ClientGetCallback.class), any(FetchContext.class), anyShort())).thenReturn( clientGetter); - } - - @Before - public void setupFreenetInterface() { - when(nodeClientCore.makeClient(anyShort(), anyBoolean(), anyBoolean())).thenReturn(highLevelSimpleClient); - setFinalField(node, "clientCore", nodeClientCore); - setFinalField(node, "random", randomSource); - setFinalField(nodeClientCore, "uskManager", uskManager); - setFinalField(nodeClientCore, "clientContext", mock(ClientContext.class)); - freenetInterface = new FreenetInterface(eventBus, node); - insertToken = freenetInterface.new InsertToken(image); - insertToken.setBucket(bucket); - } - - @Before - public void setupSone() { - InsertableClientSSK insertSsk = createRandom(randomSource, "test-0"); - when(sone.getId()).thenReturn(Base64.encode(insertSsk.getURI().getRoutingKey())); - when(sone.getRequestUri()).thenReturn(insertSsk.getURI().uskForSSK()); - } - - @Before - public void setupCallbackCaptorAndUskManager() { - doNothing().when(uskManager).subscribe(any(USK.class), callbackCaptor.capture(), anyBoolean(), any(RequestClient.class)); - } - - @Test - public void canFetchUri() throws MalformedURLException, FetchException { - FreenetURI freenetUri = new FreenetURI("KSK@GPLv3.txt"); - FetchResult fetchResult = createFetchResult(); - when(highLevelSimpleClient.fetch(freenetUri)).thenReturn(fetchResult); - Fetched fetched = freenetInterface.fetchUri(freenetUri); - assertThat(fetched, notNullValue()); - assertThat(fetched.getFetchResult(), is(fetchResult)); - assertThat(fetched.getFreenetUri(), is(freenetUri)); - } - - @Test - public void fetchFollowsRedirect() throws MalformedURLException, FetchException { - FreenetURI freenetUri = new FreenetURI("KSK@GPLv2.txt"); - FreenetURI newFreenetUri = new FreenetURI("KSK@GPLv3.txt"); - FetchResult fetchResult = createFetchResult(); - FetchException fetchException = new FetchException(FetchExceptionMode.PERMANENT_REDIRECT, newFreenetUri); - when(highLevelSimpleClient.fetch(freenetUri)).thenThrow(fetchException); - when(highLevelSimpleClient.fetch(newFreenetUri)).thenReturn(fetchResult); - Fetched fetched = freenetInterface.fetchUri(freenetUri); - assertThat(fetched.getFetchResult(), is(fetchResult)); - assertThat(fetched.getFreenetUri(), is(newFreenetUri)); - } - - @Test - public void fetchReturnsNullOnFetchExceptions() throws MalformedURLException, FetchException { - FreenetURI freenetUri = new FreenetURI("KSK@GPLv2.txt"); - FetchException fetchException = new FetchException(ALL_DATA_NOT_FOUND); - when(highLevelSimpleClient.fetch(freenetUri)).thenThrow(fetchException); - Fetched fetched = freenetInterface.fetchUri(freenetUri); - assertThat(fetched, nullValue()); - } - - private FetchResult createFetchResult() { - ClientMetadata clientMetadata = new ClientMetadata("text/plain"); - Bucket bucket = new ArrayBucket("Some Data.".getBytes()); - return new FetchResult(clientMetadata, bucket); - } - - @Test - public void insertingAnImage() throws SoneException, InsertException, IOException { - TemporaryImage temporaryImage = new TemporaryImage("image-id"); - temporaryImage.setMimeType("image/png"); - byte[] imageData = new byte[] { 1, 2, 3, 4 }; - temporaryImage.setImageData(imageData); - Image image = new ImageImpl("image-id"); - InsertToken insertToken = freenetInterface.new InsertToken(image); - InsertContext insertContext = mock(InsertContext.class); - when(highLevelSimpleClient.getInsertContext(anyBoolean())).thenReturn(insertContext); - ClientPutter clientPutter = mock(ClientPutter.class); - ArgumentCaptor insertBlockCaptor = forClass(InsertBlock.class); - when(highLevelSimpleClient.insert(insertBlockCaptor.capture(), eq((String) null), eq(false), eq(insertContext), eq(insertToken), anyShort())).thenReturn(clientPutter); - freenetInterface.insertImage(temporaryImage, image, insertToken); - assertThat(insertBlockCaptor.getValue().getData().getInputStream(), delivers(new byte[] { 1, 2, 3, 4 })); - assertThat(TestUtil.getPrivateField(insertToken, "clientPutter"), is(clientPutter)); - verify(eventBus).post(any(ImageInsertStartedEvent.class)); - } - - @Test(expected = SoneInsertException.class) - public void insertExceptionCausesASoneException() throws InsertException, SoneException, IOException { - TemporaryImage temporaryImage = new TemporaryImage("image-id"); - temporaryImage.setMimeType("image/png"); - byte[] imageData = new byte[] { 1, 2, 3, 4 }; - temporaryImage.setImageData(imageData); - Image image = new ImageImpl("image-id"); - InsertToken insertToken = freenetInterface.new InsertToken(image); - InsertContext insertContext = mock(InsertContext.class); - when(highLevelSimpleClient.getInsertContext(anyBoolean())).thenReturn(insertContext); - ArgumentCaptor insertBlockCaptor = forClass(InsertBlock.class); - when(highLevelSimpleClient.insert(insertBlockCaptor.capture(), eq((String) null), eq(false), eq(insertContext), eq(insertToken), anyShort())).thenThrow(InsertException.class); - freenetInterface.insertImage(temporaryImage, image, insertToken); - } - - @Test - public void insertingADirectory() throws InsertException, SoneException { - FreenetURI freenetUri = mock(FreenetURI.class); - HashMap manifestEntries = new HashMap<>(); - String defaultFile = "index.html"; - FreenetURI resultingUri = mock(FreenetURI.class); - when(highLevelSimpleClient.insertManifest(eq(freenetUri), eq(manifestEntries), eq(defaultFile))).thenReturn(resultingUri); - assertThat(freenetInterface.insertDirectory(freenetUri, manifestEntries, defaultFile), is(resultingUri)); - } - - @Test(expected = SoneException.class) - public void insertExceptionIsForwardedAsSoneException() throws InsertException, SoneException { - when(highLevelSimpleClient.insertManifest(ArgumentMatchers.any(), ArgumentMatchers.>any(), ArgumentMatchers.any())).thenThrow(InsertException.class); - freenetInterface.insertDirectory(null, null, null); - } - - @Test - public void soneWithWrongRequestUriWillNotBeSubscribed() throws MalformedURLException { - when(sone.getRequestUri()).thenReturn(new FreenetURI("KSK@GPLv3.txt")); - freenetInterface.registerUsk(new FreenetURI("KSK@GPLv3.txt"), null); - verify(uskManager, never()).subscribe(any(USK.class), any(USKCallback.class), anyBoolean(), any(RequestClient.class)); - } - - @Test - public void registeringAUsk() { - FreenetURI freenetUri = createRandom(randomSource, "test-0").getURI().uskForSSK(); - Callback callback = mock(Callback.class); - freenetInterface.registerUsk(freenetUri, callback); - verify(uskManager).subscribe(any(USK.class), any(USKCallback.class), anyBoolean(), any(RequestClient.class)); - } - - @Test - public void registeringANonUskKeyWillNotBeSubscribed() throws MalformedURLException { - FreenetURI freenetUri = new FreenetURI("KSK@GPLv3.txt"); - Callback callback = mock(Callback.class); - freenetInterface.registerUsk(freenetUri, callback); - verify(uskManager, never()).subscribe(any(USK.class), any(USKCallback.class), anyBoolean(), any(RequestClient.class)); - } - - @Test - public void registeringAnActiveUskWillSubscribeToItCorrectly() { - FreenetURI freenetUri = createRandom(randomSource, "test-0").getURI().uskForSSK(); - final USKCallback uskCallback = mock(USKCallback.class); - freenetInterface.registerActiveUsk(freenetUri, uskCallback); - verify(uskManager).subscribe(any(USK.class), eq(uskCallback), eq(true), any(RequestClient.class)); - } - - @Test - public void registeringAnInactiveUskWillSubscribeToItCorrectly() { - FreenetURI freenetUri = createRandom(randomSource, "test-0").getURI().uskForSSK(); - final USKCallback uskCallback = mock(USKCallback.class); - freenetInterface.registerPassiveUsk(freenetUri, uskCallback); - verify(uskManager).subscribe(any(USK.class), eq(uskCallback), eq(false), any(RequestClient.class)); - } - - @Test - public void registeringAnActiveNonUskWillNotSubscribeToAUsk() - throws MalformedURLException { - FreenetURI freenetUri = createRandom(randomSource, "test-0").getURI(); - freenetInterface.registerActiveUsk(freenetUri, null); - verify(uskManager, never()).subscribe(any(USK.class), any(USKCallback.class), anyBoolean(), any(RequestClient.class)); - } - - @Test - public void registeringAnInactiveNonUskWillNotSubscribeToAUsk() - throws MalformedURLException { - FreenetURI freenetUri = createRandom(randomSource, "test-0").getURI(); - freenetInterface.registerPassiveUsk(freenetUri, null); - verify(uskManager, never()).subscribe(any(USK.class), any(USKCallback.class), anyBoolean(), any(RequestClient.class)); - } - - @Test - public void unregisteringANotRegisteredUskDoesNothing() { - FreenetURI freenetURI = createRandom(randomSource, "test-0").getURI().uskForSSK(); - freenetInterface.unregisterUsk(freenetURI); - verify(uskManager, never()).unsubscribe(any(USK.class), any(USKCallback.class)); - } - - @Test - public void unregisteringARegisteredUsk() { - FreenetURI freenetURI = createRandom(randomSource, "test-0").getURI().uskForSSK(); - Callback callback = mock(Callback.class); - freenetInterface.registerUsk(freenetURI, callback); - freenetInterface.unregisterUsk(freenetURI); - verify(uskManager).unsubscribe(any(USK.class), any(USKCallback.class)); - } - - @Test - public void unregisteringANotRegisteredSoneDoesNothing() { - freenetInterface.unregisterUsk(sone); - verify(uskManager, never()).unsubscribe(any(USK.class), any(USKCallback.class)); - } - - @Test - public void unregisteringARegisteredSoneUnregistersTheSone() - throws MalformedURLException { - freenetInterface.registerActiveUsk(sone.getRequestUri(), mock(USKCallback.class)); - freenetInterface.unregisterUsk(sone); - verify(uskManager).unsubscribe(any(USK.class), any(USKCallback.class)); - } - - @Test - public void unregisteringASoneWithAWrongRequestKeyWillNotUnsubscribe() throws MalformedURLException { - when(sone.getRequestUri()).thenReturn(new FreenetURI("KSK@GPLv3.txt")); - freenetInterface.registerUsk(sone.getRequestUri(), null); - freenetInterface.unregisterUsk(sone); - verify(uskManager, never()).unsubscribe(any(USK.class), any(USKCallback.class)); - } - - @Test - public void callbackForNormalUskUsesDifferentPriorities() { - Callback callback = mock(Callback.class); - FreenetURI soneUri = createRandom(randomSource, "test-0").getURI().uskForSSK(); - freenetInterface.registerUsk(soneUri, callback); - assertThat(callbackCaptor.getValue().getPollingPriorityNormal(), is(PREFETCH_PRIORITY_CLASS)); - assertThat(callbackCaptor.getValue().getPollingPriorityProgress(), is(INTERACTIVE_PRIORITY_CLASS)); - } - - @Test - public void callbackForNormalUskForwardsImportantParameters() throws MalformedURLException { - Callback callback = mock(Callback.class); - FreenetURI uri = createRandom(randomSource, "test-0").getURI().uskForSSK(); - freenetInterface.registerUsk(uri, callback); - USK key = mock(USK.class); - when(key.getURI()).thenReturn(uri); - callbackCaptor.getValue().onFoundEdition(3, key, null, false, (short) 0, null, true, true); - verify(callback).editionFound(eq(uri), eq(3L), eq(true), eq(true)); - } - - @Test - public void fetchedRetainsUriAndFetchResult() { - FreenetURI freenetUri = mock(FreenetURI.class); - FetchResult fetchResult = mock(FetchResult.class); - Fetched fetched = new Fetched(freenetUri, fetchResult); - assertThat(fetched.getFreenetUri(), is(freenetUri)); - assertThat(fetched.getFetchResult(), is(fetchResult)); - } - - @Test - public void cancellingAnInsertWillFireImageInsertAbortedEvent() { - ClientPutter clientPutter = mock(ClientPutter.class); - insertToken.setClientPutter(clientPutter); - ArgumentCaptor imageInsertStartedEvent = forClass(ImageInsertStartedEvent.class); - verify(eventBus).post(imageInsertStartedEvent.capture()); - assertThat(imageInsertStartedEvent.getValue().image(), is(image)); - insertToken.cancel(); - ArgumentCaptor imageInsertAbortedEvent = forClass(ImageInsertAbortedEvent.class); - verify(eventBus, times(2)).post(imageInsertAbortedEvent.capture()); - verify(bucket).free(); - assertThat(imageInsertAbortedEvent.getValue().image(), is(image)); - } - - @Test - public void failureWithoutExceptionSendsFailedEvent() { - insertToken.onFailure(null, null); - ArgumentCaptor imageInsertFailedEvent = forClass(ImageInsertFailedEvent.class); - verify(eventBus).post(imageInsertFailedEvent.capture()); - verify(bucket).free(); - assertThat(imageInsertFailedEvent.getValue().image(), is(image)); - assertThat(imageInsertFailedEvent.getValue().cause(), nullValue()); - } - - @Test - public void failureSendsFailedEventWithException() { - InsertException insertException = new InsertException(InsertExceptionMode.INTERNAL_ERROR, "Internal error", null); - insertToken.onFailure(insertException, null); - ArgumentCaptor imageInsertFailedEvent = forClass(ImageInsertFailedEvent.class); - verify(eventBus).post(imageInsertFailedEvent.capture()); - verify(bucket).free(); - assertThat(imageInsertFailedEvent.getValue().image(), is(image)); - assertThat(imageInsertFailedEvent.getValue().cause(), is((Throwable) insertException)); - } - - @Test - public void failureBecauseCancelledByUserSendsAbortedEvent() { - InsertException insertException = new InsertException(InsertExceptionMode.CANCELLED, null); - insertToken.onFailure(insertException, null); - ArgumentCaptor imageInsertAbortedEvent = forClass(ImageInsertAbortedEvent.class); - verify(eventBus).post(imageInsertAbortedEvent.capture()); - verify(bucket).free(); - assertThat(imageInsertAbortedEvent.getValue().image(), is(image)); - } - - @Test - public void ignoredMethodsDoNotThrowExceptions() throws ResumeFailedException { - insertToken.onResume(null); - insertToken.onFetchable(null); - insertToken.onGeneratedMetadata(null, null); - } - - @Test - public void generatedUriIsPostedOnSuccess() { - FreenetURI generatedUri = mock(FreenetURI.class); - insertToken.onGeneratedURI(generatedUri, null); - insertToken.onSuccess(null); - ArgumentCaptor imageInsertFinishedEvent = forClass(ImageInsertFinishedEvent.class); - verify(eventBus).post(imageInsertFinishedEvent.capture()); - verify(bucket).free(); - assertThat(imageInsertFinishedEvent.getValue().image(), is(image)); - assertThat(imageInsertFinishedEvent.getValue().resultingUri(), is(generatedUri)); - } - - @Test - public void insertTokenSupplierSuppliesInsertTokens() { - InsertTokenSupplier insertTokenSupplier = new InsertTokenSupplier(freenetInterface); - assertThat(insertTokenSupplier.apply(image), notNullValue()); - } - - @Test - public void backgroundFetchCanBeStarted() throws Exception { - freenetInterface.startFetch(uri, backgroundFetchCallback); - verify(highLevelSimpleClient).fetch(eq(uri), anyLong(), any(ClientGetCallback.class), any(FetchContext.class), anyShort()); - } - - @Test - public void backgroundFetchRegistersSnoopAndRestartsTheRequest() throws Exception { - freenetInterface.startFetch(uri, backgroundFetchCallback); - verify(clientGetter).setMetaSnoop(any(SnoopMetadata.class)); - verify(clientGetter).restart(eq(uri), anyBoolean(), any(ClientContext.class)); - } - - @Test - public void requestIsNotCancelledForImageMimeType() { - verifySnoopCancelsRequestForMimeType("image/png", false); - verify(backgroundFetchCallback, never()).failed(uri); - } - - @Test - public void requestIsCancelledForNullMimeType() { - verifySnoopCancelsRequestForMimeType(null, true); - verify(backgroundFetchCallback, never()).shouldCancel(eq(uri), ArgumentMatchers.any(), anyLong()); - verify(backgroundFetchCallback).failed(uri); - } - - @Test - public void requestIsCancelledForVideoMimeType() { - verifySnoopCancelsRequestForMimeType("video/mkv", true); - verify(backgroundFetchCallback).failed(uri); - } - - @Test - public void requestIsCancelledForAudioMimeType() { - verifySnoopCancelsRequestForMimeType("audio/mpeg", true); - verify(backgroundFetchCallback).failed(uri); - } - - @Test - public void requestIsCancelledForTextMimeType() { - verifySnoopCancelsRequestForMimeType("text/plain", true); - verify(backgroundFetchCallback).failed(uri); - } - - private void verifySnoopCancelsRequestForMimeType(String mimeType, boolean cancel) { - when(backgroundFetchCallback.shouldCancel(eq(uri), eq(mimeType), anyLong())).thenReturn(cancel); - freenetInterface.startFetch(uri, backgroundFetchCallback); - ArgumentCaptor snoopMetadata = forClass(SnoopMetadata.class); - verify(clientGetter).setMetaSnoop(snoopMetadata.capture()); - Metadata metadata = mock(Metadata.class); - when(metadata.getMIMEType()).thenReturn(mimeType); - assertThat(snoopMetadata.getValue().snoopMetadata(metadata, mock(ClientContext.class)), is(cancel)); - } - - @Test - public void callbackOfBackgroundFetchIsNotifiedOnSuccess() throws Exception { - freenetInterface.startFetch(uri, backgroundFetchCallback); - verify(highLevelSimpleClient).fetch(eq(uri), anyLong(), clientGetCallback.capture(), any(FetchContext.class), anyShort()); - when(fetchResult.getMimeType()).thenReturn("image/png"); - when(fetchResult.asByteArray()).thenReturn(new byte[] { 1, 2, 3, 4, 5 }); - clientGetCallback.getValue().onSuccess(fetchResult, mock(ClientGetter.class)); - verify(backgroundFetchCallback).loaded(uri, "image/png", new byte[] { 1, 2, 3, 4, 5 }); - verifyNoMoreInteractions(backgroundFetchCallback); - } - - @Test - public void callbackOfBackgroundFetchIsNotifiedOnFailure() throws Exception { - freenetInterface.startFetch(uri, backgroundFetchCallback); - verify(highLevelSimpleClient).fetch(eq(uri), anyLong(), clientGetCallback.capture(), any(FetchContext.class), anyShort()); - when(fetchResult.getMimeType()).thenReturn("image/png"); - when(fetchResult.asByteArray()).thenReturn(new byte[] { 1, 2, 3, 4, 5 }); - clientGetCallback.getValue().onFailure(new FetchException(ALL_DATA_NOT_FOUND), mock(ClientGetter.class)); - verify(backgroundFetchCallback).failed(uri); - verifyNoMoreInteractions(backgroundFetchCallback); - } - - @Test - public void callbackOfBackgroundFetchIsNotifiedAsFailureIfBucketCanNotBeLoaded() throws Exception { - freenetInterface.startFetch(uri, backgroundFetchCallback); - verify(highLevelSimpleClient).fetch(eq(uri), anyLong(), clientGetCallback.capture(), any(FetchContext.class), anyShort()); - when(fetchResult.getMimeType()).thenReturn("image/png"); - when(fetchResult.asByteArray()).thenThrow(IOException.class); - clientGetCallback.getValue().onSuccess(fetchResult, mock(ClientGetter.class)); - verify(backgroundFetchCallback).failed(uri); - verifyNoMoreInteractions(backgroundFetchCallback); - } - -} diff --git a/src/test/java/net/pterodactylus/sone/core/PreferencesLoaderTest.java b/src/test/java/net/pterodactylus/sone/core/PreferencesLoaderTest.java deleted file mode 100644 index d550f25..0000000 --- a/src/test/java/net/pterodactylus/sone/core/PreferencesLoaderTest.java +++ /dev/null @@ -1,80 +0,0 @@ -package net.pterodactylus.sone.core; - -import static net.pterodactylus.sone.fcp.FcpInterface.FullAccessRequired.WRITING; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.not; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import net.pterodactylus.sone.test.TestValue; -import net.pterodactylus.util.config.Configuration; - -import com.google.common.eventbus.EventBus; -import org.junit.Before; -import org.junit.Test; - -/** - * Unit test for {@link PreferencesLoader}. - */ -public class PreferencesLoaderTest { - - private final EventBus eventBus = mock(EventBus.class); - private final Preferences preferences = new Preferences(eventBus); - private final Configuration configuration = mock(Configuration.class); - private final PreferencesLoader preferencesLoader = - new PreferencesLoader(preferences); - - @Before - public void setupConfiguration() { - setupIntValue("InsertionDelay", 15); - setupIntValue("PostsPerPage", 25); - setupIntValue("ImagesPerPage", 12); - setupIntValue("CharactersPerPost", 150); - setupIntValue("PostCutOffLength", 300); - setupBooleanValue("RequireFullAccess", true); - setupIntValue("PositiveTrust", 50); - setupIntValue("NegativeTrust", -50); - when(configuration.getStringValue("Option/TrustComment")).thenReturn( - TestValue.from("Trusted")); - setupBooleanValue("ActivateFcpInterface", true); - setupIntValue("FcpFullAccessRequired", 1); - } - - private void setupIntValue(String optionName, int value) { - when(configuration.getIntValue("Option/" + optionName)).thenReturn( - TestValue.from(value)); - } - - private void setupBooleanValue(String optionName, boolean value) { - when(configuration.getBooleanValue( - "Option/" + optionName)).thenReturn( - TestValue.from(value)); - } - - @Test - public void configurationIsLoadedCorrectly() { - setupConfiguration(); - preferencesLoader.loadFrom(configuration); - assertThat(preferences.getInsertionDelay(), is(15)); - assertThat(preferences.getPostsPerPage(), is(25)); - assertThat(preferences.getImagesPerPage(), is(12)); - assertThat(preferences.getCharactersPerPost(), is(150)); - assertThat(preferences.getPostCutOffLength(), is(300)); - assertThat(preferences.getRequireFullAccess(), is(true)); - assertThat(preferences.getPositiveTrust(), is(50)); - assertThat(preferences.getNegativeTrust(), is(-50)); - assertThat(preferences.getTrustComment(), is("Trusted")); - assertThat(preferences.getFcpInterfaceActive(), is(true)); - assertThat(preferences.getFcpFullAccessRequired(), is(WRITING)); - } - - @Test - public void configurationIsLoadedCorrectlyWithCutOffLengthMinusOne() { - setupConfiguration(); - setupIntValue("PostCutOffLength", -1); - preferencesLoader.loadFrom(configuration); - assertThat(preferences.getPostCutOffLength(), not(is(-1))); - } - -} diff --git a/src/test/java/net/pterodactylus/sone/core/SoneInserterTest.java b/src/test/java/net/pterodactylus/sone/core/SoneInserterTest.java deleted file mode 100644 index 31d7b6b..0000000 --- a/src/test/java/net/pterodactylus/sone/core/SoneInserterTest.java +++ /dev/null @@ -1,286 +0,0 @@ -package net.pterodactylus.sone.core; - -import static com.google.common.io.ByteStreams.toByteArray; -import static com.google.common.util.concurrent.MoreExecutors.directExecutor; -import static java.lang.System.currentTimeMillis; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.containsString; -import static org.hamcrest.Matchers.instanceOf; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.nullValue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; -import static org.mockito.hamcrest.MockitoHamcrest.argThat; - -import java.io.IOException; -import java.util.HashMap; -import java.util.Map; - -import net.pterodactylus.sone.core.SoneInserter.ManifestCreator; -import net.pterodactylus.sone.core.event.InsertionDelayChangedEvent; -import net.pterodactylus.sone.core.event.SoneEvent; -import net.pterodactylus.sone.core.event.SoneInsertAbortedEvent; -import net.pterodactylus.sone.core.event.SoneInsertedEvent; -import net.pterodactylus.sone.core.event.SoneInsertingEvent; -import net.pterodactylus.sone.data.Album; -import net.pterodactylus.sone.data.Sone; -import net.pterodactylus.sone.main.SonePlugin; - -import freenet.keys.FreenetURI; -import freenet.support.api.ManifestElement; - -import com.google.common.base.Charsets; -import com.google.common.base.Optional; -import com.google.common.eventbus.AsyncEventBus; -import com.google.common.eventbus.EventBus; -import org.junit.Before; -import org.junit.Test; -import org.mockito.ArgumentCaptor; -import org.mockito.invocation.InvocationOnMock; -import org.mockito.stubbing.Answer; - -/** - * Unit test for {@link SoneInserter} and its subclasses. - */ -public class SoneInserterTest { - - private final Core core = mock(Core.class); - private final EventBus eventBus = mock(EventBus.class); - private final FreenetInterface freenetInterface = mock(FreenetInterface.class); - - @Before - public void setupCore() { - UpdateChecker updateChecker = mock(UpdateChecker.class); - when(core.getUpdateChecker()).thenReturn(updateChecker); - when(core.getSone(anyString())).thenReturn(null); - } - - @Test - public void insertionDelayIsForwardedToSoneInserter() { - EventBus eventBus = new AsyncEventBus(directExecutor()); - eventBus.register(new SoneInserter(core, eventBus, freenetInterface, "SoneId")); - eventBus.post(new InsertionDelayChangedEvent(15)); - assertThat(SoneInserter.getInsertionDelay().get(), is(15)); - } - - private Sone createSone(FreenetURI insertUri, String fingerprint) { - Sone sone = mock(Sone.class); - when(sone.getInsertUri()).thenReturn(insertUri); - when(sone.getFingerprint()).thenReturn(fingerprint); - when(sone.getRootAlbum()).thenReturn(mock(Album.class)); - when(core.getSone(anyString())).thenReturn(sone); - return sone; - } - - @Test - public void isModifiedIsTrueIfModificationDetectorSaysSo() { - SoneModificationDetector soneModificationDetector = mock(SoneModificationDetector.class); - when(soneModificationDetector.isModified()).thenReturn(true); - SoneInserter soneInserter = new SoneInserter(core, eventBus, freenetInterface, "SoneId", soneModificationDetector, 1); - assertThat(soneInserter.isModified(), is(true)); - } - - @Test - public void isModifiedIsFalseIfModificationDetectorSaysSo() { - SoneModificationDetector soneModificationDetector = mock(SoneModificationDetector.class); - SoneInserter soneInserter = new SoneInserter(core, eventBus, freenetInterface, "SoneId", soneModificationDetector, 1); - assertThat(soneInserter.isModified(), is(false)); - } - - @Test - public void lastFingerprintIsStoredCorrectly() { - SoneInserter soneInserter = new SoneInserter(core, eventBus, freenetInterface, "SoneId"); - soneInserter.setLastInsertFingerprint("last-fingerprint"); - assertThat(soneInserter.getLastInsertFingerprint(), is("last-fingerprint")); - } - - @Test - public void soneInserterStopsWhenItShould() { - SoneInserter soneInserter = new SoneInserter(core, eventBus, freenetInterface, "SoneId"); - soneInserter.stop(); - soneInserter.serviceRun(); - } - - @Test - public void soneInserterInsertsASoneIfItIsEligible() throws SoneException { - FreenetURI insertUri = mock(FreenetURI.class); - final FreenetURI finalUri = mock(FreenetURI.class); - String fingerprint = "fingerprint"; - Sone sone = createSone(insertUri, fingerprint); - SoneModificationDetector soneModificationDetector = mock(SoneModificationDetector.class); - when(soneModificationDetector.isEligibleForInsert()).thenReturn(true); - when(freenetInterface.insertDirectory(eq(insertUri), any(HashMap.class), eq("index.html"))).thenReturn(finalUri); - final SoneInserter soneInserter = new SoneInserter(core, eventBus, freenetInterface, "SoneId", soneModificationDetector, 1); - doAnswer(new Answer() { - @Override - public Void answer(InvocationOnMock invocation) throws Throwable { - soneInserter.stop(); - return null; - } - }).when(core).touchConfiguration(); - soneInserter.serviceRun(); - ArgumentCaptor soneEvents = ArgumentCaptor.forClass(SoneEvent.class); - verify(freenetInterface).insertDirectory(eq(insertUri), any(HashMap.class), eq("index.html")); - verify(eventBus, times(2)).post(soneEvents.capture()); - assertThat(soneEvents.getAllValues().get(0), instanceOf(SoneInsertingEvent.class)); - assertThat(soneEvents.getAllValues().get(0).sone(), is(sone)); - assertThat(soneEvents.getAllValues().get(1), instanceOf(SoneInsertedEvent.class)); - assertThat(soneEvents.getAllValues().get(1).sone(), is(sone)); - } - - @Test - public void soneInserterBailsOutIfItIsStoppedWhileInserting() throws SoneException { - FreenetURI insertUri = mock(FreenetURI.class); - final FreenetURI finalUri = mock(FreenetURI.class); - String fingerprint = "fingerprint"; - Sone sone = createSone(insertUri, fingerprint); - SoneModificationDetector soneModificationDetector = mock(SoneModificationDetector.class); - when(soneModificationDetector.isEligibleForInsert()).thenReturn(true); - final SoneInserter soneInserter = new SoneInserter(core, eventBus, freenetInterface, "SoneId", soneModificationDetector, 1); - when(freenetInterface.insertDirectory(eq(insertUri), any(HashMap.class), eq("index.html"))).thenAnswer(new Answer() { - @Override - public FreenetURI answer(InvocationOnMock invocation) throws Throwable { - soneInserter.stop(); - return finalUri; - } - }); - soneInserter.serviceRun(); - ArgumentCaptor soneEvents = ArgumentCaptor.forClass(SoneEvent.class); - verify(freenetInterface).insertDirectory(eq(insertUri), any(HashMap.class), eq("index.html")); - verify(eventBus, times(2)).post(soneEvents.capture()); - assertThat(soneEvents.getAllValues().get(0), instanceOf(SoneInsertingEvent.class)); - assertThat(soneEvents.getAllValues().get(0).sone(), is(sone)); - assertThat(soneEvents.getAllValues().get(1), instanceOf(SoneInsertedEvent.class)); - assertThat(soneEvents.getAllValues().get(1).sone(), is(sone)); - verify(core, never()).touchConfiguration(); - } - - @Test - public void soneInserterDoesNotInsertSoneIfItIsNotEligible() throws SoneException { - FreenetURI insertUri = mock(FreenetURI.class); - String fingerprint = "fingerprint"; - Sone sone = createSone(insertUri, fingerprint); - SoneModificationDetector soneModificationDetector = mock(SoneModificationDetector.class); - final SoneInserter soneInserter = new SoneInserter(core, eventBus, freenetInterface, "SoneId", soneModificationDetector, 1); - new Thread(new Runnable() { - @Override - public void run() { - try { - Thread.sleep(500); - } catch (InterruptedException ie1) { - throw new RuntimeException(ie1); - } - soneInserter.stop(); - } - }).start(); - soneInserter.serviceRun(); - verify(freenetInterface, never()).insertDirectory(eq(insertUri), any(HashMap.class), eq("index.html")); - verify(eventBus, never()).post(argThat(org.hamcrest.Matchers.any(SoneEvent.class))); - } - - @Test - public void soneInserterPostsAbortedEventIfAnExceptionOccurs() throws SoneException { - FreenetURI insertUri = mock(FreenetURI.class); - String fingerprint = "fingerprint"; - Sone sone = createSone(insertUri, fingerprint); - SoneModificationDetector soneModificationDetector = mock(SoneModificationDetector.class); - when(soneModificationDetector.isEligibleForInsert()).thenReturn(true); - final SoneInserter soneInserter = new SoneInserter(core, eventBus, freenetInterface, "SoneId", soneModificationDetector, 1); - final SoneException soneException = new SoneException(new Exception()); - when(freenetInterface.insertDirectory(eq(insertUri), any(HashMap.class), eq("index.html"))).thenAnswer(new Answer() { - @Override - public FreenetURI answer(InvocationOnMock invocation) throws Throwable { - soneInserter.stop(); - throw soneException; - } - }); - soneInserter.serviceRun(); - ArgumentCaptor soneEvents = ArgumentCaptor.forClass(SoneEvent.class); - verify(freenetInterface).insertDirectory(eq(insertUri), any(HashMap.class), eq("index.html")); - verify(eventBus, times(2)).post(soneEvents.capture()); - assertThat(soneEvents.getAllValues().get(0), instanceOf(SoneInsertingEvent.class)); - assertThat(soneEvents.getAllValues().get(0).sone(), is(sone)); - assertThat(soneEvents.getAllValues().get(1), instanceOf(SoneInsertAbortedEvent.class)); - assertThat(soneEvents.getAllValues().get(1).sone(), is(sone)); - verify(core, never()).touchConfiguration(); - } - - @Test - public void soneInserterExitsIfSoneIsUnknown() { - SoneModificationDetector soneModificationDetector = - mock(SoneModificationDetector.class); - SoneInserter soneInserter = - new SoneInserter(core, eventBus, freenetInterface, "SoneId", - soneModificationDetector, 1); - when(soneModificationDetector.isEligibleForInsert()).thenReturn(true); - when(core.getSone("SoneId")).thenReturn(null); - soneInserter.serviceRun(); - } - - @Test - public void soneInserterCatchesExceptionAndContinues() { - SoneModificationDetector soneModificationDetector = - mock(SoneModificationDetector.class); - final SoneInserter soneInserter = - new SoneInserter(core, eventBus, freenetInterface, "SoneId", - soneModificationDetector, 1); - Answer> stopInserterAndThrowException = - new Answer>() { - @Override - public Optional answer( - InvocationOnMock invocation) { - soneInserter.stop(); - throw new NullPointerException(); - } - }; - when(soneModificationDetector.isEligibleForInsert()).thenAnswer( - stopInserterAndThrowException); - soneInserter.serviceRun(); - } - - @Test - public void templateIsRenderedCorrectlyForManifestElement() - throws IOException { - Map soneProperties = new HashMap<>(); - soneProperties.put("id", "SoneId"); - ManifestCreator manifestCreator = new ManifestCreator(core, soneProperties); - long now = currentTimeMillis(); - when(core.getStartupTime()).thenReturn(now); - ManifestElement manifestElement = manifestCreator.createManifestElement("test.txt", "plain/text; charset=utf-8", "sone-inserter-manifest.txt"); - assertThat(manifestElement.getName(), is("test.txt")); - assertThat(manifestElement.getMimeTypeOverride(), is("plain/text; charset=utf-8")); - String templateContent = new String(toByteArray(manifestElement.getData().getInputStream()), Charsets.UTF_8); - assertThat(templateContent, containsString("Sone Version: " + SonePlugin.getPluginVersion() + "\n")); - assertThat(templateContent, containsString("Core Startup: " + now + "\n")); - assertThat(templateContent, containsString("Sone ID: " + "SoneId" + "\n")); - } - - @Test - public void invalidTemplateReturnsANullManifestElement() { - Map soneProperties = new HashMap<>(); - ManifestCreator manifestCreator = new ManifestCreator(core, soneProperties); - assertThat(manifestCreator.createManifestElement("test.txt", - "plain/text; charset=utf-8", - "sone-inserter-invalid-manifest.txt"), - nullValue()); - } - - @Test - public void errorWhileRenderingTemplateReturnsANullManifestElement() { - Map soneProperties = new HashMap<>(); - ManifestCreator manifestCreator = new ManifestCreator(core, soneProperties); - when(core.toString()).thenThrow(NullPointerException.class); - assertThat(manifestCreator.createManifestElement("test.txt", - "plain/text; charset=utf-8", - "sone-inserter-faulty-manifest.txt"), - nullValue()); - } - -} diff --git a/src/test/java/net/pterodactylus/sone/core/SoneParserTest.java b/src/test/java/net/pterodactylus/sone/core/SoneParserTest.java deleted file mode 100644 index 400b9ae..0000000 --- a/src/test/java/net/pterodactylus/sone/core/SoneParserTest.java +++ /dev/null @@ -1,820 +0,0 @@ -package net.pterodactylus.sone.core; - -import static com.google.common.base.Optional.of; -import static freenet.keys.InsertableClientSSK.createRandom; -import static java.lang.System.currentTimeMillis; -import static java.util.UUID.randomUUID; -import static java.util.concurrent.TimeUnit.DAYS; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.hasSize; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.not; -import static org.hamcrest.Matchers.notNullValue; -import static org.hamcrest.Matchers.nullValue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import java.io.InputStream; -import java.net.MalformedURLException; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - -import net.pterodactylus.sone.data.Album; -import net.pterodactylus.sone.data.Album.Modifier; -import net.pterodactylus.sone.data.Client; -import net.pterodactylus.sone.data.Image; -import net.pterodactylus.sone.data.Post; -import net.pterodactylus.sone.data.PostReply; -import net.pterodactylus.sone.data.Profile; -import net.pterodactylus.sone.data.Sone; -import net.pterodactylus.sone.database.AlbumBuilder; -import net.pterodactylus.sone.database.Database; -import net.pterodactylus.sone.database.ImageBuilder; -import net.pterodactylus.sone.database.PostBuilder; -import net.pterodactylus.sone.database.PostReplyBuilder; -import net.pterodactylus.sone.database.SoneBuilder; -import net.pterodactylus.sone.database.memory.MemorySoneBuilder; -import net.pterodactylus.sone.freenet.wot.Identity; -import net.pterodactylus.sone.freenet.wot.OwnIdentity; - -import freenet.crypt.DummyRandomSource; -import freenet.keys.FreenetURI; -import freenet.keys.InsertableClientSSK; - -import com.google.common.base.Optional; -import com.google.common.collect.ArrayListMultimap; -import com.google.common.collect.ImmutableSet; -import com.google.common.collect.ListMultimap; -import org.junit.Before; -import org.junit.Test; -import org.mockito.invocation.InvocationOnMock; -import org.mockito.stubbing.Answer; - -/** - * Unit test for {@link SoneParser}. - */ -public class SoneParserTest { - - private final Database database = mock(Database.class); - private final SoneParser soneParser = new SoneParser(database); - private final Sone sone = mock(Sone.class); - private FreenetURI requestUri = mock(FreenetURI.class); - private final PostBuilder postBuilder = mock(PostBuilder.class); - private final List createdPosts = new ArrayList<>(); - private Post post = mock(Post.class); - private final PostReplyBuilder postReplyBuilder = mock(PostReplyBuilder.class); - private final Set createdPostReplies = new HashSet<>(); - private PostReply postReply = mock(PostReply.class); - private final AlbumBuilder albumBuilder = mock(AlbumBuilder.class); - private final ListMultimap - nestedAlbums = ArrayListMultimap.create(); - private final ListMultimap albumImages = ArrayListMultimap.create(); - private Album album = mock(Album.class); - private final Map albums = new HashMap<>(); - private final ImageBuilder imageBuilder = mock(ImageBuilder.class); - private Image image = mock(Image.class); - private final Map images = new HashMap<>(); - - @Before - public void setupSone() { - setupSone(this.sone, Identity.class); - } - - private void setupSone(Sone sone, Class identityClass) { - Identity identity = mock(identityClass); - InsertableClientSSK clientSSK = - createRandom(new DummyRandomSource(), "WoT"); - when(identity.getRequestUri()).thenReturn(clientSSK.getURI().toString()); - when(identity.getId()).thenReturn("identity"); - when(sone.getId()).thenReturn("identity"); - when(sone.getIdentity()).thenReturn(identity); - requestUri = clientSSK.getURI().setKeyType("USK").setDocName("Sone"); - when(sone.getRequestUri()).thenAnswer(new Answer() { - @Override - public FreenetURI answer(InvocationOnMock invocation) - throws Throwable { - return requestUri; - } - }); - when(sone.getTime()) - .thenReturn(currentTimeMillis() - DAYS.toMillis(1)); - } - - @Before - public void setupSoneBuilder() { - when(database.newSoneBuilder()).thenAnswer(new Answer() { - @Override - public SoneBuilder answer(InvocationOnMock invocation) { - return new MemorySoneBuilder(null); - } - }); - } - - @Before - public void setupPost() { - when(post.getRecipientId()).thenReturn(Optional.absent()); - } - - @Before - public void setupPostBuilder() { - when(postBuilder.withId(anyString())).thenAnswer(new Answer() { - @Override - public PostBuilder answer(InvocationOnMock invocation) throws Throwable { - when(post.getId()).thenReturn((String) invocation.getArguments()[0]); - return postBuilder; - } - }); - when(postBuilder.from(anyString())).thenAnswer(new Answer() { - @Override - public PostBuilder answer(InvocationOnMock invocation) throws Throwable { - final Sone sone = mock(Sone.class); - when(sone.getId()).thenReturn((String) invocation.getArguments()[0]); - when(post.getSone()).thenReturn(sone); - return postBuilder; - } - }); - when(postBuilder.withTime(anyLong())).thenAnswer(new Answer() { - @Override - public PostBuilder answer(InvocationOnMock invocation) throws Throwable { - when(post.getTime()).thenReturn((Long) invocation.getArguments()[0]); - return postBuilder; - } - }); - when(postBuilder.withText(anyString())).thenAnswer(new Answer() { - @Override - public PostBuilder answer(InvocationOnMock invocation) throws Throwable { - when(post.getText()).thenReturn((String) invocation.getArguments()[0]); - return postBuilder; - } - }); - when(postBuilder.to(anyString())).thenAnswer(new Answer() { - @Override - public PostBuilder answer(InvocationOnMock invocation) throws Throwable { - when(post.getRecipientId()).thenReturn(of((String) invocation.getArguments()[0])); - return postBuilder; - } - }); - when(postBuilder.build()).thenAnswer(new Answer() { - @Override - public Post answer(InvocationOnMock invocation) throws Throwable { - Post post = SoneParserTest.this.post; - SoneParserTest.this.post = mock(Post.class); - setupPost(); - createdPosts.add(post); - return post; - } - }); - when(database.newPostBuilder()).thenReturn(postBuilder); - } - - @Before - public void setupPostReplyBuilder() { - when(postReplyBuilder.withId(anyString())).thenAnswer(new Answer() { - @Override - public PostReplyBuilder answer(InvocationOnMock invocation) throws Throwable { - when(postReply.getId()).thenReturn((String) invocation.getArguments()[0]); - return postReplyBuilder; - } - }); - when(postReplyBuilder.from(anyString())).thenAnswer( - new Answer() { - @Override - public PostReplyBuilder answer( - InvocationOnMock invocation) throws Throwable { - Sone sone = when(mock(Sone.class).getId()).thenReturn( - (String) invocation.getArguments()[0]) - .getMock(); - when(postReply.getSone()).thenReturn(sone); - return postReplyBuilder; - } - }); - when(postReplyBuilder.to(anyString())).thenAnswer( - new Answer() { - @Override - public PostReplyBuilder answer( - InvocationOnMock invocation) throws Throwable { - when(postReply.getPostId()).thenReturn( - (String) invocation.getArguments()[0]); - Post post = when(mock(Post.class).getId()).thenReturn( - (String) invocation.getArguments()[0]) - .getMock(); - when(postReply.getPost()).thenReturn(of(post)); - return postReplyBuilder; - } - }); - when(postReplyBuilder.withTime(anyLong())).thenAnswer( - new Answer() { - @Override - public PostReplyBuilder answer( - InvocationOnMock invocation) throws Throwable { - when(postReply.getTime()).thenReturn( - (Long) invocation.getArguments()[0]); - return postReplyBuilder; - } - }); - when(postReplyBuilder.withText(anyString())).thenAnswer(new Answer() { - @Override - public PostReplyBuilder answer(InvocationOnMock invocation) throws Throwable { - when(postReply.getText()).thenReturn((String) invocation.getArguments()[0]); - return postReplyBuilder; - } - }); - when(postReplyBuilder.build()).thenAnswer(new Answer() { - @Override - public PostReply answer(InvocationOnMock invocation) throws Throwable { - PostReply postReply = SoneParserTest.this.postReply; - createdPostReplies.add(postReply); - SoneParserTest.this.postReply = mock(PostReply.class); - return postReply; - } - }); - when(database.newPostReplyBuilder()).thenReturn(postReplyBuilder); - } - - @Before - public void setupAlbum() { - final Album album = SoneParserTest.this.album; - doAnswer(new Answer() { - @Override - public Void answer(InvocationOnMock invocation) { - nestedAlbums.put(album, (Album) invocation.getArguments()[0]); - return null; - } - }).when(album).addAlbum(any(Album.class)); - doAnswer(new Answer() { - @Override - public Void answer(InvocationOnMock invocation) { - albumImages.put(album, (Image) invocation.getArguments()[0]); - return null; - } - }).when(album).addImage(any(Image.class)); - when(album.getAlbums()).thenAnswer(new Answer>() { - @Override - public List answer(InvocationOnMock invocation) { - return nestedAlbums.get(album); - } - }); - when(album.getImages()).thenAnswer(new Answer>() { - @Override - public List answer(InvocationOnMock invocation) { - return albumImages.get(album); - } - }); - final Modifier albumModifier = new Modifier() { - private String title = album.getTitle(); - private String description = album.getDescription(); - - @Override - public Modifier setTitle(String title) { - this.title = title; - return this; - } - - @Override - public Modifier setDescription(String description) { - this.description = description; - return this; - } - - @Override - public Album update() throws IllegalStateException { - when(album.getTitle()).thenReturn(title); - when(album.getDescription()).thenReturn(description); - return album; - } - }; - when(album.modify()).thenReturn(albumModifier); - } - - @Before - public void setupAlbumBuilder() { - when(albumBuilder.withId(anyString())).thenAnswer(new Answer() { - @Override - public AlbumBuilder answer(InvocationOnMock invocation) { - when(album.getId()).thenReturn((String) invocation.getArguments()[0]); - return albumBuilder; - } - }); - when(albumBuilder.randomId()).thenAnswer(new Answer() { - @Override - public AlbumBuilder answer(InvocationOnMock invocation) { - when(album.getId()).thenReturn(randomUUID().toString()); - return albumBuilder; - } - }); - when(albumBuilder.by(any(Sone.class))).thenAnswer(new Answer() { - @Override - public AlbumBuilder answer(InvocationOnMock invocation) { - when(album.getSone()).thenReturn((Sone) invocation.getArguments()[0]); - return albumBuilder; - } - }); - when(albumBuilder.build()).thenAnswer(new Answer() { - @Override - public Album answer(InvocationOnMock invocation) { - Album album = SoneParserTest.this.album; - albums.put(album.getId(), album); - SoneParserTest.this.album = mock(Album.class); - setupAlbum(); - return album; - } - }); - when(database.newAlbumBuilder()).thenReturn(albumBuilder); - } - - @Before - public void setupAlbums() { - when(database.getAlbum(anyString())).thenAnswer(new Answer() { - @Override - public Album answer(InvocationOnMock invocation) - throws Throwable { - return albums.get(invocation.getArguments()[0]); - } - }); - } - - @Before - public void setupImage() { - final Image image = SoneParserTest.this.image; - Image.Modifier modifier = new Image.Modifier() { - private Sone sone = image.getSone(); - private long creationTime = image.getCreationTime(); - private String key = image.getKey(); - private String title = image.getTitle(); - private String description = image.getDescription(); - private int width = image.getWidth(); - private int height = image.getHeight(); - - @Override - public Image.Modifier setSone(Sone sone) { - this.sone = sone; - return this; - } - - @Override - public Image.Modifier setCreationTime(long creationTime) { - this.creationTime = creationTime; - return this; - } - - @Override - public Image.Modifier setKey(String key) { - this.key = key; - return this; - } - - @Override - public Image.Modifier setTitle(String title) { - this.title = title; - return this; - } - - @Override - public Image.Modifier setDescription(String description) { - this.description = description; - return this; - } - - @Override - public Image.Modifier setWidth(int width) { - this.width = width; - return this; - } - - @Override - public Image.Modifier setHeight(int height) { - this.height = height; - return this; - } - - @Override - public Image update() throws IllegalStateException { - when(image.getSone()).thenReturn(sone); - when(image.getCreationTime()).thenReturn(creationTime); - when(image.getKey()).thenReturn(key); - when(image.getTitle()).thenReturn(title); - when(image.getDescription()).thenReturn(description); - when(image.getWidth()).thenReturn(width); - when(image.getHeight()).thenReturn(height); - return image; - } - }; - when(image.getSone()).thenReturn(sone); - when(image.modify()).thenReturn(modifier); - } - - @Before - public void setupImageBuilder() { - when(imageBuilder.randomId()).thenAnswer(new Answer() { - @Override - public ImageBuilder answer(InvocationOnMock invocation) { - when(image.getId()).thenReturn(randomUUID().toString()); - return imageBuilder; - } - }); - when(imageBuilder.withId(anyString())).thenAnswer(new Answer() { - @Override - public ImageBuilder answer(InvocationOnMock invocation) { - when(image.getId()).thenReturn( - (String) invocation.getArguments()[0]); - return imageBuilder; - } - }); - when(imageBuilder.build()).thenAnswer(new Answer() { - @Override - public Image answer(InvocationOnMock invocation) { - Image image = SoneParserTest.this.image; - images.put(image.getId(), image); - SoneParserTest.this.image = mock(Image.class); - setupImage(); - return image; - } - }); - when(database.newImageBuilder()).thenReturn(imageBuilder); - } - - @Before - public void setupImages() { - when(database.getImage(anyString())).thenAnswer(new Answer() { - @Override - public Image answer(InvocationOnMock invocation) - throws Throwable { - return images.get(invocation.getArguments()[0]); - } - }); - } - @Test - public void parsingASoneFailsWhenDocumentIsNotXml() throws SoneException { - InputStream inputStream = getClass().getResourceAsStream("sone-parser-not-xml.xml"); - assertThat(soneParser.parseSone(sone, inputStream), nullValue()); - } - - @Test - public void parsingASoneFailsWhenDocumentHasNegativeProtocolVersion() throws SoneException { - InputStream inputStream = getClass().getResourceAsStream("sone-parser-negative-protocol-version.xml"); - assertThat(soneParser.parseSone(sone, inputStream), nullValue()); - } - - @Test - public void parsingASoneFailsWhenProtocolVersionIsTooLarge() throws SoneException { - InputStream inputStream = getClass().getResourceAsStream("sone-parser-too-large-protocol-version.xml"); - assertThat(soneParser.parseSone(sone, inputStream), nullValue()); - } - - @Test - public void parsingASoneFailsWhenThereIsNoTime() throws SoneException { - InputStream inputStream = getClass().getResourceAsStream("sone-parser-no-time.xml"); - assertThat(soneParser.parseSone(sone, inputStream), nullValue()); - } - - @Test - public void parsingASoneFailsWhenTimeIsNotNumeric() throws SoneException { - InputStream inputStream = getClass().getResourceAsStream("sone-parser-time-not-numeric.xml"); - assertThat(soneParser.parseSone(sone, inputStream), nullValue()); - } - - @Test - public void parsingASoneFailsWhenProfileIsMissing() throws SoneException { - InputStream inputStream = getClass().getResourceAsStream("sone-parser-no-profile.xml"); - assertThat(soneParser.parseSone(sone, inputStream), nullValue()); - } - - @Test - public void parsingASoneFailsWhenProfileFieldIsMissingAFieldName() throws SoneException { - InputStream inputStream = getClass().getResourceAsStream("sone-parser-profile-missing-field-name.xml"); - assertThat(soneParser.parseSone(sone, inputStream), nullValue()); - } - - @Test - public void parsingASoneFailsWhenProfileFieldNameIsEmpty() throws SoneException { - InputStream inputStream = getClass().getResourceAsStream("sone-parser-profile-empty-field-name.xml"); - assertThat(soneParser.parseSone(sone, inputStream), nullValue()); - } - - @Test - public void parsingASoneFailsWhenProfileFieldNameIsNotUnique() throws SoneException { - InputStream inputStream = getClass().getResourceAsStream("sone-parser-profile-duplicate-field-name.xml"); - assertThat(soneParser.parseSone(sone, inputStream), nullValue()); - } - - @Test - public void parsingASoneSucceedsWithoutPayload() throws SoneException { - InputStream inputStream = getClass().getResourceAsStream("sone-parser-no-payload.xml"); - assertThat(soneParser.parseSone(sone, inputStream).getTime(), is( - 1407197508000L)); - } - - @Test - public void parsingALocalSoneSucceedsWithoutPayload() throws SoneException { - InputStream inputStream = getClass().getResourceAsStream("sone-parser-no-payload.xml"); - Sone localSone = mock(Sone.class); - setupSone(localSone, OwnIdentity.class); - when(localSone.isLocal()).thenReturn(true); - Sone parsedSone = soneParser.parseSone(localSone, inputStream); - assertThat(parsedSone.getTime(), is(1407197508000L)); - assertThat(parsedSone.isLocal(), is(true)); - } - - @Test - public void parsingASoneSucceedsWithoutProtocolVersion() throws SoneException { - InputStream inputStream = getClass().getResourceAsStream("sone-parser-missing-protocol-version.xml"); - assertThat(soneParser.parseSone(sone, inputStream), not( - nullValue())); - } - - @Test - public void parsingASoneFailsWithMissingClientName() throws SoneException { - InputStream inputStream = getClass().getResourceAsStream("sone-parser-missing-client-name.xml"); - assertThat(soneParser.parseSone(sone, inputStream), nullValue()); - } - - @Test - public void parsingASoneFailsWithMissingClientVersion() throws SoneException { - InputStream inputStream = getClass().getResourceAsStream("sone-parser-missing-client-version.xml"); - assertThat(soneParser.parseSone(sone, inputStream), nullValue()); - } - - @Test - public void parsingASoneSucceedsWithClientInfo() throws SoneException { - InputStream inputStream = getClass().getResourceAsStream("sone-parser-with-client-info.xml"); - assertThat(soneParser.parseSone(sone, inputStream).getClient(), is(new Client("some-client", "some-version"))); - } - - @Test - public void parsingASoneSucceedsWithProfile() throws SoneException, - MalformedURLException { - InputStream inputStream = getClass().getResourceAsStream("sone-parser-with-profile.xml"); - final Profile profile = soneParser.parseSone(sone, inputStream).getProfile(); - assertThat(profile.getFirstName(), is("first")); - assertThat(profile.getMiddleName(), is("middle")); - assertThat(profile.getLastName(), is("last")); - assertThat(profile.getBirthDay(), is(18)); - assertThat(profile.getBirthMonth(), is(12)); - assertThat(profile.getBirthYear(), is(1976)); - } - - @Test - public void parsingASoneSucceedsWithoutProfileFields() throws SoneException, MalformedURLException { - InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-fields.xml"); - assertThat(soneParser.parseSone(sone, inputStream), notNullValue()); - } - - @Test - public void parsingASoneFailsWithoutPostId() throws SoneException, MalformedURLException { - InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-post-id.xml"); - assertThat(soneParser.parseSone(sone, inputStream), nullValue()); - } - - @Test - public void parsingASoneFailsWithoutPostTime() throws SoneException, MalformedURLException { - InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-post-time.xml"); - assertThat(soneParser.parseSone(sone, inputStream), nullValue()); - } - - @Test - public void parsingASoneFailsWithoutPostText() throws SoneException, MalformedURLException { - InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-post-text.xml"); - assertThat(soneParser.parseSone(sone, inputStream), nullValue()); - } - - @Test - public void parsingASoneFailsWithInvalidPostTime() throws SoneException, MalformedURLException { - InputStream inputStream = getClass().getResourceAsStream("sone-parser-with-invalid-post-time.xml"); - assertThat(soneParser.parseSone(sone, inputStream), nullValue()); - } - - @Test - public void parsingASoneSucceedsWithValidPostTime() throws SoneException, MalformedURLException { - InputStream inputStream = getClass().getResourceAsStream("sone-parser-with-valid-post-time.xml"); - final List posts = soneParser.parseSone(sone, inputStream).getPosts(); - assertThat(posts, is(createdPosts)); - assertThat(posts.get(0).getSone().getId(), is(sone.getId())); - assertThat(posts.get(0).getId(), is("post-id")); - assertThat(posts.get(0).getTime(), is(1407197508000L)); - assertThat(posts.get(0).getRecipientId(), is(Optional.absent())); - assertThat(posts.get(0).getText(), is("text")); - } - - @Test - public void parsingASoneSucceedsWithRecipient() throws SoneException, MalformedURLException { - InputStream inputStream = getClass().getResourceAsStream("sone-parser-with-recipient.xml"); - final List posts = soneParser.parseSone(sone, inputStream).getPosts(); - assertThat(posts, is(createdPosts)); - assertThat(posts.get(0).getSone().getId(), is(sone.getId())); - assertThat(posts.get(0).getId(), is("post-id")); - assertThat(posts.get(0).getTime(), is(1407197508000L)); - assertThat(posts.get(0).getRecipientId(), is(of( - "1234567890123456789012345678901234567890123"))); - assertThat(posts.get(0).getText(), is("text")); - } - - @Test - public void parsingASoneSucceedsWithInvalidRecipient() throws SoneException, MalformedURLException { - InputStream inputStream = getClass().getResourceAsStream("sone-parser-with-invalid-recipient.xml"); - final List posts = soneParser.parseSone(sone, inputStream).getPosts(); - assertThat(posts, is(createdPosts)); - assertThat(posts.get(0).getSone().getId(), is(sone.getId())); - assertThat(posts.get(0).getId(), is("post-id")); - assertThat(posts.get(0).getTime(), is(1407197508000L)); - assertThat(posts.get(0).getRecipientId(), is(Optional.absent())); - assertThat(posts.get(0).getText(), is("text")); - } - - @Test - public void parsingASoneFailsWithoutPostReplyId() throws SoneException, MalformedURLException { - InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-post-reply-id.xml"); - assertThat(soneParser.parseSone(sone, inputStream), nullValue()); - } - - @Test - public void parsingASoneFailsWithoutPostReplyPostId() throws SoneException, MalformedURLException { - InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-post-reply-post-id.xml"); - assertThat(soneParser.parseSone(sone, inputStream), nullValue()); - } - - @Test - public void parsingASoneFailsWithoutPostReplyTime() throws SoneException, MalformedURLException { - InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-post-reply-time.xml"); - assertThat(soneParser.parseSone(sone, inputStream), nullValue()); - } - - @Test - public void parsingASoneFailsWithoutPostReplyText() throws SoneException, MalformedURLException { - InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-post-reply-text.xml"); - assertThat(soneParser.parseSone(sone, inputStream), nullValue()); - } - - @Test - public void parsingASoneFailsWithInvalidPostReplyTime() throws SoneException, MalformedURLException { - InputStream inputStream = getClass().getResourceAsStream("sone-parser-with-invalid-post-reply-time.xml"); - assertThat(soneParser.parseSone(sone, inputStream), nullValue()); - } - - @Test - public void parsingASoneSucceedsWithValidPostReplyTime() throws SoneException, MalformedURLException { - InputStream inputStream = getClass().getResourceAsStream("sone-parser-with-valid-post-reply-time.xml"); - final Set postReplies = soneParser.parseSone(sone, inputStream).getReplies(); - assertThat(postReplies, is(createdPostReplies)); - PostReply postReply = createdPostReplies.iterator().next(); - assertThat(postReply.getId(), is("reply-id")); - assertThat(postReply.getPostId(), is("post-id")); - assertThat(postReply.getPost().get().getId(), is("post-id")); - assertThat(postReply.getSone().getId(), is("identity")); - assertThat(postReply.getTime(), is(1407197508000L)); - assertThat(postReply.getText(), is("reply-text")); - } - - @Test - public void parsingASoneSucceedsWithoutLikedPostIds() throws SoneException, MalformedURLException { - InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-liked-post-ids.xml"); - assertThat(soneParser.parseSone(sone, inputStream), not( - nullValue())); - } - - @Test - public void parsingASoneSucceedsWithLikedPostIds() throws SoneException, MalformedURLException { - InputStream inputStream = getClass().getResourceAsStream("sone-parser-with-liked-post-ids.xml"); - assertThat(soneParser.parseSone(sone, inputStream).getLikedPostIds(), is( - (Set) ImmutableSet.of("liked-post-id"))); - } - - @Test - public void parsingASoneSucceedsWithoutLikedPostReplyIds() throws SoneException, MalformedURLException { - InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-liked-post-reply-ids.xml"); - assertThat(soneParser.parseSone(sone, inputStream), not( - nullValue())); - } - - @Test - public void parsingASoneSucceedsWithLikedPostReplyIds() throws SoneException, MalformedURLException { - InputStream inputStream = getClass().getResourceAsStream("sone-parser-with-liked-post-reply-ids.xml"); - assertThat(soneParser.parseSone(sone, inputStream).getLikedReplyIds(), is( - (Set) ImmutableSet.of("liked-post-reply-id"))); - } - - @Test - public void parsingASoneSucceedsWithoutAlbums() throws SoneException, MalformedURLException { - InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-albums.xml"); - assertThat(soneParser.parseSone(sone, inputStream), not( - nullValue())); - } - - @Test - public void parsingASoneFailsWithoutAlbumId() throws SoneException, MalformedURLException { - InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-album-id.xml"); - assertThat(soneParser.parseSone(sone, inputStream), nullValue()); - } - - @Test - public void parsingASoneFailsWithoutAlbumTitle() throws SoneException, MalformedURLException { - InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-album-title.xml"); - assertThat(soneParser.parseSone(sone, inputStream), nullValue()); - } - - @Test - public void parsingASoneSucceedsWithNestedAlbums() throws SoneException, MalformedURLException { - InputStream inputStream = getClass().getResourceAsStream("sone-parser-with-multiple-albums.xml"); - final Sone parsedSone = soneParser.parseSone(sone, inputStream); - assertThat(parsedSone, not(nullValue())); - assertThat(parsedSone.getRootAlbum().getAlbums(), hasSize(1)); - Album album = parsedSone.getRootAlbum().getAlbums().get(0); - assertThat(album.getId(), is("album-id-1")); - assertThat(album.getTitle(), is("album-title")); - assertThat(album.getDescription(), is("album-description")); - assertThat(album.getAlbums(), hasSize(1)); - Album nestedAlbum = album.getAlbums().get(0); - assertThat(nestedAlbum.getId(), is("album-id-2")); - assertThat(nestedAlbum.getTitle(), is("album-title-2")); - assertThat(nestedAlbum.getDescription(), is("album-description-2")); - assertThat(nestedAlbum.getAlbums(), hasSize(0)); - } - - @Test - public void parsingASoneFailsWithInvalidParentAlbumId() throws SoneException, MalformedURLException { - InputStream inputStream = getClass().getResourceAsStream("sone-parser-with-invalid-parent-album-id.xml"); - assertThat(soneParser.parseSone(sone, inputStream), nullValue()); - } - - @Test - public void parsingASoneSucceedsWithoutImages() throws SoneException, MalformedURLException { - InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-images.xml"); - assertThat(soneParser.parseSone(sone, inputStream), not( - nullValue())); - } - - @Test - public void parsingASoneFailsWithoutImageId() throws SoneException, MalformedURLException { - InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-image-id.xml"); - assertThat(soneParser.parseSone(sone, inputStream), nullValue()); - } - - @Test - public void parsingASoneFailsWithoutImageTime() throws SoneException, MalformedURLException { - InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-image-time.xml"); - assertThat(soneParser.parseSone(sone, inputStream), nullValue()); - } - - @Test - public void parsingASoneFailsWithoutImageKey() throws SoneException, MalformedURLException { - InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-image-key.xml"); - assertThat(soneParser.parseSone(sone, inputStream), nullValue()); - } - - @Test - public void parsingASoneFailsWithoutImageTitle() throws SoneException, MalformedURLException { - InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-image-title.xml"); - assertThat(soneParser.parseSone(sone, inputStream), nullValue()); - } - - @Test - public void parsingASoneFailsWithoutImageWidth() throws SoneException, MalformedURLException { - InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-image-width.xml"); - assertThat(soneParser.parseSone(sone, inputStream), nullValue()); - } - - @Test - public void parsingASoneFailsWithoutImageHeight() throws SoneException, MalformedURLException { - InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-image-height.xml"); - assertThat(soneParser.parseSone(sone, inputStream), nullValue()); - } - - @Test - public void parsingASoneFailsWithInvalidImageWidth() throws SoneException, MalformedURLException { - InputStream inputStream = getClass().getResourceAsStream("sone-parser-with-invalid-image-width.xml"); - assertThat(soneParser.parseSone(sone, inputStream), nullValue()); - } - - @Test - public void parsingASoneFailsWithInvalidImageHeight() throws SoneException, MalformedURLException { - InputStream inputStream = getClass().getResourceAsStream("sone-parser-with-invalid-image-height.xml"); - assertThat(soneParser.parseSone(sone, inputStream), nullValue()); - } - - @Test - public void parsingASoneSucceedsWithImage() throws SoneException, MalformedURLException { - InputStream inputStream = getClass().getResourceAsStream("sone-parser-with-image.xml"); - final Sone sone = soneParser.parseSone(this.sone, inputStream); - assertThat(sone, not(nullValue())); - assertThat(sone.getRootAlbum().getAlbums(), hasSize(1)); - assertThat(sone.getRootAlbum().getAlbums().get(0).getImages(), hasSize(1)); - Image image = sone.getRootAlbum().getAlbums().get(0).getImages().get(0); - assertThat(image.getId(), is("image-id")); - assertThat(image.getCreationTime(), is(1407197508000L)); - assertThat(image.getKey(), is("KSK@GPLv3.txt")); - assertThat(image.getTitle(), is("image-title")); - assertThat(image.getDescription(), is("image-description")); - assertThat(image.getWidth(), is(1920)); - assertThat(image.getHeight(), is(1080)); - assertThat(sone.getProfile().getAvatar(), is("image-id")); - } - - -} diff --git a/src/test/java/net/pterodactylus/sone/core/UpdateCheckerTest.java b/src/test/java/net/pterodactylus/sone/core/UpdateCheckerTest.java deleted file mode 100644 index e7f1afe..0000000 --- a/src/test/java/net/pterodactylus/sone/core/UpdateCheckerTest.java +++ /dev/null @@ -1,260 +0,0 @@ -package net.pterodactylus.sone.core; - -import static java.lang.Long.MAX_VALUE; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.instanceOf; -import static org.hamcrest.Matchers.is; -import static org.mockito.ArgumentCaptor.forClass; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; -import static org.mockito.hamcrest.MockitoHamcrest.argThat; - -import java.io.IOException; -import java.io.InputStream; - -import net.pterodactylus.sone.core.FreenetInterface.Callback; -import net.pterodactylus.sone.core.event.UpdateFoundEvent; -import net.pterodactylus.sone.main.PluginHomepage; -import net.pterodactylus.util.version.Version; - -import freenet.client.ClientMetadata; -import freenet.client.FetchResult; -import freenet.keys.FreenetURI; -import freenet.support.api.Bucket; -import freenet.support.io.ArrayBucket; - -import com.google.common.eventbus.EventBus; -import org.junit.Before; -import org.junit.Test; -import org.mockito.ArgumentCaptor; -import org.mockito.invocation.InvocationOnMock; -import org.mockito.stubbing.Answer; - -/** - * Unit test for {@link UpdateChecker}. - */ -public class UpdateCheckerTest { - - private final EventBus eventBus = mock(EventBus.class); - private final FreenetInterface freenetInterface = mock(FreenetInterface.class); - private final Version currentVersion = new Version(1, 0, 0); - private final PluginHomepage pluginHomepage = new PluginHomepage("KSK@homepage"); - private final UpdateChecker updateChecker = new UpdateChecker(eventBus, freenetInterface, currentVersion, pluginHomepage); - - @Before - public void startUpdateChecker() { - updateChecker.start(); - } - - @Test - public void newUpdateCheckerDoesNotHaveALatestVersion() { - assertThat(updateChecker.hasLatestVersion(), is(false)); - assertThat(updateChecker.getLatestVersion(), is(currentVersion)); - } - - @Test - public void startingAnUpdateCheckerRegisterAUsk() { - verify(freenetInterface).registerUsk(any(FreenetURI.class), any(Callback.class)); - } - - @Test - public void stoppingAnUpdateCheckerUnregistersAUsk() { - updateChecker.stop(); - verify(freenetInterface).unregisterUsk(any(FreenetURI.class)); - } - - @Test - public void callbackDoesNotDownloadIfNewEditionIsNotFound() { - setupCallbackWithEdition(MAX_VALUE, false, false); - verify(freenetInterface, never()).fetchUri(any(FreenetURI.class)); - verify(eventBus, never()).post(argThat(instanceOf(UpdateFoundEvent.class))); - } - - private void setupCallbackWithEdition(long edition, boolean newKnownGood, boolean newSlot) { - ArgumentCaptor uri = forClass(FreenetURI.class); - ArgumentCaptor callback = forClass(Callback.class); - verify(freenetInterface).registerUsk(uri.capture(), callback.capture()); - callback.getValue().editionFound(uri.getValue(), edition, newKnownGood, newSlot); - } - - @Test - public void callbackStartsIfNewEditionIsFound() { - setupFetchResult(createFutureFetchResult()); - setupCallbackWithEdition(MAX_VALUE, true, false); - verifyAFreenetUriIsFetched(); - verifyEventIsFired(new Version(99, 0, 0), 11865368297000L, false); - verifyThatUpdateCheckerKnowsLatestVersion(new Version(99, 0, 0), 11865368297000L); - } - - private FetchResult createFutureFetchResult() { - ClientMetadata clientMetadata = new ClientMetadata("application/xml"); - Bucket fetched = new ArrayBucket(("# MapConfigurationBackendVersion=1\n" + - "CurrentVersion/Version: 99.0.0\n" + - "CurrentVersion/ReleaseTime: 11865368297000\n" + - "DisruptiveVersion/0.1.2: true").getBytes()); - return new FetchResult(clientMetadata, fetched); - } - - private void verifyEventIsFired(Version version, long releaseTime, boolean disruptive) { - ArgumentCaptor updateFoundEvent = forClass(UpdateFoundEvent.class); - verify(eventBus, times(1)).post(updateFoundEvent.capture()); - assertThat(updateFoundEvent.getValue().version(), is(version)); - assertThat(updateFoundEvent.getValue().releaseTime(), is(releaseTime)); - assertThat(updateFoundEvent.getValue().disruptive(), is(disruptive)); - } - - private void verifyThatUpdateCheckerKnowsLatestVersion(Version version, long releaseTime) { - assertThat(updateChecker.getLatestVersion(), is(version)); - assertThat(updateChecker.getLatestVersionDate(), is(releaseTime)); - assertThat(updateChecker.hasLatestVersion(), is(true)); - } - - @Test - public void callbackDoesNotStartIfNoNewEditionIsFound() { - setupFetchResult(createPastFetchResult()); - setupCallbackWithEdition(updateChecker.getLatestEdition(), true, false); - verifyAFreenetUriIsFetched(); - verifyNoUpdateFoundEventIsFired(); - } - - private void setupFetchResult(final FetchResult pastFetchResult) { - when(freenetInterface.fetchUri(any(FreenetURI.class))).thenAnswer(new Answer() { - @Override - public Fetched answer(InvocationOnMock invocation) throws Throwable { - FreenetURI freenetUri = (FreenetURI) invocation.getArguments()[0]; - return new Fetched(freenetUri, pastFetchResult); - } - }); - } - - private FetchResult createPastFetchResult() { - ClientMetadata clientMetadata = new ClientMetadata("application/xml"); - Bucket fetched = new ArrayBucket(("# MapConfigurationBackendVersion=1\n" + - "CurrentVersion/Version: 0.2\n" + - "CurrentVersion/ReleaseTime: 1289417883000").getBytes()); - return new FetchResult(clientMetadata, fetched); - } - - @Test - public void invalidUpdateFileDoesNotStartCallback() { - setupFetchResult(createInvalidFetchResult()); - setupCallbackWithEdition(MAX_VALUE, true, false); - verifyAFreenetUriIsFetched(); - verifyNoUpdateFoundEventIsFired(); - } - - private FetchResult createInvalidFetchResult() { - ClientMetadata clientMetadata = new ClientMetadata("text/plain"); - Bucket fetched = new ArrayBucket("Some other data.".getBytes()); - return new FetchResult(clientMetadata, fetched); - } - - @Test - public void nonExistingPropertiesWillNotCauseUpdateToBeFound() { - setupCallbackWithEdition(MAX_VALUE, true, false); - verifyAFreenetUriIsFetched(); - verifyNoUpdateFoundEventIsFired(); - } - - private void verifyNoUpdateFoundEventIsFired() { - verify(eventBus, never()).post(any(UpdateFoundEvent.class)); - } - - private void verifyAFreenetUriIsFetched() { - verify(freenetInterface).fetchUri(any(FreenetURI.class)); - } - - @Test - public void brokenBucketDoesNotCauseUpdateToBeFound() { - setupFetchResult(createBrokenBucketFetchResult()); - setupCallbackWithEdition(MAX_VALUE, true, false); - verifyAFreenetUriIsFetched(); - verifyNoUpdateFoundEventIsFired(); - } - - private FetchResult createBrokenBucketFetchResult() { - ClientMetadata clientMetadata = new ClientMetadata("text/plain"); - Bucket fetched = new ArrayBucket("Some other data.".getBytes()) { - @Override - public InputStream getInputStream() { - try { - return when(mock(InputStream.class).read()).thenThrow(IOException.class).getMock(); - } catch (IOException ioe1) { - /* won’t throw here. */ - return null; - } - } - }; - return new FetchResult(clientMetadata, fetched); - } - - @Test - public void invalidTimeDoesNotCauseAnUpdateToBeFound() { - setupFetchResult(createInvalidTimeFetchResult()); - setupCallbackWithEdition(MAX_VALUE, true, false); - verifyAFreenetUriIsFetched(); - verifyNoUpdateFoundEventIsFired(); - } - - private FetchResult createInvalidTimeFetchResult() { - ClientMetadata clientMetadata = new ClientMetadata("application/xml"); - Bucket fetched = new ArrayBucket(("# MapConfigurationBackendVersion=1\n" + - "CurrentVersion/Version: 0.2\n" + - "CurrentVersion/ReleaseTime: invalid").getBytes()); - return new FetchResult(clientMetadata, fetched); - } - - @Test - public void invalidPropertiesDoesNotCauseAnUpdateToBeFound() { - setupFetchResult(createMissingTimeFetchResult()); - setupCallbackWithEdition(MAX_VALUE, true, false); - verifyAFreenetUriIsFetched(); - verifyNoUpdateFoundEventIsFired(); - } - - private FetchResult createMissingTimeFetchResult() { - ClientMetadata clientMetadata = new ClientMetadata("application/xml"); - Bucket fetched = new ArrayBucket(("# MapConfigurationBackendVersion=1\n" + - "CurrentVersion/Version: 0.2\n").getBytes()); - return new FetchResult(clientMetadata, fetched); - } - - @Test - public void invalidVersionDoesNotCauseAnUpdateToBeFound() { - setupFetchResult(createInvalidVersionFetchResult()); - setupCallbackWithEdition(MAX_VALUE, true, false); - verifyAFreenetUriIsFetched(); - verifyNoUpdateFoundEventIsFired(); - } - - private FetchResult createInvalidVersionFetchResult() { - ClientMetadata clientMetadata = new ClientMetadata("application/xml"); - Bucket fetched = new ArrayBucket(("# MapConfigurationBackendVersion=1\n" + - "CurrentVersion/Version: foo\n" + - "CurrentVersion/ReleaseTime: 1289417883000").getBytes()); - return new FetchResult(clientMetadata, fetched); - } - - @Test - public void disruptiveVersionGetsNotification() { - setupFetchResult(createDisruptiveVersionFetchResult()); - setupCallbackWithEdition(MAX_VALUE, true, false); - verifyAFreenetUriIsFetched(); - verifyEventIsFired(new Version(1, 2, 3), 1289417883000L, true); - verifyThatUpdateCheckerKnowsLatestVersion(new Version(1, 2, 3), 1289417883000L); - } - - private FetchResult createDisruptiveVersionFetchResult() { - ClientMetadata clientMetadata = new ClientMetadata("application/xml"); - Bucket fetched = new ArrayBucket(("# MapConfigurationBackendVersion=1\n" + - "CurrentVersion/Version: 1.2.3\n" + - "CurrentVersion/ReleaseTime: 1289417883000\n" + - "DisruptiveVersion/1.2.3: true").getBytes()); - return new FetchResult(clientMetadata, fetched); - } - -} diff --git a/src/test/java/net/pterodactylus/sone/core/WebOfTrustUpdaterTest.java b/src/test/java/net/pterodactylus/sone/core/WebOfTrustUpdaterTest.java index 664e4a1..d7d9b48 100644 --- a/src/test/java/net/pterodactylus/sone/core/WebOfTrustUpdaterTest.java +++ b/src/test/java/net/pterodactylus/sone/core/WebOfTrustUpdaterTest.java @@ -7,27 +7,19 @@ import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; import java.util.concurrent.CountDownLatch; import net.pterodactylus.sone.core.WebOfTrustUpdaterImpl.AddContextJob; import net.pterodactylus.sone.core.WebOfTrustUpdaterImpl.RemoveContextJob; import net.pterodactylus.sone.core.WebOfTrustUpdaterImpl.SetPropertyJob; -import net.pterodactylus.sone.core.WebOfTrustUpdaterImpl.SetTrustJob; import net.pterodactylus.sone.core.WebOfTrustUpdaterImpl.WebOfTrustContextUpdateJob; import net.pterodactylus.sone.core.WebOfTrustUpdaterImpl.WebOfTrustUpdateJob; import net.pterodactylus.sone.freenet.plugin.PluginException; import net.pterodactylus.sone.freenet.wot.Identity; import net.pterodactylus.sone.freenet.wot.OwnIdentity; -import net.pterodactylus.sone.freenet.wot.Trust; import net.pterodactylus.sone.freenet.wot.WebOfTrustConnector; -import net.pterodactylus.sone.freenet.wot.WebOfTrustException; import org.junit.Test; import org.mockito.invocation.InvocationOnMock; @@ -39,9 +31,6 @@ import org.mockito.stubbing.Answer; public class WebOfTrustUpdaterTest { private static final String CONTEXT = "test-context"; - private static final Integer SCORE = 50; - private static final Integer OTHER_SCORE = 25; - private static final String TRUST_COMMENT = "set in a test"; private static final String PROPERTY_NAME = "test-property"; private final WebOfTrustConnector webOfTrustConnector = mock(WebOfTrustConnector.class); private final WebOfTrustUpdaterImpl webOfTrustUpdater = new WebOfTrustUpdaterImpl(webOfTrustConnector); @@ -51,7 +40,6 @@ public class WebOfTrustUpdaterTest { private final WebOfTrustContextUpdateJob contextUpdateJob = webOfTrustUpdater.new WebOfTrustContextUpdateJob(ownIdentity, CONTEXT); private final AddContextJob addContextJob = webOfTrustUpdater.new AddContextJob(ownIdentity, CONTEXT); private final RemoveContextJob removeContextJob = webOfTrustUpdater.new RemoveContextJob(ownIdentity, CONTEXT); - private final Identity trustee = when(mock(Identity.class).getId()).thenReturn("trustee-id").getMock(); private WebOfTrustUpdateJob createWebOfTrustUpdateJob(final boolean success) { return webOfTrustUpdater.new WebOfTrustUpdateJob() { @@ -235,81 +223,6 @@ public class WebOfTrustUpdaterTest { } @Test - public void setTrustJobSetsTrust() throws PluginException { - SetTrustJob setTrustJob = webOfTrustUpdater.new SetTrustJob(ownIdentity, trustee, SCORE, TRUST_COMMENT); - setTrustJob.run(); - verify(webOfTrustConnector).setTrust(eq(ownIdentity), eq(trustee), eq(SCORE), eq(TRUST_COMMENT)); - verify(trustee).setTrust(eq(ownIdentity), eq(new Trust(SCORE, null, 0))); - assertThat(setTrustJob.waitForCompletion(), is(true)); - } - - @Test - public void settingNullTrustRemovesTrust() throws WebOfTrustException { - SetTrustJob setTrustJob = webOfTrustUpdater.new SetTrustJob(ownIdentity, trustee, null, TRUST_COMMENT); - setTrustJob.run(); - verify(webOfTrustConnector).removeTrust(eq(ownIdentity), eq(trustee)); - verify(trustee).removeTrust(eq(ownIdentity)); - assertThat(setTrustJob.waitForCompletion(), is(true)); - } - - @Test - public void exceptionWhileSettingTrustIsCaught() throws PluginException { - doThrow(PluginException.class).when(webOfTrustConnector).setTrust(eq(ownIdentity), eq(trustee), eq(SCORE), eq(TRUST_COMMENT)); - SetTrustJob setTrustJob = webOfTrustUpdater.new SetTrustJob(ownIdentity, trustee, SCORE, TRUST_COMMENT); - setTrustJob.run(); - verify(webOfTrustConnector).setTrust(eq(ownIdentity), eq(trustee), eq(SCORE), eq(TRUST_COMMENT)); - verify(trustee, never()).setTrust(eq(ownIdentity), eq(new Trust(SCORE, null, 0))); - assertThat(setTrustJob.waitForCompletion(), is(false)); - } - - @Test - public void setTrustJobsWithDifferentClassesAreNotEqual() { - SetTrustJob firstSetTrustJob = webOfTrustUpdater.new SetTrustJob(ownIdentity, trustee, SCORE, TRUST_COMMENT); - SetTrustJob secondSetTrustJob = webOfTrustUpdater.new SetTrustJob(ownIdentity, trustee, SCORE, TRUST_COMMENT) { - }; - assertThat(firstSetTrustJob, not(is(secondSetTrustJob))); - assertThat(secondSetTrustJob, not(is(firstSetTrustJob))); - } - - @Test - public void setTrustJobsWithDifferentTrustersAreNotEqual() { - SetTrustJob firstSetTrustJob = webOfTrustUpdater.new SetTrustJob(ownIdentity, trustee, SCORE, TRUST_COMMENT); - SetTrustJob secondSetTrustJob = webOfTrustUpdater.new SetTrustJob(mock(OwnIdentity.class), trustee, SCORE, TRUST_COMMENT); - assertThat(firstSetTrustJob, not(is(secondSetTrustJob))); - assertThat(secondSetTrustJob, not(is(firstSetTrustJob))); - } - - @Test - public void setTrustJobsWithDifferentTrusteesAreNotEqual() { - SetTrustJob firstSetTrustJob = webOfTrustUpdater.new SetTrustJob(ownIdentity, trustee, SCORE, TRUST_COMMENT); - SetTrustJob secondSetTrustJob = webOfTrustUpdater.new SetTrustJob(ownIdentity, mock(Identity.class), SCORE, TRUST_COMMENT); - assertThat(firstSetTrustJob, not(is(secondSetTrustJob))); - assertThat(secondSetTrustJob, not(is(firstSetTrustJob))); - } - - @Test - public void setTrustJobsWithDifferentScoreAreEqual() { - SetTrustJob firstSetTrustJob = webOfTrustUpdater.new SetTrustJob(ownIdentity, trustee, SCORE, TRUST_COMMENT); - SetTrustJob secondSetTrustJob = webOfTrustUpdater.new SetTrustJob(ownIdentity, trustee, OTHER_SCORE, TRUST_COMMENT); - assertThat(firstSetTrustJob, is(secondSetTrustJob)); - assertThat(secondSetTrustJob, is(firstSetTrustJob)); - assertThat(firstSetTrustJob.hashCode(), is(secondSetTrustJob.hashCode())); - } - - @Test - public void setTrustJobDoesNotEqualNull() { - SetTrustJob setTrustJob = webOfTrustUpdater.new SetTrustJob(ownIdentity, trustee, SCORE, TRUST_COMMENT); - assertThat(setTrustJob, not(is((Object) null))); - } - - @Test - public void toStringOfSetTrustJobContainsIdsOfTrusterAndTrustee() { - SetTrustJob setTrustJob = webOfTrustUpdater.new SetTrustJob(ownIdentity, trustee, SCORE, TRUST_COMMENT); - assertThat(setTrustJob.toString(), containsString(ownIdentity.getId())); - assertThat(setTrustJob.toString(), containsString(trustee.getId())); - } - - @Test public void webOfTrustUpdaterStopsWhenItShould() { webOfTrustUpdater.stop(); webOfTrustUpdater.serviceRun(); @@ -404,48 +317,4 @@ public class WebOfTrustUpdaterTest { verify(ownIdentity).removeContext(eq(CONTEXT)); } - @Test - public void setTrustSetsTrust() throws InterruptedException, PluginException { - final CountDownLatch trustSetTrigger = new CountDownLatch(1); - doAnswer(new Answer() { - @Override - public Void answer(InvocationOnMock invocation) throws Throwable { - trustSetTrigger.countDown(); - return null; - } - }).when(trustee).setTrust(eq(ownIdentity), eq(new Trust(SCORE, null, 0))); - webOfTrustUpdater.start(); - webOfTrustUpdater.setTrust(ownIdentity, trustee, SCORE, TRUST_COMMENT); - assertThat(trustSetTrigger.await(1, SECONDS), is(true)); - verify(trustee).setTrust(eq(ownIdentity), eq(new Trust(SCORE, null, 0))); - verify(webOfTrustConnector).setTrust(eq(ownIdentity), eq(trustee), eq(SCORE), eq(TRUST_COMMENT)); - } - - @Test - public void setTrustRequestsAreCoalesced() throws InterruptedException, PluginException { - final CountDownLatch trustSetTrigger = new CountDownLatch(1); - doAnswer(new Answer() { - @Override - public Void answer(InvocationOnMock invocation) throws Throwable { - trustSetTrigger.countDown(); - return null; - } - }).when(trustee).setTrust(eq(ownIdentity), eq(new Trust(SCORE, null, 0))); - for (int i = 1; i <= 2; i++) { - /* this is so fucking volatile. */ - if (i > 1) { - sleep(200); - } - new Thread(new Runnable() { - public void run() { - webOfTrustUpdater.setTrust(ownIdentity, trustee, SCORE, TRUST_COMMENT); - } - }).start(); - } - webOfTrustUpdater.start(); - assertThat(trustSetTrigger.await(1, SECONDS), is(true)); - verify(trustee).setTrust(eq(ownIdentity), eq(new Trust(SCORE, null, 0))); - verify(webOfTrustConnector).setTrust(eq(ownIdentity), eq(trustee), eq(SCORE), eq(TRUST_COMMENT)); - } - } diff --git a/src/test/java/net/pterodactylus/sone/freenet/KeyTest.java b/src/test/java/net/pterodactylus/sone/freenet/KeyTest.java deleted file mode 100644 index 80e1e45..0000000 --- a/src/test/java/net/pterodactylus/sone/freenet/KeyTest.java +++ /dev/null @@ -1,67 +0,0 @@ -package net.pterodactylus.sone.freenet; - -import static freenet.support.Base64.encode; -import static net.pterodactylus.sone.freenet.Key.from; -import static net.pterodactylus.sone.freenet.Key.routingKey; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.is; - -import java.net.MalformedURLException; - -import freenet.keys.FreenetURI; - -import org.junit.Test; - -/** - * Unit test for {@link Key}. - */ -public class KeyTest { - - private final FreenetURI uri; - private final Key key; - - public KeyTest() throws MalformedURLException { - uri = new FreenetURI( - "SSK@NfUYvxDwU9vqb2mh-qdT~DYJ6U0XNbxMGGoLe0aCHJs,Miglsgix0VR56ZiPl4NgjnUd~UdrnHqIvXJ3KKHmxmI,AQACAAE/some-site-12/foo/bar.html"); - key = from(uri); - } - - @Test - public void keyCanBeCreatedFromFreenetUri() throws MalformedURLException { - assertThat(key.getRoutingKey(), - is("NfUYvxDwU9vqb2mh-qdT~DYJ6U0XNbxMGGoLe0aCHJs")); - assertThat(key.getCryptoKey(), - is("Miglsgix0VR56ZiPl4NgjnUd~UdrnHqIvXJ3KKHmxmI")); - assertThat(key.getExtra(), is("AQACAAE")); - } - - @Test - public void keyCanBeConvertedToUsk() throws MalformedURLException { - FreenetURI uskUri = key.toUsk("other-site", 15, "some", "path.html"); - assertThat(uskUri.toString(), - is("USK@NfUYvxDwU9vqb2mh-qdT~DYJ6U0XNbxMGGoLe0aCHJs,Miglsgix0VR56ZiPl4NgjnUd~UdrnHqIvXJ3KKHmxmI,AQACAAE/other-site/15/some/path.html")); - } - - @Test - public void keyCanBeConvertedToSskWithoutEdition() - throws MalformedURLException { - FreenetURI uskUri = key.toSsk("other-site", "some", "path.html"); - assertThat(uskUri.toString(), - is("SSK@NfUYvxDwU9vqb2mh-qdT~DYJ6U0XNbxMGGoLe0aCHJs,Miglsgix0VR56ZiPl4NgjnUd~UdrnHqIvXJ3KKHmxmI,AQACAAE/other-site/some/path.html")); - } - - @Test - public void keyCanBeConvertedToSskWithEdition() - throws MalformedURLException { - FreenetURI uskUri = key.toSsk("other-site", 15, "some", "path.html"); - assertThat(uskUri.toString(), - is("SSK@NfUYvxDwU9vqb2mh-qdT~DYJ6U0XNbxMGGoLe0aCHJs,Miglsgix0VR56ZiPl4NgjnUd~UdrnHqIvXJ3KKHmxmI,AQACAAE/other-site-15/some/path.html")); - } - - @Test - public void routingKeyIsExtractCorrectly() { - assertThat(routingKey(uri), - is("NfUYvxDwU9vqb2mh-qdT~DYJ6U0XNbxMGGoLe0aCHJs")); - } - -} diff --git a/src/test/java/net/pterodactylus/sone/freenet/wot/DefaultIdentityTest.java b/src/test/java/net/pterodactylus/sone/freenet/wot/DefaultIdentityTest.java deleted file mode 100644 index 9d59570..0000000 --- a/src/test/java/net/pterodactylus/sone/freenet/wot/DefaultIdentityTest.java +++ /dev/null @@ -1,150 +0,0 @@ -/* - * Sone - DefaultIdentityTest.java - Copyright © 2013–2019 David 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 . - */ - -package net.pterodactylus.sone.freenet.wot; - -import static com.google.common.collect.ImmutableMap.of; -import static java.util.Arrays.asList; -import static net.pterodactylus.sone.test.Matchers.matchesRegex; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.containsInAnyOrder; -import static org.hamcrest.Matchers.empty; -import static org.hamcrest.Matchers.hasEntry; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.not; -import static org.hamcrest.Matchers.nullValue; -import static org.hamcrest.collection.IsIterableContainingInOrder.contains; -import static org.mockito.Mockito.mock; - -import java.util.Collections; - -import org.junit.Test; - -/** - * Unit test for {@link DefaultIdentity}. - */ -public class DefaultIdentityTest { - - protected final DefaultIdentity identity = createIdentity(); - - protected DefaultIdentity createIdentity() { - return new DefaultIdentity("Id", "Nickname", "RequestURI"); - } - - @Test - public void identityCanBeCreated() { - assertThat(identity.getId(), is("Id")); - assertThat(identity.getNickname(), is("Nickname")); - assertThat(identity.getRequestUri(), is("RequestURI")); - assertThat(identity.getContexts(), empty()); - assertThat(identity.getProperties(), is(Collections.emptyMap())); - } - - @Test - public void contextsAreAddedCorrectly() { - identity.addContext("Test"); - assertThat(identity.getContexts(), contains("Test")); - assertThat(identity.hasContext("Test"), is(true)); - } - - @Test - public void contextsAreRemovedCorrectly() { - identity.addContext("Test"); - identity.removeContext("Test"); - assertThat(identity.getContexts(), empty()); - assertThat(identity.hasContext("Test"), is(false)); - } - - @Test - public void contextsAreSetCorrectlyInBulk() { - identity.addContext("Test"); - identity.setContexts(asList("Test1", "Test2")); - assertThat(identity.getContexts(), containsInAnyOrder("Test1", "Test2")); - assertThat(identity.hasContext("Test"), is(false)); - assertThat(identity.hasContext("Test1"), is(true)); - assertThat(identity.hasContext("Test2"), is(true)); - } - - @Test - public void propertiesAreAddedCorrectly() { - identity.setProperty("Key", "Value"); - assertThat(identity.getProperties().size(), is(1)); - assertThat(identity.getProperties(), hasEntry("Key", "Value")); - assertThat(identity.getProperty("Key"), is("Value")); - } - - @Test - public void propertiesAreRemovedCorrectly() { - identity.setProperty("Key", "Value"); - identity.removeProperty("Key"); - assertThat(identity.getProperties(), is(Collections.emptyMap())); - assertThat(identity.getProperty("Key"), nullValue()); - } - - @Test - public void propertiesAreSetCorrectlyInBulk() { - identity.setProperty("Key", "Value"); - identity.setProperties(of("Key1", "Value1", "Key2", "Value2")); - assertThat(identity.getProperties().size(), is(2)); - assertThat(identity.getProperty("Key"), nullValue()); - assertThat(identity.getProperty("Key1"), is("Value1")); - assertThat(identity.getProperty("Key2"), is("Value2")); - } - - @Test - public void trustRelationshipsAreAddedCorrectly() { - OwnIdentity ownIdentity = mock(OwnIdentity.class); - Trust trust = mock(Trust.class); - identity.setTrust(ownIdentity, trust); - assertThat(identity.getTrust(ownIdentity), is(trust)); - } - - @Test - public void trustRelationshipsAreRemovedCorrectly() { - OwnIdentity ownIdentity = mock(OwnIdentity.class); - Trust trust = mock(Trust.class); - identity.setTrust(ownIdentity, trust); - identity.removeTrust(ownIdentity); - assertThat(identity.getTrust(ownIdentity), nullValue()); - } - - @Test - public void identitiesWithTheSameIdAreEqual() { - DefaultIdentity identity2 = new DefaultIdentity("Id", "Nickname2", "RequestURI2"); - assertThat(identity2, is(identity)); - assertThat(identity, is(identity2)); - } - - @Test - public void twoEqualIdentitiesHaveTheSameHashCode() { - DefaultIdentity identity2 = new DefaultIdentity("Id", "Nickname2", "RequestURI2"); - assertThat(identity.hashCode(), is(identity2.hashCode())); - } - - @Test - public void nullDoesNotMatchAnIdentity() { - assertThat(identity, not(is((Object) null))); - } - - @Test - public void toStringContainsIdAndNickname() { - String identityString = identity.toString(); - assertThat(identityString, matchesRegex(".*\\bId\\b.*")); - assertThat(identityString, matchesRegex(".*\\bNickname\\b.*")); - } - -} diff --git a/src/test/java/net/pterodactylus/sone/freenet/wot/DefaultOwnIdentityTest.java b/src/test/java/net/pterodactylus/sone/freenet/wot/DefaultOwnIdentityTest.java deleted file mode 100644 index 138ced3..0000000 --- a/src/test/java/net/pterodactylus/sone/freenet/wot/DefaultOwnIdentityTest.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Sone - DefaultOwnIdentityTest.java - Copyright © 2013–2019 David 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 . - */ - -package net.pterodactylus.sone.freenet.wot; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.is; - -import org.junit.Test; - -/** - * Unit test for {@link DefaultOwnIdentity}. - */ -public class DefaultOwnIdentityTest extends DefaultIdentityTest { - - @Override - protected DefaultIdentity createIdentity() { - return new DefaultOwnIdentity("Id", "Nickname", "RequestURI", "InsertURI"); - } - - @Test - public void ownIdentityCanBeCreated() { - assertThat(((OwnIdentity) identity).getInsertUri(), is("InsertURI")); - } - -} diff --git a/src/test/java/net/pterodactylus/sone/freenet/wot/Identities.java b/src/test/java/net/pterodactylus/sone/freenet/wot/Identities.java deleted file mode 100644 index 7ed2b59..0000000 --- a/src/test/java/net/pterodactylus/sone/freenet/wot/Identities.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Sone - Identities.java - Copyright © 2013–2019 David 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 . - */ - -package net.pterodactylus.sone.freenet.wot; - -import java.util.Collection; -import java.util.Map; - -/** - * Creates {@link Identity}s and {@link OwnIdentity}s. - */ -public class Identities { - - public static OwnIdentity createOwnIdentity(String id, Collection contexts, Map properties) { - DefaultOwnIdentity ownIdentity = new DefaultOwnIdentity(id, "Nickname" + id, "Request" + id, "Insert" + id); - setContextsAndPropertiesOnIdentity(ownIdentity, contexts, properties); - return ownIdentity; - } - - public static Identity createIdentity(String id, Collection contexts, Map properties) { - DefaultIdentity identity = new DefaultIdentity(id, "Nickname" + id, "Request" + id); - setContextsAndPropertiesOnIdentity(identity, contexts, properties); - return identity; - } - - private static void setContextsAndPropertiesOnIdentity(Identity identity, Collection contexts, Map properties) { - identity.setContexts(contexts); - identity.setProperties(properties); - } - -} diff --git a/src/test/java/net/pterodactylus/sone/freenet/wot/IdentityChangeDetectorTest.java b/src/test/java/net/pterodactylus/sone/freenet/wot/IdentityChangeDetectorTest.java deleted file mode 100644 index e1eb1c8..0000000 --- a/src/test/java/net/pterodactylus/sone/freenet/wot/IdentityChangeDetectorTest.java +++ /dev/null @@ -1,188 +0,0 @@ -/* - * Sone - IdentityChangeDetectorTest.java - Copyright © 2013–2019 David 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 . - */ - -package net.pterodactylus.sone.freenet.wot; - -import static com.google.common.collect.ImmutableMap.of; -import static com.google.common.collect.Lists.newArrayList; -import static java.util.Arrays.asList; -import static net.pterodactylus.sone.freenet.wot.Identities.createIdentity; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.containsInAnyOrder; -import static org.hamcrest.Matchers.empty; - -import java.util.Collection; - -import net.pterodactylus.sone.freenet.wot.IdentityChangeDetector.IdentityProcessor; - -import org.junit.Before; -import org.junit.Test; - -/** - * Unit test for {@link IdentityChangeDetector}. - */ -public class IdentityChangeDetectorTest { - - private final IdentityChangeDetector identityChangeDetector = new IdentityChangeDetector(createOldIdentities()); - private final Collection newIdentities = newArrayList(); - private final Collection removedIdentities = newArrayList(); - private final Collection changedIdentities = newArrayList(); - private final Collection unchangedIdentities = newArrayList(); - - @Before - public void setup() { - identityChangeDetector.onNewIdentity(new IdentityProcessor() { - @Override - public void processIdentity(Identity identity) { - newIdentities.add(identity); - } - }); - identityChangeDetector.onRemovedIdentity(new IdentityProcessor() { - @Override - public void processIdentity(Identity identity) { - removedIdentities.add(identity); - } - }); - identityChangeDetector.onChangedIdentity(new IdentityProcessor() { - @Override - public void processIdentity(Identity identity) { - changedIdentities.add(identity); - } - }); - identityChangeDetector.onUnchangedIdentity(new IdentityProcessor() { - @Override - public void processIdentity(Identity identity) { - unchangedIdentities.add(identity); - } - }); - } - - @Test - public void noDifferencesAreDetectedWhenSendingTheOldIdentitiesAgain() { - identityChangeDetector.detectChanges(createOldIdentities()); - assertThat(newIdentities, empty()); - assertThat(removedIdentities, empty()); - assertThat(changedIdentities, empty()); - assertThat(unchangedIdentities, containsInAnyOrder(createIdentity1(), createIdentity2(), createIdentity3())); - } - - @Test - public void detectThatAnIdentityWasRemoved() { - identityChangeDetector.detectChanges(asList(createIdentity1(), createIdentity3())); - assertThat(newIdentities, empty()); - assertThat(removedIdentities, containsInAnyOrder(createIdentity2())); - assertThat(changedIdentities, empty()); - assertThat(unchangedIdentities, containsInAnyOrder(createIdentity1(), createIdentity3())); - } - - @Test - public void detectThatAnIdentityWasAdded() { - identityChangeDetector.detectChanges(asList(createIdentity1(), createIdentity2(), createIdentity3(), createIdentity4())); - assertThat(newIdentities, containsInAnyOrder(createIdentity4())); - assertThat(removedIdentities, empty()); - assertThat(changedIdentities, empty()); - assertThat(unchangedIdentities, containsInAnyOrder(createIdentity1(), createIdentity2(), createIdentity3())); - } - - @Test - public void detectThatAContextWasRemoved() { - Identity identity2 = createIdentity2(); - identity2.removeContext("Context C"); - identityChangeDetector.detectChanges(asList(createIdentity1(), identity2, createIdentity3())); - assertThat(newIdentities, empty()); - assertThat(removedIdentities, empty()); - assertThat(changedIdentities, containsInAnyOrder(identity2)); - assertThat(unchangedIdentities, containsInAnyOrder(createIdentity1(), createIdentity3())); - } - - @Test - public void detectThatAContextWasAdded() { - Identity identity2 = createIdentity2(); - identity2.addContext("Context C1"); - identityChangeDetector.detectChanges(asList(createIdentity1(), identity2, createIdentity3())); - assertThat(newIdentities, empty()); - assertThat(removedIdentities, empty()); - assertThat(changedIdentities, containsInAnyOrder(identity2)); - assertThat(unchangedIdentities, containsInAnyOrder(createIdentity1(), createIdentity3())); - } - - @Test - public void detectThatAPropertyWasRemoved() { - Identity identity1 = createIdentity1(); - identity1.removeProperty("Key A"); - identityChangeDetector.detectChanges(asList(identity1, createIdentity2(), createIdentity3())); - assertThat(newIdentities, empty()); - assertThat(removedIdentities, empty()); - assertThat(changedIdentities, containsInAnyOrder(identity1)); - assertThat(unchangedIdentities, containsInAnyOrder(createIdentity2(), createIdentity3())); - } - - @Test - public void detectThatAPropertyWasAdded() { - Identity identity3 = createIdentity3(); - identity3.setProperty("Key A", "Value A"); - identityChangeDetector.detectChanges(asList(createIdentity1(), createIdentity2(), identity3)); - assertThat(newIdentities, empty()); - assertThat(removedIdentities, empty()); - assertThat(changedIdentities, containsInAnyOrder(identity3)); - assertThat(unchangedIdentities, containsInAnyOrder(createIdentity1(), createIdentity2())); - } - - @Test - public void detectThatAPropertyWasChanged() { - Identity identity3 = createIdentity3(); - identity3.setProperty("Key E", "Value F"); - identityChangeDetector.detectChanges(asList(createIdentity1(), createIdentity2(), identity3)); - assertThat(newIdentities, empty()); - assertThat(removedIdentities, empty()); - assertThat(changedIdentities, containsInAnyOrder(identity3)); - assertThat(unchangedIdentities, containsInAnyOrder(createIdentity1(), createIdentity2())); - } - - @Test - public void noRemovedIdentitiesAreDetectedWithoutAnIdentityProcessor() { - identityChangeDetector.onRemovedIdentity(null); - identityChangeDetector.detectChanges(asList(createIdentity1(), createIdentity3())); - } - - @Test - public void noAddedIdentitiesAreDetectedWithoutAnIdentityProcessor() { - identityChangeDetector.onNewIdentity(null); - identityChangeDetector.detectChanges(asList(createIdentity1(), createIdentity2(), createIdentity3(), createIdentity4())); - } - - private static Collection createOldIdentities() { - return asList(createIdentity1(), createIdentity2(), createIdentity3()); - } - - private static Identity createIdentity1() { - return createIdentity("Test1", asList("Context A", "Context B"), of("Key A", "Value A", "Key B", "Value B")); - } - - private static Identity createIdentity2() { - return createIdentity("Test2", asList("Context C", "Context D"), of("Key C", "Value C", "Key D", "Value D")); - } - - private static Identity createIdentity3() { - return createIdentity("Test3", asList("Context E", "Context F"), of("Key E", "Value E", "Key F", "Value F")); - } - - private static Identity createIdentity4() { - return createIdentity("Test4", asList("Context G", "Context H"), of("Key G", "Value G", "Key H", "Value H")); - } - -} diff --git a/src/test/java/net/pterodactylus/sone/freenet/wot/IdentityChangeEventSenderTest.java b/src/test/java/net/pterodactylus/sone/freenet/wot/IdentityChangeEventSenderTest.java deleted file mode 100644 index ff442a4..0000000 --- a/src/test/java/net/pterodactylus/sone/freenet/wot/IdentityChangeEventSenderTest.java +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Sone - IdentityChangeEventSenderTest.java - Copyright © 2013–2019 David 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 . - */ - -package net.pterodactylus.sone.freenet.wot; - -import static com.google.common.collect.ImmutableMap.of; -import static java.util.Arrays.asList; -import static net.pterodactylus.sone.freenet.wot.Identities.createIdentity; -import static net.pterodactylus.sone.freenet.wot.Identities.createOwnIdentity; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; - -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import net.pterodactylus.sone.freenet.wot.event.IdentityAddedEvent; -import net.pterodactylus.sone.freenet.wot.event.IdentityRemovedEvent; -import net.pterodactylus.sone.freenet.wot.event.IdentityUpdatedEvent; -import net.pterodactylus.sone.freenet.wot.event.OwnIdentityAddedEvent; -import net.pterodactylus.sone.freenet.wot.event.OwnIdentityRemovedEvent; - -import com.google.common.eventbus.EventBus; -import org.junit.Test; - -/** - * Unit test for {@link IdentityChangeEventSender}. - */ -public class IdentityChangeEventSenderTest { - - private final EventBus eventBus = mock(EventBus.class); - private final List ownIdentities = asList( - createOwnIdentity("O1", asList("Test"), of("KeyA", "ValueA")), - createOwnIdentity("O2", asList("Test2"), of("KeyB", "ValueB")), - createOwnIdentity("O3", asList("Test3"), of("KeyC", "ValueC")) - ); - private final List identities = asList( - createIdentity("I1", Collections.emptyList(), Collections.emptyMap()), - createIdentity("I2", Collections.emptyList(), Collections.emptyMap()), - createIdentity("I3", Collections.emptyList(), Collections.emptyMap()), - createIdentity("I2", asList("Test"), Collections.emptyMap()) - ); - private final IdentityChangeEventSender identityChangeEventSender = new IdentityChangeEventSender(eventBus, createOldIdentities()); - - @Test - public void addingAnOwnIdentityIsDetectedAndReportedCorrectly() { - Map> newIdentities = createNewIdentities(); - identityChangeEventSender.detectChanges(newIdentities); - verify(eventBus).post(eq(new OwnIdentityRemovedEvent(ownIdentities.get(0)))); - verify(eventBus).post(eq(new IdentityRemovedEvent(ownIdentities.get(0), identities.get(0)))); - verify(eventBus).post(eq(new IdentityRemovedEvent(ownIdentities.get(0), identities.get(1)))); - verify(eventBus).post(eq(new OwnIdentityAddedEvent(ownIdentities.get(2)))); - verify(eventBus).post(eq(new IdentityAddedEvent(ownIdentities.get(2), identities.get(1)))); - verify(eventBus).post(eq(new IdentityAddedEvent(ownIdentities.get(2), identities.get(2)))); - verify(eventBus).post(eq(new IdentityRemovedEvent(ownIdentities.get(1), identities.get(0)))); - verify(eventBus).post(eq(new IdentityAddedEvent(ownIdentities.get(1), identities.get(2)))); - verify(eventBus).post(eq(new IdentityUpdatedEvent(ownIdentities.get(1), identities.get(1)))); - } - - private Map> createNewIdentities() { - Map> oldIdentities = new HashMap<>(); - oldIdentities.put(ownIdentities.get(1), asList(identities.get(3), identities.get(2))); - oldIdentities.put(ownIdentities.get(2), asList(identities.get(1), identities.get(2))); - return oldIdentities; - } - - private Map> createOldIdentities() { - Map> oldIdentities = new HashMap<>(); - oldIdentities.put(ownIdentities.get(0), asList(identities.get(0), identities.get(1))); - oldIdentities.put(ownIdentities.get(1), asList(identities.get(0), identities.get(1))); - return oldIdentities; - } - -} diff --git a/src/test/java/net/pterodactylus/sone/freenet/wot/IdentityLoaderTest.java b/src/test/java/net/pterodactylus/sone/freenet/wot/IdentityLoaderTest.java deleted file mode 100644 index 8744c15..0000000 --- a/src/test/java/net/pterodactylus/sone/freenet/wot/IdentityLoaderTest.java +++ /dev/null @@ -1,154 +0,0 @@ -/* - * Sone - IdentityLoaderTest.java - Copyright © 2013–2019 David 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 . - */ - -package net.pterodactylus.sone.freenet.wot; - -import static com.google.common.base.Optional.of; -import static com.google.common.collect.Lists.newArrayList; -import static com.google.common.collect.Sets.newHashSet; -import static java.util.Arrays.asList; -import static java.util.Collections.emptySet; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.containsInAnyOrder; -import static org.hamcrest.Matchers.hasSize; -import static org.hamcrest.Matchers.is; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Set; - -import com.google.common.base.Optional; -import com.google.common.collect.ImmutableMap; -import org.hamcrest.Matchers; -import org.junit.Before; -import org.junit.Test; - -/** - * Unit test for {@link IdentityLoader}. - */ -public class IdentityLoaderTest { - - private final WebOfTrustConnector webOfTrustConnector = mock(WebOfTrustConnector.class); - private final IdentityLoader identityLoader = new IdentityLoader(webOfTrustConnector, of(new Context("Test"))); - private final IdentityLoader identityLoaderWithoutContext = new IdentityLoader(webOfTrustConnector); - - @Before - public void setup() throws WebOfTrustException { - List ownIdentities = createOwnIdentities(); - when(webOfTrustConnector.loadAllOwnIdentities()).thenReturn(newHashSet(ownIdentities)); - when(webOfTrustConnector.loadTrustedIdentities(eq(ownIdentities.get(0)), any(Optional.class))).thenReturn(createTrustedIdentitiesForFirstOwnIdentity()); - when(webOfTrustConnector.loadTrustedIdentities(eq(ownIdentities.get(1)), any(Optional.class))).thenReturn(createTrustedIdentitiesForSecondOwnIdentity()); - when(webOfTrustConnector.loadTrustedIdentities(eq(ownIdentities.get(2)), any(Optional.class))).thenReturn(createTrustedIdentitiesForThirdOwnIdentity()); - when(webOfTrustConnector.loadTrustedIdentities(eq(ownIdentities.get(3)), any(Optional.class))).thenReturn(createTrustedIdentitiesForFourthOwnIdentity()); - } - - private List createOwnIdentities() { - return newArrayList( - createOwnIdentity("O1", "ON1", "OR1", "OI1", asList("Test", "Test2"), ImmutableMap.of("KeyA", "ValueA", "KeyB", "ValueB")), - createOwnIdentity("O2", "ON2", "OR2", "OI2", asList("Test"), ImmutableMap.of("KeyC", "ValueC")), - createOwnIdentity("O3", "ON3", "OR3", "OI3", asList("Test2"), ImmutableMap.of("KeyE", "ValueE", "KeyD", "ValueD")), - createOwnIdentity("O4", "ON4", "OR$", "OI4", asList("Test"), ImmutableMap.of("KeyA", "ValueA", "KeyD", "ValueD")) - ); - } - - private Set createTrustedIdentitiesForFirstOwnIdentity() { - return newHashSet( - createIdentity("I11", "IN11", "IR11", asList("Test"), ImmutableMap.of("KeyA", "ValueA")) - ); - } - - private Set createTrustedIdentitiesForSecondOwnIdentity() { - return newHashSet( - createIdentity("I21", "IN21", "IR21", asList("Test", "Test2"), ImmutableMap.of("KeyB", "ValueB")) - ); - } - - private Set createTrustedIdentitiesForThirdOwnIdentity() { - return newHashSet( - createIdentity("I31", "IN31", "IR31", asList("Test", "Test3"), ImmutableMap.of("KeyC", "ValueC")) - ); - } - - private Set createTrustedIdentitiesForFourthOwnIdentity() { - return emptySet(); - } - - private OwnIdentity createOwnIdentity(String id, String nickname, String requestUri, String insertUri, List contexts, ImmutableMap properties) { - OwnIdentity ownIdentity = new DefaultOwnIdentity(id, nickname, requestUri, insertUri); - ownIdentity.setContexts(contexts); - ownIdentity.setProperties(properties); - return ownIdentity; - } - - private Identity createIdentity(String id, String nickname, String requestUri, List contexts, ImmutableMap properties) { - Identity identity = new DefaultIdentity(id, nickname, requestUri); - identity.setContexts(contexts); - identity.setProperties(properties); - return identity; - } - - @Test - public void loadingIdentities() throws WebOfTrustException { - List ownIdentities = createOwnIdentities(); - Map> identities = identityLoader.loadIdentities(); - verify(webOfTrustConnector).loadAllOwnIdentities(); - verify(webOfTrustConnector).loadTrustedIdentities(eq(ownIdentities.get(0)), eq(of("Test"))); - verify(webOfTrustConnector).loadTrustedIdentities(eq(ownIdentities.get(1)), eq(of("Test"))); - verify(webOfTrustConnector, never()).loadTrustedIdentities(eq(ownIdentities.get(2)), any(Optional.class)); - verify(webOfTrustConnector).loadTrustedIdentities(eq(ownIdentities.get(3)), eq(of("Test"))); - assertThat(identities.keySet(), hasSize(4)); - assertThat(identities.keySet(), containsInAnyOrder(ownIdentities.get(0), ownIdentities.get(1), ownIdentities.get(2), ownIdentities.get(3))); - verifyIdentitiesForOwnIdentity(identities, ownIdentities.get(0), createTrustedIdentitiesForFirstOwnIdentity()); - verifyIdentitiesForOwnIdentity(identities, ownIdentities.get(1), createTrustedIdentitiesForSecondOwnIdentity()); - verifyIdentitiesForOwnIdentity(identities, ownIdentities.get(2), Collections.emptySet()); - verifyIdentitiesForOwnIdentity(identities, ownIdentities.get(3), createTrustedIdentitiesForFourthOwnIdentity()); - } - - @Test - public void loadingIdentitiesWithoutContext() throws WebOfTrustException { - List ownIdentities = createOwnIdentities(); - Map> identities = identityLoaderWithoutContext.loadIdentities(); - verify(webOfTrustConnector).loadAllOwnIdentities(); - verify(webOfTrustConnector).loadTrustedIdentities(eq(ownIdentities.get(0)), eq(Optional.absent())); - verify(webOfTrustConnector).loadTrustedIdentities(eq(ownIdentities.get(1)), eq(Optional.absent())); - verify(webOfTrustConnector).loadTrustedIdentities(eq(ownIdentities.get(2)), eq(Optional.absent())); - verify(webOfTrustConnector).loadTrustedIdentities(eq(ownIdentities.get(3)), eq(Optional.absent())); - assertThat(identities.keySet(), hasSize(4)); - OwnIdentity firstOwnIdentity = ownIdentities.get(0); - OwnIdentity secondOwnIdentity = ownIdentities.get(1); - OwnIdentity thirdOwnIdentity = ownIdentities.get(2); - OwnIdentity fourthOwnIdentity = ownIdentities.get(3); - assertThat(identities.keySet(), containsInAnyOrder(firstOwnIdentity, secondOwnIdentity, thirdOwnIdentity, fourthOwnIdentity)); - verifyIdentitiesForOwnIdentity(identities, firstOwnIdentity, createTrustedIdentitiesForFirstOwnIdentity()); - verifyIdentitiesForOwnIdentity(identities, secondOwnIdentity, createTrustedIdentitiesForSecondOwnIdentity()); - verifyIdentitiesForOwnIdentity(identities, thirdOwnIdentity, createTrustedIdentitiesForThirdOwnIdentity()); - verifyIdentitiesForOwnIdentity(identities, fourthOwnIdentity, createTrustedIdentitiesForFourthOwnIdentity()); - } - - private void verifyIdentitiesForOwnIdentity(Map> identities, OwnIdentity ownIdentity, Set trustedIdentities) { - assertThat(identities.get(ownIdentity), Matchers.>is(trustedIdentities)); - } - -} diff --git a/src/test/java/net/pterodactylus/sone/freenet/wot/IdentityManagerTest.java b/src/test/java/net/pterodactylus/sone/freenet/wot/IdentityManagerTest.java deleted file mode 100644 index 45e95db..0000000 --- a/src/test/java/net/pterodactylus/sone/freenet/wot/IdentityManagerTest.java +++ /dev/null @@ -1,37 +0,0 @@ -package net.pterodactylus.sone.freenet.wot; - -import static com.google.common.base.Optional.of; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.is; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; - -import net.pterodactylus.sone.freenet.plugin.PluginException; - -import com.google.common.eventbus.EventBus; -import org.junit.Test; - -/** - * Unit test for {@link IdentityManagerImpl}. - */ -public class IdentityManagerTest { - - private final EventBus eventBus = mock(EventBus.class); - private final WebOfTrustConnector webOfTrustConnector = mock(WebOfTrustConnector.class); - private final IdentityManager identityManager = new IdentityManagerImpl(eventBus, webOfTrustConnector, new IdentityLoader(webOfTrustConnector, of(new Context("Test")))); - - @Test - public void identityManagerPingsWotConnector() throws PluginException { - assertThat(identityManager.isConnected(), is(true)); - verify(webOfTrustConnector).ping(); - } - - @Test - public void disconnectedWotConnectorIsRecognized() throws PluginException { - doThrow(PluginException.class).when(webOfTrustConnector).ping(); - assertThat(identityManager.isConnected(), is(false)); - verify(webOfTrustConnector).ping(); - } - -} diff --git a/src/test/java/net/pterodactylus/sone/freenet/wot/IdentityManagerTest.kt b/src/test/java/net/pterodactylus/sone/freenet/wot/IdentityManagerTest.kt new file mode 100644 index 0000000..3768efd --- /dev/null +++ b/src/test/java/net/pterodactylus/sone/freenet/wot/IdentityManagerTest.kt @@ -0,0 +1,33 @@ +package net.pterodactylus.sone.freenet.wot + +import com.google.common.eventbus.* +import net.pterodactylus.sone.freenet.plugin.* +import net.pterodactylus.sone.test.* +import org.hamcrest.MatcherAssert.* +import org.hamcrest.Matchers.* +import org.junit.* +import org.mockito.Mockito.* + +/** + * Unit test for [IdentityManagerImpl]. + */ +class IdentityManagerTest { + + private val eventBus = mock() + private val webOfTrustConnector = mock() + private val identityManager = IdentityManagerImpl(eventBus, webOfTrustConnector, IdentityLoader(webOfTrustConnector, Context("Test"))) + + @Test + fun identityManagerPingsWotConnector() { + assertThat(identityManager.isConnected, equalTo(true)) + verify(webOfTrustConnector).ping() + } + + @Test + fun disconnectedWotConnectorIsRecognized() { + doThrow(PluginException::class.java).whenever(webOfTrustConnector).ping() + assertThat(identityManager.isConnected, equalTo(false)) + verify(webOfTrustConnector).ping() + } + +} diff --git a/src/test/java/net/pterodactylus/sone/freenet/wot/event/IdentityEventTest.java b/src/test/java/net/pterodactylus/sone/freenet/wot/event/IdentityEventTest.java deleted file mode 100644 index 4fa5bc9..0000000 --- a/src/test/java/net/pterodactylus/sone/freenet/wot/event/IdentityEventTest.java +++ /dev/null @@ -1,52 +0,0 @@ -package net.pterodactylus.sone.freenet.wot.event; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.not; -import static org.mockito.Mockito.mock; - -import net.pterodactylus.sone.freenet.wot.Identity; -import net.pterodactylus.sone.freenet.wot.OwnIdentity; - -import org.junit.Test; - -/** - * Unit test for {@link IdentityEvent}. - */ -public class IdentityEventTest { - - private final OwnIdentity ownIdentity = mock(OwnIdentity.class); - private final Identity identity = mock(Identity.class); - private final IdentityEvent identityEvent = createIdentityEvent(ownIdentity, identity); - - private IdentityEvent createIdentityEvent(final OwnIdentity ownIdentity, final Identity identity) { - return new IdentityEvent(ownIdentity, identity) { - }; - } - - @Test - public void identityEventRetainsIdentities() { - assertThat(identityEvent.ownIdentity(), is(ownIdentity)); - assertThat(identityEvent.identity(), is(identity)); - } - - @Test - public void eventsWithTheSameIdentityHaveTheSameHashCode() { - IdentityEvent secondIdentityEvent = createIdentityEvent(ownIdentity, identity); - assertThat(identityEvent.hashCode(), is(secondIdentityEvent.hashCode())); - } - - @Test - public void eventsWithTheSameIdentitiesAreEqual() { - IdentityEvent secondIdentityEvent = createIdentityEvent(ownIdentity, identity); - assertThat(identityEvent, is(secondIdentityEvent)); - assertThat(secondIdentityEvent, is(identityEvent)); - } - - @Test - public void nullDoesNotEqualIdentityEvent() { - assertThat(identityEvent, not(is((Object) null))); - } - - -} diff --git a/src/test/java/net/pterodactylus/sone/freenet/wot/event/OwnIdentityEventTest.java b/src/test/java/net/pterodactylus/sone/freenet/wot/event/OwnIdentityEventTest.java deleted file mode 100644 index 3d27c34..0000000 --- a/src/test/java/net/pterodactylus/sone/freenet/wot/event/OwnIdentityEventTest.java +++ /dev/null @@ -1,46 +0,0 @@ -package net.pterodactylus.sone.freenet.wot.event; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.not; -import static org.mockito.Mockito.mock; - -import net.pterodactylus.sone.freenet.wot.OwnIdentity; - -import org.junit.Test; - -/** - * Unit test for {@link OwnIdentityEvent}. - */ -public class OwnIdentityEventTest { - - private final OwnIdentity ownIdentity = mock(OwnIdentity.class); - private final OwnIdentityEvent ownIdentityEvent = createOwnIdentityEvent(ownIdentity); - - @Test - public void eventRetainsOwnIdentity() { - assertThat(ownIdentityEvent.ownIdentity(), is(ownIdentity)); - } - - protected OwnIdentityEvent createOwnIdentityEvent(final OwnIdentity ownIdentity) { - return new OwnIdentityEvent(ownIdentity) { - }; - } - - @Test - public void twoOwnIdentityEventsWithTheSameIdentityHaveTheSameHashCode() { - OwnIdentityEvent secondOwnIdentityEvent = createOwnIdentityEvent(ownIdentity); - assertThat(secondOwnIdentityEvent.hashCode(), is(ownIdentityEvent.hashCode())); - } - - @Test - public void ownIdentityEventDoesNotMatchNull() { - assertThat(ownIdentityEvent, not(is((Object) null))); - } - - @Test - public void ownIdentityEventDoesNotMatchObjectWithADifferentClass() { - assertThat(ownIdentityEvent, not(is(new Object()))); - } - -} diff --git a/src/test/java/net/pterodactylus/sone/main/DebugLoadersTest.java b/src/test/java/net/pterodactylus/sone/main/DebugLoadersTest.java index 7a84978..72f0bf7 100644 --- a/src/test/java/net/pterodactylus/sone/main/DebugLoadersTest.java +++ b/src/test/java/net/pterodactylus/sone/main/DebugLoadersTest.java @@ -23,7 +23,6 @@ import net.pterodactylus.util.web.Response; import freenet.clients.http.SessionManager; import freenet.clients.http.ToadletContext; -import freenet.l10n.BaseL10n; import freenet.support.api.HTTPRequest; import com.google.common.base.Charsets; @@ -41,7 +40,6 @@ public class DebugLoadersTest { @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); - private final BaseL10n l10n = mock(BaseL10n.class); private final StringWriter stringWriter = new StringWriter(); private final TemplateContext templateContext = new TemplateContext(); private Loaders loaders; @@ -72,7 +70,7 @@ public class DebugLoadersTest { HTTPRequest httpRequest = mock(HTTPRequest.class); ToadletContext toadletContext = mock(ToadletContext.class); SessionManager sessionManager = mock(SessionManager.class); - FreenetRequest request = new FreenetRequest(uri, method, httpRequest, toadletContext, l10n, sessionManager); + FreenetRequest request = new FreenetRequest(uri, method, httpRequest, toadletContext, sessionManager); OutputStream outputStream = new ByteArrayOutputStream(); Response response = new Response(outputStream); page.handleRequest(request, response); diff --git a/src/test/java/net/pterodactylus/sone/main/DefaultLoadersTest.java b/src/test/java/net/pterodactylus/sone/main/DefaultLoadersTest.java index dc51479..d91e9e3 100644 --- a/src/test/java/net/pterodactylus/sone/main/DefaultLoadersTest.java +++ b/src/test/java/net/pterodactylus/sone/main/DefaultLoadersTest.java @@ -23,7 +23,6 @@ import net.pterodactylus.util.web.Response; import freenet.clients.http.SessionManager; import freenet.clients.http.ToadletContext; -import freenet.l10n.BaseL10n; import freenet.support.api.HTTPRequest; import org.junit.Test; @@ -33,7 +32,6 @@ import org.junit.Test; */ public class DefaultLoadersTest { - private final BaseL10n l10n = mock(BaseL10n.class); private final Loaders loaders = new DefaultLoaders(); private final StringWriter stringWriter = new StringWriter(); private final TemplateContext templateContext = new TemplateContext(); @@ -53,7 +51,7 @@ public class DefaultLoadersTest { HTTPRequest httpRequest = mock(HTTPRequest.class); ToadletContext toadletContext = mock(ToadletContext.class); SessionManager sessionManager = mock(SessionManager.class); - FreenetRequest request = new FreenetRequest(uri, method, httpRequest, toadletContext, l10n, sessionManager); + FreenetRequest request = new FreenetRequest(uri, method, httpRequest, toadletContext, sessionManager); OutputStream outputStream = new ByteArrayOutputStream(); Response response = new Response(outputStream); staticPage.handleRequest(request, response); diff --git a/src/test/java/net/pterodactylus/sone/notify/ListNotificationTest.java b/src/test/java/net/pterodactylus/sone/notify/ListNotificationTest.java deleted file mode 100644 index 5e40f81..0000000 --- a/src/test/java/net/pterodactylus/sone/notify/ListNotificationTest.java +++ /dev/null @@ -1,120 +0,0 @@ -package net.pterodactylus.sone.notify; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.emptyIterable; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.not; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; -import static org.mockito.hamcrest.MockitoHamcrest.argThat; - -import java.util.Arrays; - -import net.pterodactylus.util.notify.NotificationListener; -import net.pterodactylus.util.template.Template; -import net.pterodactylus.util.template.TemplateContext; - -import org.hamcrest.Matchers; -import org.junit.Test; - -/** - * Unit test for {@link ListNotification}. - */ -public class ListNotificationTest { - - private static final String ID = "notification-id"; - private static final String KEY = "element-key"; - private static final String OTHER_KEY = "other-key"; - - private final Template template = mock(Template.class); - private final TemplateContext templateInitialContext = mock(TemplateContext.class); - private ListNotification listNotification; - - public ListNotificationTest() { - when(template.getInitialContext()).thenReturn(templateInitialContext); - listNotification = new ListNotification<>(ID, KEY, template); - } - - @Test - public void creatingAListNotificationSetsEmptyIterableOnElementKeyInTemplateContext() { - verify(templateInitialContext).set(eq(KEY), argThat(emptyIterable())); - } - - @Test - public void newListNotificationHasNoElement() { - assertThat(listNotification.getElements(), emptyIterable()); - } - - @Test - public void newListNotificationIsEmpty() { - assertThat(listNotification.isEmpty(), is(true)); - } - - @Test - public void listNotificationRetainsSetElements() { - listNotification.setElements(Arrays.asList("a", "b", "c")); - assertThat(listNotification.getElements(), Matchers.contains("a", "b", "c")); - } - - @Test - public void listNotificationRetainsAddedElements() { - listNotification.add("a"); - listNotification.add("b"); - listNotification.add("c"); - assertThat(listNotification.getElements(), Matchers.contains("a", "b", "c")); - } - - @Test - public void listNotificationRemovesCorrectElement() { - listNotification.setElements(Arrays.asList("a", "b", "c")); - listNotification.remove("b"); - assertThat(listNotification.getElements(), Matchers.contains("a", "c")); - } - - @Test - public void removingTheLastElementDismissesTheNotification() { - NotificationListener notificationListener = mock(NotificationListener.class); - listNotification.addNotificationListener(notificationListener); - listNotification.add("a"); - listNotification.remove("a"); - verify(notificationListener).notificationDismissed(listNotification); - } - - @Test - public void dismissingTheListNotificationRemovesAllElements() { - listNotification.setElements(Arrays.asList("a", "b", "c")); - listNotification.dismiss(); - assertThat(listNotification.getElements(), emptyIterable()); - } - - @Test - public void listNotificationWithDifferentElementsIsNotEqual() { - ListNotification secondNotification = new ListNotification(ID, KEY, template); - listNotification.add("a"); - secondNotification.add("b"); - assertThat(listNotification, not(is(secondNotification))); - } - - @Test - public void listNotificationWithDifferentKeyIsNotEqual() { - ListNotification secondNotification = new ListNotification(ID, OTHER_KEY, template); - assertThat(listNotification, not(is(secondNotification))); - } - - @Test - public void copiedNotificationsHaveTheSameHashCode() { - ListNotification secondNotification = new ListNotification(listNotification); - listNotification.add("a"); - secondNotification.add("a"); - listNotification.setLastUpdateTime(secondNotification.getLastUpdatedTime()); - assertThat(listNotification.hashCode(), is(secondNotification.hashCode())); - } - - @Test - public void listNotificationIsNotEqualToOtherObjects() { - assertThat(listNotification, not(is(new Object()))); - } - -} diff --git a/src/test/java/net/pterodactylus/sone/template/FilesystemTemplateTest.java b/src/test/java/net/pterodactylus/sone/template/FilesystemTemplateTest.java deleted file mode 100644 index 9259ef5..0000000 --- a/src/test/java/net/pterodactylus/sone/template/FilesystemTemplateTest.java +++ /dev/null @@ -1,125 +0,0 @@ -package net.pterodactylus.sone.template; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.notNullValue; - -import java.io.File; -import java.io.IOException; -import java.io.StringWriter; -import java.io.Writer; -import java.util.Arrays; -import java.util.concurrent.atomic.AtomicReference; - -import net.pterodactylus.util.template.Part; -import net.pterodactylus.util.template.TemplateContext; -import net.pterodactylus.util.template.TemplateException; - -import com.google.common.base.Charsets; -import com.google.common.io.Files; -import org.junit.Before; -import org.junit.Test; - -/** - * Unit test for {@link FilesystemTemplate}. - */ -public class FilesystemTemplateTest { - - private final File tempFile; - private final FilesystemTemplate filesystemTemplate; - private final AtomicReference stringWriter = new AtomicReference<>(new StringWriter()); - private final TemplateContext templateContext = new TemplateContext(); - - public FilesystemTemplateTest() throws IOException { - tempFile = File.createTempFile("template-", ".dat"); - writeTemplate("Text"); - filesystemTemplate = new FilesystemTemplate(tempFile.getAbsolutePath()); - } - - private void writeTemplate(String text) throws IOException { - Files.write(text + ".<%foreach values value><% value><%/foreach>", tempFile, Charsets.UTF_8); - } - - @Before - public void setupTemplateContext() { - templateContext.set("values", Arrays.asList("a", 1)); - } - - @Test(expected = FilesystemTemplate.TemplateFileNotFoundException.class) - public void loadingTemplateFromNonExistingFileThrowsException() throws IOException { - FilesystemTemplate filesystemTemplate = new FilesystemTemplate("/a/b/c.dat"); - filesystemTemplate.getInitialContext(); - } - - @Test - public void templateCanBeLoadedFromTheFilesystem() { - filesystemTemplate.render(templateContext, stringWriter.get()); - assertThat(getRenderedString(), is("Text.a1")); - } - - @Test - public void templateCanBeReloaded() throws IOException, InterruptedException { - filesystemTemplate.render(templateContext, stringWriter.get()); - assertThat(getRenderedString(), is("Text.a1")); - Thread.sleep(1000); - writeTemplate("New"); - filesystemTemplate.render(templateContext, stringWriter.get()); - assertThat(getRenderedString(), is("New.a1")); - } - - @Test - public void templateIsNotReloadedIfNotChanged() { - filesystemTemplate.render(templateContext, stringWriter.get()); - assertThat(getRenderedString(), is("Text.a1")); - filesystemTemplate.render(templateContext, stringWriter.get()); - assertThat(getRenderedString(), is("Text.a1")); - } - - private String getRenderedString() { - String renderedString = stringWriter.get().toString(); - stringWriter.set(new StringWriter()); - return renderedString; - } - - @Test - public void initialContextIsCopiedToReloadedTemplates() throws IOException, InterruptedException { - filesystemTemplate.getInitialContext().set("values", "test"); - Thread.sleep(1000); - writeTemplate("New"); - assertThat(filesystemTemplate.getInitialContext().get("values"), is((Object) "test")); - } - - @Test - public void partsAreCopiedToReloadedTemplates() throws InterruptedException, IOException { - filesystemTemplate.add(new Part() { - @Override - public void render(TemplateContext templateContext, Writer writer) throws TemplateException { - try { - writer.write(".Test"); - } catch (IOException e) { - throw new TemplateException(e); - } - } - }); - Thread.sleep(1000); - writeTemplate("New"); - filesystemTemplate.render(templateContext, stringWriter.get()); - assertThat(getRenderedString(), is("New.a1.Test")); - } - - @Test - public void columnOfReturnedTemplateIsReturnedAsZero() { - assertThat(filesystemTemplate.getColumn(), is(0)); - } - - @Test - public void lineOfReturnedTemplateIsReturnedAsZero() { - assertThat(filesystemTemplate.getLine(), is(0)); - } - - @Test - public void templateCanBeIteratedOver() { - assertThat(filesystemTemplate.iterator(), notNullValue()); - } - -} diff --git a/src/test/java/net/pterodactylus/sone/template/PostAccessorTest.java b/src/test/java/net/pterodactylus/sone/template/PostAccessorTest.java deleted file mode 100644 index e60dce9..0000000 --- a/src/test/java/net/pterodactylus/sone/template/PostAccessorTest.java +++ /dev/null @@ -1,118 +0,0 @@ -package net.pterodactylus.sone.template; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.contains; -import static org.hamcrest.Matchers.is; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.Set; - -import net.pterodactylus.sone.core.Core; -import net.pterodactylus.sone.data.Post; -import net.pterodactylus.sone.data.PostReply; -import net.pterodactylus.sone.data.Sone; -import net.pterodactylus.util.template.TemplateContext; - -import org.junit.Before; -import org.junit.Test; - -/** - * Unit test for {@link PostAccessor}. - */ -public class PostAccessorTest { - - private final Core core = mock(Core.class); - private final PostAccessor accessor = new PostAccessor(core); - private final Post post = mock(Post.class); - - private final long now = System.currentTimeMillis(); - - @Before - public void setupPost() { - when(post.getId()).thenReturn("post-id"); - } - - @Test - @SuppressWarnings("unchecked") - public void accessorReturnsTheCorrectReplies() { - List replies = new ArrayList<>(); - replies.add(createPostReply(2000)); - replies.add(createPostReply(-1000)); - replies.add(createPostReply(-2000)); - replies.add(createPostReply(-3000)); - replies.add(createPostReply(-4000)); - when(core.getReplies("post-id")).thenReturn(replies); - Collection repliesForPost = (Collection) accessor.get(null, post, "replies"); - assertThat(repliesForPost, contains( - replies.get(1), - replies.get(2), - replies.get(3), - replies.get(4) - )); - } - - private PostReply createPostReply(long timeOffset) { - PostReply postReply = mock(PostReply.class); - when(postReply.getTime()).thenReturn(now + timeOffset); - return postReply; - } - - @Test - @SuppressWarnings("unchecked") - public void accessorReturnsTheLikingSones() { - Set sones = mock(Set.class); - when(core.getLikes(post)).thenReturn(sones); - Set likingSones = (Set) accessor.get(null, post, "likes"); - assertThat(likingSones, is(sones)); - } - - @Test - public void accessorReturnsWhetherTheCurrentSoneLikedAPost() { - Sone sone = mock(Sone.class); - when(sone.isLikedPostId("post-id")).thenReturn(true); - TemplateContext templateContext = new TemplateContext(); - templateContext.set("currentSone", sone); - assertThat(accessor.get(templateContext, post, "liked"), is((Object) true)); - } - - @Test - public void accessorReturnsFalseIfPostIsNotLiked() { - Sone sone = mock(Sone.class); - TemplateContext templateContext = new TemplateContext(); - templateContext.set("currentSone", sone); - assertThat(accessor.get(templateContext, post, "liked"), is((Object) false)); - } - - @Test - public void accessorReturnsFalseIfThereIsNoCurrentSone() { - TemplateContext templateContext = new TemplateContext(); - assertThat(accessor.get(templateContext, post, "liked"), is((Object) false)); - } - - @Test - public void accessorReturnsThatNotKnownPostIsNew() { - assertThat(accessor.get(null, post, "new"), is((Object) true)); - } - - @Test - public void accessorReturnsThatKnownPostIsNotNew() { - when(post.isKnown()).thenReturn(true); - assertThat(accessor.get(null, post, "new"), is((Object) false)); - } - - @Test - public void accessorReturnsIfPostIsBookmarked() { - when(core.isBookmarked(post)).thenReturn(true); - assertThat(accessor.get(null, post, "bookmarked"), is((Object) true)); - } - - @Test - public void accessorReturnsOtherProperties() { - assertThat(accessor.get(null, post, "hashCode"), is((Object) post.hashCode())); - } - -} diff --git a/src/test/java/net/pterodactylus/sone/test/Matchers.java b/src/test/java/net/pterodactylus/sone/test/Matchers.java index d04da0f..65fb3a6 100644 --- a/src/test/java/net/pterodactylus/sone/test/Matchers.java +++ b/src/test/java/net/pterodactylus/sone/test/Matchers.java @@ -1,5 +1,5 @@ /* - * Sone - Matchers.java - Copyright © 2013–2019 David Roden + * Sone - Matchers.java - Copyright © 2013–2020 David 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 @@ -22,6 +22,8 @@ import static java.util.regex.Pattern.compile; import java.io.IOException; import java.io.InputStream; +import javax.annotation.*; + import net.pterodactylus.sone.data.Album; import net.pterodactylus.sone.data.Image; import net.pterodactylus.sone.data.Post; @@ -91,8 +93,7 @@ public class Matchers { }; } - public static Matcher isPost(String postId, long time, - String text, Optional recipient) { + public static Matcher isPost(String postId, long time, String text, @Nullable String recipient) { return new PostMatcher(postId, time, text, recipient); } @@ -229,10 +230,10 @@ public class Matchers { private final String postId; private final long time; private final String text; - private final Optional recipient; + @Nullable + private final String recipient; - private PostMatcher(String postId, long time, String text, - Optional recipient) { + private PostMatcher(String postId, long time, String text, @Nullable String recipient) { this.postId = postId; this.time = time; this.text = text; @@ -257,15 +258,15 @@ public class Matchers { .appendValue(text); return false; } - if (recipient.isPresent()) { + if (recipient != null) { if (!post.getRecipientId().isPresent()) { mismatchDescription.appendText( "Recipient not present"); return false; } - if (!post.getRecipientId().get().equals(recipient.get())) { + if (!post.getRecipientId().get().equals(recipient)) { mismatchDescription.appendText("Recipient is not ") - .appendValue(recipient.get()); + .appendValue(recipient); return false; } } else { @@ -283,9 +284,9 @@ public class Matchers { .appendValue(postId); description.appendText(", created at @").appendValue(time); description.appendText(", text ").appendValue(text); - if (recipient.isPresent()) { + if (recipient != null) { description.appendText(", directed at ") - .appendValue(recipient.get()); + .appendValue(recipient); } } diff --git a/src/test/kotlin/net/pterodactylus/sone/core/ConfigurationSoneParserTest.kt b/src/test/kotlin/net/pterodactylus/sone/core/ConfigurationSoneParserTest.kt new file mode 100644 index 0000000..90e3fa6 --- /dev/null +++ b/src/test/kotlin/net/pterodactylus/sone/core/ConfigurationSoneParserTest.kt @@ -0,0 +1,436 @@ +package net.pterodactylus.sone.core + +import com.google.common.base.Optional.* +import net.pterodactylus.sone.core.ConfigurationSoneParser.* +import net.pterodactylus.sone.data.* +import net.pterodactylus.sone.database.* +import net.pterodactylus.sone.test.* +import net.pterodactylus.sone.test.Matchers.* +import net.pterodactylus.util.config.* +import org.hamcrest.MatcherAssert.* +import org.hamcrest.Matchers.* +import org.junit.* +import org.junit.rules.* +import org.mockito.ArgumentMatchers.* +import org.mockito.ArgumentMatchers.eq + +/** + * Unit test for [ConfigurationSoneParser]. + */ +class ConfigurationSoneParserTest { + + @Rule + @JvmField + val expectedException = ExpectedException.none()!! + + private val configuration = mock() + private val sone = mock().apply { + whenever(this.id).thenReturn("1") + } + private val configurationSoneParser = ConfigurationSoneParser(configuration, sone) + + @Test + fun emptyProfileIsLoadedCorrectly() { + setupEmptyProfile() + val profile = configurationSoneParser.parseProfile() + assertThat(profile, notNullValue()) + assertThat(profile.firstName, nullValue()) + assertThat(profile.middleName, nullValue()) + assertThat(profile.lastName, nullValue()) + assertThat(profile.birthDay, nullValue()) + assertThat(profile.birthMonth, nullValue()) + assertThat(profile.birthYear, nullValue()) + assertThat(profile.fields, emptyIterable()) + } + + private fun setupEmptyProfile() { + whenever(configuration.getStringValue(anyString())).thenReturn(TestValue.from(null)) + whenever(configuration.getIntValue(anyString())).thenReturn(TestValue.from(null)) + } + + @Test + fun filledProfileWithFieldsIsParsedCorrectly() { + setupFilledProfile() + val profile = configurationSoneParser.parseProfile() + assertThat(profile, notNullValue()) + assertThat(profile.firstName, equalTo("First")) + assertThat(profile.middleName, equalTo("M.")) + assertThat(profile.lastName, equalTo("Last")) + assertThat(profile.birthDay, equalTo(18)) + assertThat(profile.birthMonth, equalTo(12)) + assertThat(profile.birthYear, equalTo(1976)) + val fields = profile.fields + assertThat(fields, hasSize(2)) + assertThat(fields[0].name, equalTo("Field1")) + assertThat(fields[0].value, equalTo("Value1")) + assertThat(fields[1].name, equalTo("Field2")) + assertThat(fields[1].value, equalTo("Value2")) + } + + private fun setupFilledProfile() { + setupString("Sone/1/Profile/FirstName", "First") + setupString("Sone/1/Profile/MiddleName", "M.") + setupString("Sone/1/Profile/LastName", "Last") + setupInteger("Sone/1/Profile/BirthDay", 18) + setupInteger("Sone/1/Profile/BirthMonth", 12) + setupInteger("Sone/1/Profile/BirthYear", 1976) + setupString("Sone/1/Profile/Fields/0/Name", "Field1") + setupString("Sone/1/Profile/Fields/0/Value", "Value1") + setupString("Sone/1/Profile/Fields/1/Name", "Field2") + setupString("Sone/1/Profile/Fields/1/Value", "Value2") + setupString("Sone/1/Profile/Fields/2/Name") + } + + private fun setupString(nodeName: String, value: String? = null) { + whenever(configuration.getStringValue(eq(nodeName))).thenReturn(TestValue.from(value)) + } + + private fun setupInteger(nodeName: String, value: Int?) { + whenever(configuration.getIntValue(eq(nodeName))).thenReturn(TestValue.from(value)) + } + + @Test + fun postsAreParsedCorrectly() { + setupCompletePosts() + val postBuilderFactory = createPostBuilderFactory() + val posts = configurationSoneParser.parsePosts(postBuilderFactory) + assertThat(posts, containsInAnyOrder( + isPost("P0", 1000L, "T0", null), + isPost("P1", 1001L, "T1", "1234567890123456789012345678901234567890123") + )) + } + + private fun createPostBuilderFactory(): PostBuilderFactory { + val postBuilderFactory = mock() + whenever(postBuilderFactory.newPostBuilder()).thenAnswer { TestPostBuilder() } + return postBuilderFactory + } + + private fun setupCompletePosts() { + setupPost("0", "P0", 1000L, "T0") + setupPost("1", "P1", 1001L, "T1", "1234567890123456789012345678901234567890123") + setupPost("2") + } + + private fun setupPost(postNumber: String, postId: String? = null, time: Long = 0, text: String? = null, recipientId: String? = null) { + setupString("Sone/1/Posts/$postNumber/ID", postId) + setupLong("Sone/1/Posts/$postNumber/Time", time) + setupString("Sone/1/Posts/$postNumber/Text", text) + setupString("Sone/1/Posts/$postNumber/Recipient", recipientId) + } + + private fun setupLong(nodeName: String, value: Long?) { + whenever(configuration.getLongValue(eq(nodeName))).thenReturn(TestValue.from(value)) + } + + @Test + fun postWithoutTimeIsRecognized() { + setupPostWithoutTime() + expectedException.expect() + configurationSoneParser.parsePosts(createPostBuilderFactory()) + } + + private fun setupPostWithoutTime() { + setupPost("0", "P0", 0L, "T0") + } + + @Test + fun postWithoutTextIsRecognized() { + setupPostWithoutText() + expectedException.expect() + configurationSoneParser.parsePosts(createPostBuilderFactory()) + } + + private fun setupPostWithoutText() { + setupPost("0", "P0", 1000L) + } + + @Test + fun postWithInvalidRecipientIdIsRecognized() { + setupPostWithInvalidRecipientId() + val posts = configurationSoneParser.parsePosts(createPostBuilderFactory()) + assertThat(posts, contains(isPost("P0", 1000L, "T0", null))) + } + + private fun setupPostWithInvalidRecipientId() { + setupPost("0", "P0", 1000L, "T0", "123") + setupPost("1") + } + + @Test + fun postRepliesAreParsedCorrectly() { + setupPostReplies() + val postReplyBuilderFactory = object : PostReplyBuilderFactory { + override fun newPostReplyBuilder(): PostReplyBuilder { + return TestPostReplyBuilder() + } + } + val postReplies = configurationSoneParser.parsePostReplies(postReplyBuilderFactory) + assertThat(postReplies, hasSize(2)) + assertThat(postReplies, containsInAnyOrder( + isPostReply("R0", "P0", 1000L, "T0"), + isPostReply("R1", "P1", 1001L, "T1") + )) + } + + private fun setupPostReplies() { + setupPostReply("0", "R0", "P0", 1000L, "T0") + setupPostReply("1", "R1", "P1", 1001L, "T1") + setupPostReply("2") + } + + private fun setupPostReply(postReplyNumber: String, postReplyId: String? = null, postId: String? = null, time: Long = 0, text: String? = null) { + setupString("Sone/1/Replies/$postReplyNumber/ID", postReplyId) + setupString("Sone/1/Replies/$postReplyNumber/Post/ID", postId) + setupLong("Sone/1/Replies/$postReplyNumber/Time", time) + setupString("Sone/1/Replies/$postReplyNumber/Text", text) + } + + @Test + fun missingPostIdIsRecognized() { + setupPostReplyWithMissingPostId() + expectedException.expect() + configurationSoneParser.parsePostReplies(null) + } + + private fun setupPostReplyWithMissingPostId() { + setupPostReply("0", "R0", null, 1000L, "T0") + } + + @Test + fun missingPostReplyTimeIsRecognized() { + setupPostReplyWithMissingPostReplyTime() + expectedException.expect() + configurationSoneParser.parsePostReplies(null) + } + + private fun setupPostReplyWithMissingPostReplyTime() { + setupPostReply("0", "R0", "P0", 0L, "T0") + } + + @Test + fun missingPostReplyTextIsRecognized() { + setupPostReplyWithMissingPostReplyText() + expectedException.expect() + configurationSoneParser.parsePostReplies(null) + } + + private fun setupPostReplyWithMissingPostReplyText() { + setupPostReply("0", "R0", "P0", 1000L) + } + + @Test + fun likedPostIdsParsedCorrectly() { + setupLikedPostIds() + val likedPostIds = configurationSoneParser.parseLikedPostIds() + assertThat(likedPostIds, containsInAnyOrder("P1", "P2", "P3")) + } + + private fun setupLikedPostIds() { + setupString("Sone/1/Likes/Post/0/ID", "P1") + setupString("Sone/1/Likes/Post/1/ID", "P2") + setupString("Sone/1/Likes/Post/2/ID", "P3") + setupString("Sone/1/Likes/Post/3/ID") + } + + @Test + fun likedPostReplyIdsAreParsedCorrectly() { + setupLikedPostReplyIds() + val likedPostReplyIds = configurationSoneParser.parseLikedPostReplyIds() + assertThat(likedPostReplyIds, containsInAnyOrder("R1", "R2", "R3")) + } + + private fun setupLikedPostReplyIds() { + setupString("Sone/1/Likes/Reply/0/ID", "R1") + setupString("Sone/1/Likes/Reply/1/ID", "R2") + setupString("Sone/1/Likes/Reply/2/ID", "R3") + setupString("Sone/1/Likes/Reply/3/ID") + } + + @Test + fun friendsAreParsedCorrectly() { + setupFriends() + val friends = configurationSoneParser.parseFriends() + assertThat(friends, containsInAnyOrder("F1", "F2", "F3")) + } + + private fun setupFriends() { + setupString("Sone/1/Friends/0/ID", "F1") + setupString("Sone/1/Friends/1/ID", "F2") + setupString("Sone/1/Friends/2/ID", "F3") + setupString("Sone/1/Friends/3/ID") + } + + @Test + fun topLevelAlbumsAreParsedCorrectly() { + setupTopLevelAlbums() + val albumBuilderFactory = createAlbumBuilderFactory() + val topLevelAlbums = configurationSoneParser.parseTopLevelAlbums(albumBuilderFactory) + assertThat(topLevelAlbums, hasSize(2)) + val firstAlbum = topLevelAlbums[0] + assertThat(firstAlbum, isAlbum("A1", null, "T1", "D1")) + assertThat(firstAlbum.albums, emptyIterable()) + assertThat>(firstAlbum.images, emptyIterable()) + val secondAlbum = topLevelAlbums[1] + assertThat(secondAlbum, isAlbum("A2", null, "T2", "D2")) + assertThat(secondAlbum.albums, hasSize(1)) + assertThat>(secondAlbum.images, emptyIterable()) + val thirdAlbum = secondAlbum.albums[0] + assertThat(thirdAlbum, isAlbum("A3", "A2", "T3", "D3")) + assertThat(thirdAlbum.albums, emptyIterable()) + assertThat>(thirdAlbum.images, emptyIterable()) + } + + private fun setupTopLevelAlbums() { + setupAlbum(0, "A1", null, "T1", "D1", "I1") + setupAlbum(1, "A2", null, "T2", "D2") + setupAlbum(2, "A3", "A2", "T3", "D3", "I3") + setupAlbum(3) + } + + private fun setupAlbum(albumNumber: Int, albumId: String? = null, parentAlbumId: String? = null, title: String? = null, description: String? = null, imageId: String? =null) { + val albumPrefix = "Sone/1/Albums/$albumNumber" + setupString("$albumPrefix/ID", albumId) + setupString("$albumPrefix/Title", title) + setupString("$albumPrefix/Description", description) + setupString("$albumPrefix/Parent", parentAlbumId) + setupString("$albumPrefix/AlbumImage", imageId) + } + + private fun createAlbumBuilderFactory(): AlbumBuilderFactory { + val albumBuilderFactory = mock() + whenever(albumBuilderFactory.newAlbumBuilder()).thenAnswer { TestAlbumBuilder() } + return albumBuilderFactory + } + + @Test + fun albumWithInvalidTitleIsRecognized() { + setupAlbum(0, "A1", null, null, "D1", "I1") + expectedException.expect() + configurationSoneParser.parseTopLevelAlbums(createAlbumBuilderFactory()) + } + + @Test + fun albumWithInvalidDescriptionIsRecognized() { + setupAlbum(0, "A1", null, "T1", null, "I1") + expectedException.expect() + configurationSoneParser.parseTopLevelAlbums(createAlbumBuilderFactory()) + } + + @Test + fun albumWithInvalidParentIsRecognized() { + setupAlbum(0, "A1", "A0", "T1", "D1", "I1") + expectedException.expect() + configurationSoneParser.parseTopLevelAlbums(createAlbumBuilderFactory()) + } + + @Test + fun imagesAreParsedCorrectly() { + setupTopLevelAlbums() + configurationSoneParser.parseTopLevelAlbums(createAlbumBuilderFactory()) + setupImages() + configurationSoneParser.parseImages(createImageBuilderFactory()) + val albums = configurationSoneParser.albums + assertThat>(albums["A1"]!!.images, contains(isImage("I1", 1000L, "K1", "T1", "D1", 16, 9))) + assertThat>(albums["A2"]!!.images, contains(isImage("I2", 2000L, "K2", "T2", "D2", 16 * 2, 9 * 2))) + assertThat>(albums["A3"]!!.images, contains(isImage("I3", 3000L, "K3", "T3", "D3", 16 * 3, 9 * 3))) + } + + private fun setupImages() { + setupImage(0, "I1", "A1", 1000L, "K1", "T1", "D1", 16, 9) + setupImage(1, "I2", "A2", 2000L, "K2", "T2", "D2", 16 * 2, 9 * 2) + setupImage(2, "I3", "A3", 3000L, "K3", "T3", "D3", 16 * 3, 9 * 3) + setupImage(3, null, null, 0L, null, null, null, 0, 0) + } + + private fun setupImage(imageNumber: Int, id: String?, parentAlbumId: String?, creationTime: Long?, key: String?, title: String?, description: String?, width: Int?, height: Int?) { + val imagePrefix = "Sone/1/Images/$imageNumber" + setupString("$imagePrefix/ID", id) + setupString("$imagePrefix/Album", parentAlbumId) + setupLong("$imagePrefix/CreationTime", creationTime) + setupString("$imagePrefix/Key", key) + setupString("$imagePrefix/Title", title) + setupString("$imagePrefix/Description", description) + setupInteger("$imagePrefix/Width", width) + setupInteger("$imagePrefix/Height", height) + } + + private fun createImageBuilderFactory(): ImageBuilderFactory { + val imageBuilderFactory = mock() + whenever(imageBuilderFactory.newImageBuilder()).thenAnswer { TestImageBuilder() } + return imageBuilderFactory + } + + @Test + fun missingAlbumIdIsRecognized() { + setupTopLevelAlbums() + configurationSoneParser.parseTopLevelAlbums(createAlbumBuilderFactory()) + setupImage(0, "I1", null, 1000L, "K1", "T1", "D1", 16, 9) + expectedException.expect() + configurationSoneParser.parseImages(createImageBuilderFactory()) + } + + @Test + fun invalidAlbumIdIsRecognized() { + setupTopLevelAlbums() + configurationSoneParser.parseTopLevelAlbums(createAlbumBuilderFactory()) + setupImage(0, "I1", "A4", 1000L, "K1", "T1", "D1", 16, 9) + expectedException.expect() + configurationSoneParser.parseImages(createImageBuilderFactory()) + } + + @Test + fun missingCreationTimeIsRecognized() { + setupTopLevelAlbums() + configurationSoneParser.parseTopLevelAlbums(createAlbumBuilderFactory()) + setupImage(0, "I1", "A1", null, "K1", "T1", "D1", 16, 9) + expectedException.expect() + configurationSoneParser.parseImages(createImageBuilderFactory()) + } + + @Test + fun missingKeyIsRecognized() { + setupTopLevelAlbums() + configurationSoneParser.parseTopLevelAlbums(createAlbumBuilderFactory()) + setupImage(0, "I1", "A1", 1000L, null, "T1", "D1", 16, 9) + expectedException.expect() + configurationSoneParser.parseImages(createImageBuilderFactory()) + } + + @Test + fun missingTitleIsRecognized() { + setupTopLevelAlbums() + configurationSoneParser.parseTopLevelAlbums(createAlbumBuilderFactory()) + setupImage(0, "I1", "A1", 1000L, "K1", null, "D1", 16, 9) + expectedException.expect() + configurationSoneParser.parseImages(createImageBuilderFactory()) + } + + @Test + fun missingDescriptionIsRecognized() { + setupTopLevelAlbums() + configurationSoneParser.parseTopLevelAlbums(createAlbumBuilderFactory()) + setupImage(0, "I1", "A1", 1000L, "K1", "T1", null, 16, 9) + expectedException.expect() + configurationSoneParser.parseImages(createImageBuilderFactory()) + } + + @Test + fun missingWidthIsRecognized() { + setupTopLevelAlbums() + configurationSoneParser.parseTopLevelAlbums(createAlbumBuilderFactory()) + setupImage(0, "I1", "A1", 1000L, "K1", "T1", "D1", null, 9) + expectedException.expect() + configurationSoneParser.parseImages(createImageBuilderFactory()) + } + + @Test + fun missingHeightIsRecognized() { + setupTopLevelAlbums() + configurationSoneParser.parseTopLevelAlbums(createAlbumBuilderFactory()) + setupImage(0, "I1", "A1", 1000L, "K1", "T1", "D1", 16, null) + expectedException.expect() + configurationSoneParser.parseImages(createImageBuilderFactory()) + } + +} diff --git a/src/test/kotlin/net/pterodactylus/sone/core/CoreTest.kt b/src/test/kotlin/net/pterodactylus/sone/core/CoreTest.kt new file mode 100644 index 0000000..9eb123c --- /dev/null +++ b/src/test/kotlin/net/pterodactylus/sone/core/CoreTest.kt @@ -0,0 +1,175 @@ +package net.pterodactylus.sone.core + +import com.codahale.metrics.* +import com.google.common.collect.* +import com.google.common.eventbus.* +import net.pterodactylus.sone.core.event.* +import net.pterodactylus.sone.data.* +import net.pterodactylus.sone.database.* +import net.pterodactylus.sone.freenet.wot.* +import net.pterodactylus.sone.freenet.wot.event.* +import net.pterodactylus.sone.test.* +import net.pterodactylus.util.config.* +import org.hamcrest.* +import org.hamcrest.MatcherAssert.* +import org.hamcrest.Matchers.* +import org.mockito.Mockito.inOrder +import org.mockito.Mockito.verify +import org.mockito.hamcrest.MockitoHamcrest.* +import kotlin.test.* + +/** + * Unit test for [Core] and its subclasses. + */ +class CoreTest { + + @Test + fun `mark post known marks post as known`() { + val core = mock() + val post = mock() + core.MarkPostKnown(post).run() + verify(core).markPostKnown(eq(post)) + } + + @Test + fun `mark reply known marks reply as known`() { + val core = mock() + val postReply = mock() + core.MarkReplyKnown(postReply).run() + verify(core).markReplyKnown(eq(postReply)) + } + + @Test + fun `removing an identity sends removal events for all sone elements`() { + // given + val configuration = mock() + val freenetInterface = mock() + val identityManager = mock() + val soneDownloader = mock() + val imageInserter = mock() + val updateChecker = mock() + val webOfTrustUpdater = mock() + val eventBus = mock() + val database = mock() + val metricRegistry = MetricRegistry() + val core = Core(configuration, freenetInterface, identityManager, soneDownloader, imageInserter, updateChecker, webOfTrustUpdater, eventBus, database, metricRegistry) + val ownIdentity = mock() + val identity = mock() + whenever(identity.id).thenReturn("sone-id") + val sone = mock() + whenever(database.getSone("sone-id")).thenReturn(sone) + val postReply1 = mock() + val postReply2 = mock() + whenever(sone.replies).thenReturn(ImmutableSet.of(postReply1, postReply2)) + val post1 = mock() + val post2 = mock() + whenever(sone.posts).thenReturn(ImmutableList.of(post1, post2)) + + // when + core.identityRemoved(IdentityRemovedEvent(ownIdentity, identity)) + + // then + val inOrder = inOrder(eventBus, database) + inOrder.verify(eventBus).post(argThat(isPostReplyRemoved(postReply1))) + inOrder.verify(eventBus).post(argThat(isPostReplyRemoved(postReply2))) + inOrder.verify(eventBus).post(argThat(isPostRemoved(post1))) + inOrder.verify(eventBus).post(argThat(isPostRemoved(post2))) + inOrder.verify(eventBus).post(argThat(isSoneRemoved(sone))) + inOrder.verify(database).removeSone(sone) + } + + private fun isPostRemoved(post: Post): Matcher { + return object : TypeSafeDiagnosingMatcher() { + override fun matchesSafely(item: Any, mismatchDescription: Description): Boolean { + if (item !is PostRemovedEvent) { + mismatchDescription.appendText("is not PostRemovedEvent") + return false + } + if (item.post !== post) { + mismatchDescription.appendText("post is ").appendValue(item.post) + return false + } + return true + } + + override fun describeTo(description: Description) { + description.appendText("is PostRemovedEvent and post is ").appendValue(post) + } + } + } + + private fun isPostReplyRemoved(postReply: PostReply): Matcher { + return object : TypeSafeDiagnosingMatcher() { + override fun matchesSafely(item: Any, mismatchDescription: Description): Boolean { + if (item !is PostReplyRemovedEvent) { + mismatchDescription.appendText("is not PostReplyRemovedEvent") + return false + } + if (item.postReply !== postReply) { + mismatchDescription.appendText("post reply is ").appendValue(item.postReply) + return false + } + return true + } + + override fun describeTo(description: Description) { + description.appendText("is PostReplyRemovedEvent and post is ").appendValue(postReply) + } + } + } + + private fun isSoneRemoved(sone: Sone): Matcher { + return object : TypeSafeDiagnosingMatcher() { + override fun matchesSafely(item: Any, mismatchDescription: Description): Boolean { + if (item !is SoneRemovedEvent) { + mismatchDescription.appendText("is not SoneRemovedEvent") + return false + } + if (item.sone !== sone) { + mismatchDescription.appendText("sone is ").appendValue(item.sone) + return false + } + return true + } + + override fun describeTo(description: Description) { + description.appendText("is SoneRemovedEvent and sone is ").appendValue(sone) + } + } + } + + @Test + fun `core starts with debug set to false`() { + val core = createCore() + assertThat(core.debug, equalTo(false)) + } + + @Test + fun `debug flag can be set`() { + val core = createCore() + core.setDebug() + assertThat(core.debug, equalTo(true)) + } + + @Test + fun `setting debug flag posts event to event bus`() { + val eventBus = mock() + val core = createCore(eventBus) + core.setDebug() + verify(eventBus).post(argThat(instanceOf(DebugActivatedEvent::class.java))) + } + + private fun createCore(eventBus: EventBus = mock()): Core { + val configuration = mock() + val freenetInterface = mock() + val identityManager = mock() + val soneDownloader = mock() + val imageInserter = mock() + val updateChecker = mock() + val webOfTrustUpdater = mock() + val database = mock() + val metricRegistry = MetricRegistry() + return Core(configuration, freenetInterface, identityManager, soneDownloader, imageInserter, updateChecker, webOfTrustUpdater, eventBus, database, metricRegistry) + } + +} diff --git a/src/test/kotlin/net/pterodactylus/sone/core/DefaultElementLoaderTest.kt b/src/test/kotlin/net/pterodactylus/sone/core/DefaultElementLoaderTest.kt index 776d0c9..c66e6ff 100644 --- a/src/test/kotlin/net/pterodactylus/sone/core/DefaultElementLoaderTest.kt +++ b/src/test/kotlin/net/pterodactylus/sone/core/DefaultElementLoaderTest.kt @@ -4,15 +4,12 @@ import com.google.common.base.Ticker import com.google.common.io.ByteStreams import freenet.keys.FreenetURI import net.pterodactylus.sone.core.FreenetInterface.BackgroundFetchCallback -import net.pterodactylus.sone.test.capture -import net.pterodactylus.sone.test.mock +import net.pterodactylus.sone.test.* import org.hamcrest.MatcherAssert.assertThat -import org.hamcrest.Matchers.`is` import org.hamcrest.Matchers.equalTo import org.junit.Test import org.mockito.ArgumentMatchers.any import org.mockito.ArgumentMatchers.eq -import org.mockito.Mockito.`when` import org.mockito.Mockito.times import org.mockito.Mockito.verify import java.io.ByteArrayOutputStream @@ -23,16 +20,6 @@ import java.util.concurrent.TimeUnit */ class DefaultElementLoaderTest { - companion object { - private const val IMAGE_ID = "KSK@gpl.png" - private val freenetURI = FreenetURI(IMAGE_ID) - private const val decomposedKey = "CHK@DCiVgTWW9nnWHJc9EVwtFJ6jAfBSVyy~rgiPvhUKbS4,mNY85V0x7dYcv7SnEYo1PCC6y2wNWMDNt-y9UWQx9fI,AAMC--8/fru%CC%88hstu%CC%88ck.jpg" - private const val normalizedKey = "CHK@DCiVgTWW9nnWHJc9EVwtFJ6jAfBSVyy~rgiPvhUKbS4,mNY85V0x7dYcv7SnEYo1PCC6y2wNWMDNt-y9UWQx9fI,AAMC--8/frühstück.jpg" - private const val textKey = "KSK@gpl.html" - private val sizeOkay = 2097152L - private val sizeNotOkay = sizeOkay + 1 - } - private val freenetInterface = mock() private val ticker = mock() private val elementLoader = DefaultElementLoader(freenetInterface, ticker) @@ -53,49 +40,49 @@ class DefaultElementLoaderTest { @Test fun `element loader returns loading element on first call`() { - assertThat(elementLoader.loadElement(IMAGE_ID).loading, `is`(true)) + assertThat(elementLoader.loadElement(IMAGE_ID).loading, equalTo(true)) } @Test fun `element loader does not cancel on image mime type with 2 mib size`() { elementLoader.loadElement(IMAGE_ID) verify(freenetInterface).startFetch(eq(freenetURI), callback.capture()) - assertThat(callback.value.shouldCancel(freenetURI, "image/png", sizeOkay), `is`(false)) + assertThat(callback.value.shouldCancel(freenetURI, "image/png", sizeOkay), equalTo(false)) } @Test fun `element loader does cancel on image mime type with more than 2 mib size`() { elementLoader.loadElement(IMAGE_ID) verify(freenetInterface).startFetch(eq(freenetURI), callback.capture()) - assertThat(callback.value.shouldCancel(freenetURI, "image/png", sizeNotOkay), `is`(true)) + assertThat(callback.value.shouldCancel(freenetURI, "image/png", sizeNotOkay), equalTo(true)) } @Test fun `element loader does cancel on audio mime type`() { elementLoader.loadElement(IMAGE_ID) verify(freenetInterface).startFetch(eq(freenetURI), callback.capture()) - assertThat(callback.value.shouldCancel(freenetURI, "audio/mpeg", sizeOkay), `is`(true)) + assertThat(callback.value.shouldCancel(freenetURI, "audio/mpeg", sizeOkay), equalTo(true)) } @Test fun `element loader does cancel on video mime type`() { elementLoader.loadElement(IMAGE_ID) verify(freenetInterface).startFetch(eq(freenetURI), callback.capture()) - assertThat(callback.value.shouldCancel(freenetURI, "video/mkv", sizeOkay), `is`(true)) + assertThat(callback.value.shouldCancel(freenetURI, "video/mkv", sizeOkay), equalTo(true)) } @Test fun `element loader does cancel on text mime type`() { elementLoader.loadElement(IMAGE_ID) verify(freenetInterface).startFetch(eq(freenetURI), callback.capture()) - assertThat(callback.value.shouldCancel(freenetURI, "text/plain", sizeOkay), `is`(true)) + assertThat(callback.value.shouldCancel(freenetURI, "text/plain", sizeOkay), equalTo(true)) } @Test fun `element loader does not cancel on text html mime type`() { elementLoader.loadElement(IMAGE_ID) verify(freenetInterface).startFetch(eq(freenetURI), callback.capture()) - assertThat(callback.value.shouldCancel(freenetURI, "text/html", sizeOkay), `is`(false)) + assertThat(callback.value.shouldCancel(freenetURI, "text/html", sizeOkay), equalTo(false)) } @Test @@ -104,14 +91,14 @@ class DefaultElementLoaderTest { verify(freenetInterface).startFetch(eq(FreenetURI(decomposedKey)), callback.capture()) callback.value.loaded(FreenetURI(normalizedKey), "image/png", read("/static/images/unknown-image-0.png")) val linkedElement = elementLoader.loadElement(decomposedKey) - assertThat(linkedElement, `is`(LinkedElement(normalizedKey, properties = mapOf( + assertThat(linkedElement, equalTo(LinkedElement(normalizedKey, properties = mapOf( "type" to "image", "size" to 2451, "sizeHuman" to "2 KiB" )))) } @Test fun `element loader can extract description from description header`() { - elementLoader.loadElement(textKey) + elementLoader.loadElement(textKey) verify(freenetInterface).startFetch(eq(FreenetURI(textKey)), callback.capture()) callback.value.loaded(FreenetURI(textKey), "text/html; charset=UTF-8", read("element-loader.html")) val linkedElement = elementLoader.loadElement(textKey) @@ -126,7 +113,7 @@ class DefaultElementLoaderTest { @Test fun `element loader can extract description from first non-heading paragraph`() { - elementLoader.loadElement(textKey) + elementLoader.loadElement(textKey) verify(freenetInterface).startFetch(eq(FreenetURI(textKey)), callback.capture()) callback.value.loaded(FreenetURI(textKey), "text/html; charset=UTF-8", read("element-loader2.html")) val linkedElement = elementLoader.loadElement(textKey) @@ -141,7 +128,7 @@ class DefaultElementLoaderTest { @Test fun `element loader can not extract description if html is more complicated`() { - elementLoader.loadElement(textKey) + elementLoader.loadElement(textKey) verify(freenetInterface).startFetch(eq(FreenetURI(textKey)), callback.capture()) callback.value.loaded(FreenetURI(textKey), "text/html; charset=UTF-8", read("element-loader3.html")) val linkedElement = elementLoader.loadElement(textKey) @@ -156,7 +143,7 @@ class DefaultElementLoaderTest { @Test fun `element loader can not extract title if it is missing`() { - elementLoader.loadElement(textKey) + elementLoader.loadElement(textKey) verify(freenetInterface).startFetch(eq(FreenetURI(textKey)), callback.capture()) callback.value.loaded(FreenetURI(textKey), "text/html; charset=UTF-8", read("element-loader4.html")) val linkedElement = elementLoader.loadElement(textKey) @@ -174,7 +161,7 @@ class DefaultElementLoaderTest { elementLoader.loadElement(IMAGE_ID) verify(freenetInterface).startFetch(eq(freenetURI), callback.capture()) callback.value.failed(freenetURI) - assertThat(elementLoader.loadElement(IMAGE_ID).failed, `is`(true)) + assertThat(elementLoader.loadElement(IMAGE_ID).failed, equalTo(true)) verify(freenetInterface).startFetch(eq(freenetURI), callback.capture()) } @@ -183,10 +170,10 @@ class DefaultElementLoaderTest { elementLoader.loadElement(IMAGE_ID) verify(freenetInterface).startFetch(eq(freenetURI), callback.capture()) callback.value.failed(freenetURI) - `when`(ticker.read()).thenReturn(TimeUnit.MINUTES.toNanos(31)) + whenever(ticker.read()).thenReturn(TimeUnit.MINUTES.toNanos(31)) val linkedElement = elementLoader.loadElement(IMAGE_ID) - assertThat(linkedElement.failed, `is`(false)) - assertThat(linkedElement.loading, `is`(true)) + assertThat(linkedElement.failed, equalTo(false)) + assertThat(linkedElement.loading, equalTo(true)) verify(freenetInterface, times(2)).startFetch(eq(freenetURI), callback.capture()) } @@ -199,3 +186,11 @@ class DefaultElementLoaderTest { } ?: ByteArray(0) } + +private const val IMAGE_ID = "KSK@gpl.png" +private val freenetURI = FreenetURI(IMAGE_ID) +private const val decomposedKey = "CHK@DCiVgTWW9nnWHJc9EVwtFJ6jAfBSVyy~rgiPvhUKbS4,mNY85V0x7dYcv7SnEYo1PCC6y2wNWMDNt-y9UWQx9fI,AAMC--8/fru%CC%88hstu%CC%88ck.jpg" +private const val normalizedKey = "CHK@DCiVgTWW9nnWHJc9EVwtFJ6jAfBSVyy~rgiPvhUKbS4,mNY85V0x7dYcv7SnEYo1PCC6y2wNWMDNt-y9UWQx9fI,AAMC--8/frühstück.jpg" +private const val textKey = "KSK@gpl.html" +private const val sizeOkay = 2097152L +private const val sizeNotOkay = sizeOkay + 1 diff --git a/src/test/kotlin/net/pterodactylus/sone/core/FreenetInterfaceTest.kt b/src/test/kotlin/net/pterodactylus/sone/core/FreenetInterfaceTest.kt new file mode 100644 index 0000000..e02cddb --- /dev/null +++ b/src/test/kotlin/net/pterodactylus/sone/core/FreenetInterfaceTest.kt @@ -0,0 +1,471 @@ +package net.pterodactylus.sone.core + +import com.google.common.eventbus.* +import freenet.client.* +import freenet.client.FetchException.FetchExceptionMode.* +import freenet.client.InsertException.* +import freenet.client.async.* +import freenet.crypt.* +import freenet.keys.* +import freenet.keys.InsertableClientSSK.* +import freenet.node.* +import freenet.node.RequestStarter.* +import freenet.support.api.* +import freenet.support.io.* +import net.pterodactylus.sone.core.FreenetInterface.* +import net.pterodactylus.sone.core.event.* +import net.pterodactylus.sone.data.* +import net.pterodactylus.sone.data.impl.* +import net.pterodactylus.sone.test.* +import net.pterodactylus.sone.test.Matchers.* +import net.pterodactylus.sone.test.TestUtil.* +import net.pterodactylus.sone.utils.* +import org.hamcrest.MatcherAssert.* +import org.hamcrest.Matchers.equalTo +import org.hamcrest.Matchers.notNullValue +import org.hamcrest.Matchers.nullValue +import org.junit.* +import org.junit.rules.* +import org.mockito.* +import org.mockito.ArgumentCaptor.* +import org.mockito.ArgumentMatchers.eq +import org.mockito.Mockito.* +import java.io.* +import java.util.* +import kotlin.test.Test + +/** + * Unit test for [FreenetInterface]. + */ +class FreenetInterfaceTest { + + @Rule + @JvmField + val expectionException: ExpectedException = ExpectedException.none() + + @Suppress("UnstableApiUsage") + private val eventBus = mock() + private val node = mock() + private val nodeClientCore = mock() + private val highLevelSimpleClient: HighLevelSimpleClient = mock(HighLevelSimpleClient::class.java, withSettings().extraInterfaces(RequestClient::class.java)) + private val randomSource = DummyRandomSource() + private val uskManager = mock() + private val sone = mock() + private val callbackCaptor: ArgumentCaptor = forClass(USKCallback::class.java) + private val image = mock() + private val insertToken: InsertToken + private val bucket = mock() + private val clientGetCallback: ArgumentCaptor = forClass(ClientGetCallback::class.java) + private val uri = FreenetURI("KSK@pgl.png") + private val fetchResult = mock() + private val backgroundFetchCallback = mock() + private val clientGetter = mock() + private val freenetInterface: FreenetInterface + + init { + whenever(nodeClientCore.makeClient(anyShort(), anyBoolean(), anyBoolean())).thenReturn(highLevelSimpleClient) + setField(node, "clientCore", nodeClientCore) + setField(node, "random", randomSource) + setField(nodeClientCore, "uskManager", uskManager) + setField(nodeClientCore, "clientContext", mock()) + freenetInterface = FreenetInterface(eventBus, node) + insertToken = freenetInterface.InsertToken(image) + insertToken.setBucket(bucket) + } + + @Before + fun setupHighLevelSimpleClient() { + whenever(highLevelSimpleClient.fetchContext).thenReturn(mock()) + whenever(highLevelSimpleClient.fetch(eq(uri), anyLong(), any(ClientGetCallback::class.java), any(FetchContext::class.java), anyShort())).thenReturn(clientGetter) + } + + @Before + fun setupSone() { + val insertSsk = createRandom(randomSource, "test-0") + whenever(sone.id).thenReturn(insertSsk.uri.routingKey.asFreenetBase64) + whenever(sone.requestUri).thenReturn(insertSsk.uri.uskForSSK()) + } + + @Before + fun setupCallbackCaptorAndUskManager() { + doNothing().whenever(uskManager).subscribe(any(USK::class.java), callbackCaptor.capture(), anyBoolean(), any(RequestClient::class.java)) + } + + @Test + fun `can fetch uri`() { + val freenetUri = FreenetURI("KSK@GPLv3.txt") + val fetchResult = createFetchResult() + whenever(highLevelSimpleClient.fetch(freenetUri)).thenReturn(fetchResult) + val fetched = freenetInterface.fetchUri(freenetUri) + assertThat(fetched, notNullValue()) + assertThat(fetched!!.fetchResult, equalTo(fetchResult)) + assertThat(fetched.freenetUri, equalTo(freenetUri)) + } + + @Test + fun `fetch follows redirect`() { + val freenetUri = FreenetURI("KSK@GPLv2.txt") + val newFreenetUri = FreenetURI("KSK@GPLv3.txt") + val fetchResult = createFetchResult() + val fetchException = FetchException(PERMANENT_REDIRECT, newFreenetUri) + whenever(highLevelSimpleClient.fetch(freenetUri)).thenThrow(fetchException) + whenever(highLevelSimpleClient.fetch(newFreenetUri)).thenReturn(fetchResult) + val fetched = freenetInterface.fetchUri(freenetUri) + assertThat(fetched!!.fetchResult, equalTo(fetchResult)) + assertThat(fetched.freenetUri, equalTo(newFreenetUri)) + } + + @Test + fun `fetch returns null on fetch exceptions`() { + val freenetUri = FreenetURI("KSK@GPLv2.txt") + val fetchException = FetchException(ALL_DATA_NOT_FOUND) + whenever(highLevelSimpleClient.fetch(freenetUri)).thenThrow(fetchException) + val fetched = freenetInterface.fetchUri(freenetUri) + assertThat(fetched, nullValue()) + } + + private fun createFetchResult(): FetchResult { + val clientMetadata = ClientMetadata("text/plain") + val bucket = ArrayBucket("Some Data.".toByteArray()) + return FetchResult(clientMetadata, bucket) + } + + @Test + fun `inserting an image`() { + val temporaryImage = TemporaryImage("image-id") + temporaryImage.mimeType = "image/png" + val imageData = byteArrayOf(1, 2, 3, 4) + temporaryImage.imageData = imageData + val image = ImageImpl("image-id") + val insertToken = freenetInterface.InsertToken(image) + val insertContext = mock() + whenever(highLevelSimpleClient.getInsertContext(anyBoolean())).thenReturn(insertContext) + val clientPutter = mock() + val insertBlockCaptor = forClass(InsertBlock::class.java) + whenever(highLevelSimpleClient.insert(insertBlockCaptor.capture(), eq(null as String?), eq(false), eq(insertContext), eq(insertToken), anyShort())).thenReturn(clientPutter) + freenetInterface.insertImage(temporaryImage, image, insertToken) + assertThat(insertBlockCaptor.value.data.inputStream, delivers(byteArrayOf(1, 2, 3, 4))) + assertThat(getPrivateField(insertToken, "clientPutter"), equalTo(clientPutter)) + verify(eventBus).post(any(ImageInsertStartedEvent::class.java)) + } + + @Test + fun `insert exception causes a sone exception`() { + val temporaryImage = TemporaryImage("image-id") + temporaryImage.mimeType = "image/png" + val imageData = byteArrayOf(1, 2, 3, 4) + temporaryImage.imageData = imageData + val image = ImageImpl("image-id") + val insertToken = freenetInterface.InsertToken(image) + val insertContext = mock() + whenever(highLevelSimpleClient.getInsertContext(anyBoolean())).thenReturn(insertContext) + val insertBlockCaptor = forClass(InsertBlock::class.java) + whenever(highLevelSimpleClient.insert(insertBlockCaptor.capture(), eq(null as String?), eq(false), eq(insertContext), eq(insertToken), anyShort())).thenThrow(InsertException::class.java) + expectionException.expect(SoneInsertException::class.java) + freenetInterface.insertImage(temporaryImage, image, insertToken) + } + + @Test + fun `inserting a directory`() { + val freenetUri = mock() + val manifestEntries = HashMap() + val defaultFile = "index.html" + val resultingUri = mock() + whenever(highLevelSimpleClient.insertManifest(eq(freenetUri), eq(manifestEntries), eq(defaultFile))).thenReturn(resultingUri) + assertThat(freenetInterface.insertDirectory(freenetUri, manifestEntries, defaultFile), equalTo(resultingUri)) + } + + @Test + fun `insert exception is forwarded as sone exception`() { + whenever(highLevelSimpleClient.insertManifest(any(), any(), any())).thenThrow(InsertException::class.java) + expectionException.expect(SoneException::class.java) + freenetInterface.insertDirectory(null, null, null) + } + + @Test + fun `sone with wrong request uri will not be subscribed`() { + whenever(sone.requestUri).thenReturn(FreenetURI("KSK@GPLv3.txt")) + freenetInterface.registerUsk(FreenetURI("KSK@GPLv3.txt"), null) + verify(uskManager, never()).subscribe(any(USK::class.java), any(USKCallback::class.java), anyBoolean(), any(RequestClient::class.java)) + } + + @Test + fun `registering a usk`() { + val freenetUri = createRandom(randomSource, "test-0").uri.uskForSSK() + val callback = mock() + freenetInterface.registerUsk(freenetUri, callback) + verify(uskManager).subscribe(any(USK::class.java), any(USKCallback::class.java), anyBoolean(), any(RequestClient::class.java)) + } + + @Test + fun `registering a non-usk key will not be subscribed`() { + val freenetUri = FreenetURI("KSK@GPLv3.txt") + val callback = mock() + freenetInterface.registerUsk(freenetUri, callback) + verify(uskManager, never()).subscribe(any(USK::class.java), any(USKCallback::class.java), anyBoolean(), any(RequestClient::class.java)) + } + + @Test + fun `registering an active usk will subscribe to it correctly`() { + val freenetUri = createRandom(randomSource, "test-0").uri.uskForSSK() + val uskCallback = mock() + freenetInterface.registerActiveUsk(freenetUri, uskCallback) + verify(uskManager).subscribe(any(USK::class.java), eq(uskCallback), eq(true), any(RequestClient::class.java)) + } + + @Test + fun `registering an inactive usk will subscribe to it correctly`() { + val freenetUri = createRandom(randomSource, "test-0").uri.uskForSSK() + val uskCallback = mock() + freenetInterface.registerPassiveUsk(freenetUri, uskCallback) + verify(uskManager).subscribe(any(USK::class.java), eq(uskCallback), eq(false), any(RequestClient::class.java)) + } + + @Test + fun `registering an active non-usk will not subscribe to a usk`() { + val freenetUri = createRandom(randomSource, "test-0").uri + freenetInterface.registerActiveUsk(freenetUri, null) + verify(uskManager, never()).subscribe(any(USK::class.java), any(USKCallback::class.java), anyBoolean(), any(RequestClient::class.java)) + } + + @Test + fun `registering an inactive non-usk will not subscribe to a usk`() { + val freenetUri = createRandom(randomSource, "test-0").uri + freenetInterface.registerPassiveUsk(freenetUri, null) + verify(uskManager, never()).subscribe(any(USK::class.java), any(USKCallback::class.java), anyBoolean(), any(RequestClient::class.java)) + } + + @Test + fun `unregistering a not registered usk does nothing`() { + val freenetURI = createRandom(randomSource, "test-0").uri.uskForSSK() + freenetInterface.unregisterUsk(freenetURI) + verify(uskManager, never()).unsubscribe(any(USK::class.java), any(USKCallback::class.java)) + } + + @Test + fun `unregistering a registered usk`() { + val freenetURI = createRandom(randomSource, "test-0").uri.uskForSSK() + val callback = mock() + freenetInterface.registerUsk(freenetURI, callback) + freenetInterface.unregisterUsk(freenetURI) + verify(uskManager).unsubscribe(any(USK::class.java), any(USKCallback::class.java)) + } + + @Test + fun `unregistering a not registered sone does nothing`() { + freenetInterface.unregisterUsk(sone) + verify(uskManager, never()).unsubscribe(any(USK::class.java), any(USKCallback::class.java)) + } + + @Test + fun `unregistering aregistered sone unregisters the sone`() { + freenetInterface.registerActiveUsk(sone.requestUri, mock()) + freenetInterface.unregisterUsk(sone) + verify(uskManager).unsubscribe(any(USK::class.java), any(USKCallback::class.java)) + } + + @Test + fun `unregistering asone with awrong request key will not unsubscribe`() { + whenever(sone.requestUri).thenReturn(FreenetURI("KSK@GPLv3.txt")) + freenetInterface.registerUsk(sone.requestUri, null) + freenetInterface.unregisterUsk(sone) + verify(uskManager, never()).unsubscribe(any(USK::class.java), any(USKCallback::class.java)) + } + + @Test + fun `callback for normal usk uses different priorities`() { + val callback = mock() + val soneUri = createRandom(randomSource, "test-0").uri.uskForSSK() + freenetInterface.registerUsk(soneUri, callback) + assertThat(callbackCaptor.value.pollingPriorityNormal, equalTo(PREFETCH_PRIORITY_CLASS)) + assertThat(callbackCaptor.value.pollingPriorityProgress, equalTo(INTERACTIVE_PRIORITY_CLASS)) + } + + @Test + fun `callback for normal usk forwards important parameters`() { + val callback = mock() + val uri = createRandom(randomSource, "test-0").uri.uskForSSK() + freenetInterface.registerUsk(uri, callback) + val key = mock() + whenever(key.uri).thenReturn(uri) + callbackCaptor.value.onFoundEdition(3, key, null, false, 0.toShort(), null, true, true) + verify(callback).editionFound(eq(uri), eq(3L), eq(true), eq(true)) + } + + @Test + fun `fetched retains uri and fetch result`() { + val freenetUri = mock() + val fetchResult = mock() + val (freenetUri1, fetchResult1) = Fetched(freenetUri, fetchResult) + assertThat(freenetUri1, equalTo(freenetUri)) + assertThat(fetchResult1, equalTo(fetchResult)) + } + + @Test + fun `cancelling an insert will fire image insert aborted event`() { + val clientPutter = mock() + insertToken.setClientPutter(clientPutter) + val imageInsertStartedEvent = forClass(ImageInsertStartedEvent::class.java) + verify(eventBus).post(imageInsertStartedEvent.capture()) + assertThat(imageInsertStartedEvent.value.image, equalTo(image)) + insertToken.cancel() + val imageInsertAbortedEvent = forClass(ImageInsertAbortedEvent::class.java) + verify(eventBus, times(2)).post(imageInsertAbortedEvent.capture()) + verify(bucket).free() + assertThat(imageInsertAbortedEvent.value.image, equalTo(image)) + } + + @Test + fun `failure without exception sends failed event`() { + val insertException = InsertException(mock()) + insertToken.onFailure(insertException, null) + val imageInsertFailedEvent = forClass(ImageInsertFailedEvent::class.java) + verify(eventBus).post(imageInsertFailedEvent.capture()) + verify(bucket).free() + assertThat(imageInsertFailedEvent.value.image, equalTo(image)) + assertThat(imageInsertFailedEvent.value.cause, equalTo(insertException)) + } + + @Test + fun `failure sends failed event with exception`() { + val insertException = InsertException(InsertExceptionMode.INTERNAL_ERROR, "Internal error", null) + insertToken.onFailure(insertException, null) + val imageInsertFailedEvent = forClass(ImageInsertFailedEvent::class.java) + verify(eventBus).post(imageInsertFailedEvent.capture()) + verify(bucket).free() + assertThat(imageInsertFailedEvent.value.image, equalTo(image)) + assertThat(imageInsertFailedEvent.value.cause, equalTo(insertException as Throwable)) + } + + @Test + fun `failure because cancelled by user sends aborted event`() { + val insertException = InsertException(InsertExceptionMode.CANCELLED, null) + insertToken.onFailure(insertException, null) + val imageInsertAbortedEvent = forClass(ImageInsertAbortedEvent::class.java) + verify(eventBus).post(imageInsertAbortedEvent.capture()) + verify(bucket).free() + assertThat(imageInsertAbortedEvent.value.image, equalTo(image)) + } + + @Test + fun `ignored methods do not throw exceptions`() { + insertToken.onResume(null) + insertToken.onFetchable(null) + insertToken.onGeneratedMetadata(null, null) + } + + @Test + fun `generated uri is posted on success`() { + val generatedUri = mock() + insertToken.onGeneratedURI(generatedUri, null) + insertToken.onSuccess(null) + val imageInsertFinishedEvent = forClass(ImageInsertFinishedEvent::class.java) + verify(eventBus).post(imageInsertFinishedEvent.capture()) + verify(bucket).free() + assertThat(imageInsertFinishedEvent.value.image, equalTo(image)) + assertThat(imageInsertFinishedEvent.value.resultingUri, equalTo(generatedUri)) + } + + @Test + fun `insert token supplier supplies insert tokens`() { + val insertTokenSupplier = InsertTokenSupplier(freenetInterface) + assertThat(insertTokenSupplier.apply(image), notNullValue()) + } + + @Test + fun `background fetch can be started`() { + freenetInterface.startFetch(uri, backgroundFetchCallback) + verify(highLevelSimpleClient).fetch(eq(uri), anyLong(), any(ClientGetCallback::class.java), any(FetchContext::class.java), anyShort()) + } + + @Test + fun `background fetch registers snoop and restarts the request`() { + freenetInterface.startFetch(uri, backgroundFetchCallback) + verify(clientGetter).metaSnoop = any(SnoopMetadata::class.java) + verify(clientGetter).restart(eq(uri), anyBoolean(), any(ClientContext::class.java)) + } + + @Test + fun `request is not cancelled for image mime type`() { + verifySnoopCancelsRequestForMimeType("image/png", false) + verify(backgroundFetchCallback, never()).failed(uri) + } + + @Test + fun `request is cancelled for null mime type`() { + verifySnoopCancelsRequestForMimeType(null, true) + verify(backgroundFetchCallback, never()).shouldCancel(eq(uri), any(), anyLong()) + verify(backgroundFetchCallback).failed(uri) + } + + @Test + fun `request is cancelled for video mime type`() { + verifySnoopCancelsRequestForMimeType("video/mkv", true) + verify(backgroundFetchCallback).failed(uri) + } + + @Test + fun `request is cancelled for audio mime type`() { + verifySnoopCancelsRequestForMimeType("audio/mpeg", true) + verify(backgroundFetchCallback).failed(uri) + } + + @Test + fun `request is cancelled for text mime type`() { + verifySnoopCancelsRequestForMimeType("text/plain", true) + verify(backgroundFetchCallback).failed(uri) + } + + private fun verifySnoopCancelsRequestForMimeType(mimeType: String?, cancel: Boolean) { + whenever(backgroundFetchCallback.shouldCancel(eq(uri), if (mimeType != null) eq(mimeType) else isNull(), anyLong())).thenReturn(cancel) + freenetInterface.startFetch(uri, backgroundFetchCallback) + val snoopMetadata = forClass(SnoopMetadata::class.java) + verify(clientGetter).metaSnoop = snoopMetadata.capture() + val metadata = mock() + whenever(metadata.mimeType).thenReturn(mimeType) + assertThat(snoopMetadata.value.snoopMetadata(metadata, mock()), equalTo(cancel)) + } + + @Test + fun `callback of background fetch is notified on success`() { + freenetInterface.startFetch(uri, backgroundFetchCallback) + verify(highLevelSimpleClient).fetch(eq(uri), anyLong(), clientGetCallback.capture(), any(FetchContext::class.java), anyShort()) + whenever(fetchResult.mimeType).thenReturn("image/png") + whenever(fetchResult.asByteArray()).thenReturn(byteArrayOf(1, 2, 3, 4, 5)) + clientGetCallback.value.onSuccess(fetchResult, mock()) + verify(backgroundFetchCallback).loaded(uri, "image/png", byteArrayOf(1, 2, 3, 4, 5)) + verifyNoMoreInteractions(backgroundFetchCallback) + } + + @Test + fun `callback of background fetch is notified on failure`() { + freenetInterface.startFetch(uri, backgroundFetchCallback) + verify(highLevelSimpleClient).fetch(eq(uri), anyLong(), clientGetCallback.capture(), any(FetchContext::class.java), anyShort()) + whenever(fetchResult.mimeType).thenReturn("image/png") + whenever(fetchResult.asByteArray()).thenReturn(byteArrayOf(1, 2, 3, 4, 5)) + clientGetCallback.value.onFailure(FetchException(ALL_DATA_NOT_FOUND), mock()) + verify(backgroundFetchCallback).failed(uri) + verifyNoMoreInteractions(backgroundFetchCallback) + } + + @Test + fun `callback of background fetch is notified as failure if bucket can not be loaded`() { + freenetInterface.startFetch(uri, backgroundFetchCallback) + verify(highLevelSimpleClient).fetch(eq(uri), anyLong(), clientGetCallback.capture(), any(FetchContext::class.java), anyShort()) + whenever(fetchResult.mimeType).thenReturn("image/png") + whenever(fetchResult.asByteArray()).thenThrow(IOException::class.java) + clientGetCallback.value.onSuccess(fetchResult, mock()) + verify(backgroundFetchCallback).failed(uri) + verifyNoMoreInteractions(backgroundFetchCallback) + } + + @Test + fun `unregistering a registered USK with different edition unregisters USK`() { + val callback = mock() + val uri = createRandom(randomSource, "test-123").uri.uskForSSK() + freenetInterface.registerUsk(uri, callback) + freenetInterface.unregisterUsk(uri.setSuggestedEdition(234)) + verify(uskManager).unsubscribe(any(), any()) + } + +} diff --git a/src/test/kotlin/net/pterodactylus/sone/core/ImageInserterTest.kt b/src/test/kotlin/net/pterodactylus/sone/core/ImageInserterTest.kt index 39a11dc..60bb7e2 100644 --- a/src/test/kotlin/net/pterodactylus/sone/core/ImageInserterTest.kt +++ b/src/test/kotlin/net/pterodactylus/sone/core/ImageInserterTest.kt @@ -37,7 +37,7 @@ class ImageInserterTest { @Test fun `exception when inserting image is ignored`() { - doThrow(SoneException::class.java).`when`(freenetInterface).insertImage(eq(temporaryImage), eq(image), any(InsertToken::class.java)) + doThrow(SoneException::class.java).whenever(freenetInterface).insertImage(eq(temporaryImage), eq(image), any(InsertToken::class.java)) imageInserter.insertImage(temporaryImage, image) verify(freenetInterface).insertImage(eq(temporaryImage), eq(image), any(InsertToken::class.java)) } diff --git a/src/test/kotlin/net/pterodactylus/sone/core/PreferencesLoaderTest.kt b/src/test/kotlin/net/pterodactylus/sone/core/PreferencesLoaderTest.kt new file mode 100644 index 0000000..0cd7e6d --- /dev/null +++ b/src/test/kotlin/net/pterodactylus/sone/core/PreferencesLoaderTest.kt @@ -0,0 +1,61 @@ +package net.pterodactylus.sone.core + +import com.google.common.eventbus.* +import net.pterodactylus.sone.fcp.FcpInterface.* +import net.pterodactylus.util.config.* +import org.hamcrest.MatcherAssert.* +import org.hamcrest.Matchers.* +import org.junit.* + +/** + * Unit test for [PreferencesLoader]. + */ +class PreferencesLoaderTest { + + @Suppress("UnstableApiUsage") + private val eventBus = EventBus() + private val preferences = Preferences(eventBus) + private val configuration = Configuration(MapConfigurationBackend()) + private val preferencesLoader = PreferencesLoader(preferences) + + @Before + fun setupConfiguration() { + setupIntValue("InsertionDelay", 15) + setupIntValue("PostsPerPage", 25) + setupIntValue("ImagesPerPage", 12) + setupIntValue("CharactersPerPost", 150) + setupIntValue("PostCutOffLength", 300) + setupBooleanValue("RequireFullAccess", true) + setupBooleanValue("ActivateFcpInterface", true) + setupIntValue("FcpFullAccessRequired", 1) + } + + private fun setupIntValue(optionName: String, value: Int) { + configuration.getIntValue("Option/$optionName").value = value + } + + private fun setupBooleanValue(optionName: String, value: Boolean) { + configuration.getBooleanValue("Option/$optionName").value = value + } + + @Test + fun `configuration is loaded correctly`() { + preferencesLoader.loadFrom(configuration) + assertThat(preferences.insertionDelay, equalTo(15)) + assertThat(preferences.postsPerPage, equalTo(25)) + assertThat(preferences.imagesPerPage, equalTo(12)) + assertThat(preferences.charactersPerPost, equalTo(150)) + assertThat(preferences.postCutOffLength, equalTo(300)) + assertThat(preferences.requireFullAccess, equalTo(true)) + assertThat(preferences.fcpInterfaceActive, equalTo(true)) + assertThat(preferences.fcpFullAccessRequired, equalTo(FullAccessRequired.WRITING)) + } + + @Test + fun `configuration is loaded correctly with cut off length minus one`() { + setupIntValue("PostCutOffLength", -1) + preferencesLoader.loadFrom(configuration) + assertThat(preferences.postCutOffLength, not(equalTo(-1))) + } + +} diff --git a/src/test/kotlin/net/pterodactylus/sone/core/PreferencesTest.kt b/src/test/kotlin/net/pterodactylus/sone/core/PreferencesTest.kt index 199604b..245ff03 100644 --- a/src/test/kotlin/net/pterodactylus/sone/core/PreferencesTest.kt +++ b/src/test/kotlin/net/pterodactylus/sone/core/PreferencesTest.kt @@ -6,6 +6,7 @@ import net.pterodactylus.sone.fcp.FcpInterface.* import net.pterodactylus.sone.fcp.FcpInterface.FullAccessRequired.* import net.pterodactylus.sone.fcp.event.* import net.pterodactylus.sone.test.* +import net.pterodactylus.util.config.* import org.hamcrest.MatcherAssert.* import org.hamcrest.Matchers.* import org.junit.* @@ -64,6 +65,13 @@ class PreferencesTest { } @Test + fun `preferences saves null for default insertion delay setting`() { + val configuration = Configuration(MapConfigurationBackend()) + preferences.saveTo(configuration) + assertThat(configuration.getIntValue("Option/InsertionDelay").getValue(null), nullValue()) + } + + @Test fun `preferences retain posts per page`() { preferences.newPostsPerPage = 15 assertThat(preferences.postsPerPage, equalTo(15)) @@ -180,69 +188,6 @@ class PreferencesTest { } @Test - fun `preferences retain positive trust`() { - preferences.newPositiveTrust = 15 - assertThat(preferences.positiveTrust, equalTo(15)) - } - - @Test(expected = IllegalArgumentException::class) - fun `invalid positive trust is rejected`() { - preferences.newPositiveTrust = -15 - } - - @Test - fun `preferences return default value when positive trust is set to null`() { - preferences.newPositiveTrust = null - assertThat(preferences.positiveTrust, equalTo(75)) - } - - @Test - fun `preferences start with positive trust default value`() { - assertThat(preferences.positiveTrust, equalTo(75)) - } - - @Test - fun `preferences retain negative trust`() { - preferences.newNegativeTrust = -15 - assertThat(preferences.negativeTrust, equalTo(-15)) - } - - @Test(expected = IllegalArgumentException::class) - fun `invalid negative trust is rejected`() { - preferences.newNegativeTrust = 150 - } - - @Test - fun `preferences return default value when negative trust is set to null`() { - preferences.newNegativeTrust = null - assertThat(preferences.negativeTrust, equalTo(-25)) - } - - @Test - fun `preferences start with negative trust default value`() { - assertThat(preferences.negativeTrust, equalTo(-25)) - } - - @Test - fun `preferences retain trust comment`() { - preferences.newTrustComment = "Trust" - assertThat(preferences.trustComment, equalTo("Trust")) - } - - @Test - fun `preferences return default value when trust comment is set to null`() { - preferences.newTrustComment = null - assertThat(preferences.trustComment, - equalTo("Set from Sone Web Interface")) - } - - @Test - fun `preferences start with trust comment default value`() { - assertThat(preferences.trustComment, - equalTo("Set from Sone Web Interface")) - } - - @Test fun `preferences retain fcp interface active of true`() { preferences.newFcpInterfaceActive = true assertThat(preferences.fcpInterfaceActive, equalTo(true)) diff --git a/src/test/kotlin/net/pterodactylus/sone/core/SoneInserterTest.kt b/src/test/kotlin/net/pterodactylus/sone/core/SoneInserterTest.kt new file mode 100644 index 0000000..2f90250 --- /dev/null +++ b/src/test/kotlin/net/pterodactylus/sone/core/SoneInserterTest.kt @@ -0,0 +1,278 @@ +package net.pterodactylus.sone.core + +import com.codahale.metrics.* +import com.google.common.base.* +import com.google.common.base.Optional +import com.google.common.eventbus.* +import com.google.common.io.ByteStreams.* +import com.google.common.util.concurrent.MoreExecutors.* +import freenet.keys.* +import net.pterodactylus.sone.core.SoneInserter.* +import net.pterodactylus.sone.core.event.* +import net.pterodactylus.sone.data.* +import net.pterodactylus.sone.main.* +import net.pterodactylus.sone.test.* +import org.hamcrest.MatcherAssert.* +import org.hamcrest.Matchers.* +import org.junit.* +import org.mockito.* +import org.mockito.ArgumentMatchers.any +import org.mockito.ArgumentMatchers.anyString +import org.mockito.ArgumentMatchers.eq +import org.mockito.Mockito.doAnswer +import org.mockito.Mockito.never +import org.mockito.Mockito.times +import org.mockito.Mockito.verify +import org.mockito.hamcrest.MockitoHamcrest.* +import org.mockito.stubbing.* +import java.lang.System.* +import java.util.* +import kotlin.test.Test + +/** + * Unit test for [SoneInserter] and its subclasses. + */ +class SoneInserterTest { + + private val metricRegistry = MetricRegistry() + private val core = mock() + private val eventBus = mock() + private val freenetInterface = mock() + + @Before + fun setupCore() { + val updateChecker = mock() + whenever(core.updateChecker).thenReturn(updateChecker) + whenever(core.getSone(anyString())).thenReturn(null) + } + + @Test + fun `insertion delay is forwarded to sone inserter`() { + val eventBus = AsyncEventBus(directExecutor()) + eventBus.register(SoneInserter(core, eventBus, freenetInterface, metricRegistry, "SoneId")) + eventBus.post(InsertionDelayChangedEvent(15)) + assertThat(SoneInserter.getInsertionDelay().get(), equalTo(15)) + } + + private fun createSone(insertUri: FreenetURI, fingerprint: String = "fingerprint"): Sone { + val sone = mock() + whenever(sone.insertUri).thenReturn(insertUri) + whenever(sone.fingerprint).thenReturn(fingerprint) + whenever(sone.rootAlbum).thenReturn(mock()) + whenever(core.getSone(anyString())).thenReturn(sone) + return sone + } + + @Test + fun `isModified is true if modification detector says so`() { + val soneModificationDetector = mock() + whenever(soneModificationDetector.isModified).thenReturn(true) + val soneInserter = SoneInserter(core, eventBus, freenetInterface, metricRegistry, "SoneId", soneModificationDetector, 1) + assertThat(soneInserter.isModified, equalTo(true)) + } + + @Test + fun `isModified is false if modification detector says so`() { + val soneModificationDetector = mock() + val soneInserter = SoneInserter(core, eventBus, freenetInterface, metricRegistry, "SoneId", soneModificationDetector, 1) + assertThat(soneInserter.isModified, equalTo(false)) + } + + @Test + fun `last fingerprint is stored correctly`() { + val soneInserter = SoneInserter(core, eventBus, freenetInterface, metricRegistry, "SoneId") + soneInserter.lastInsertFingerprint = "last-fingerprint" + assertThat(soneInserter.lastInsertFingerprint, equalTo("last-fingerprint")) + } + + @Test + fun `sone inserter stops when it should`() { + val soneInserter = SoneInserter(core, eventBus, freenetInterface, metricRegistry, "SoneId") + soneInserter.stop() + soneInserter.serviceRun() + } + + @Test + fun `sone inserter inserts a sone if it is eligible`() { + val insertUri = mock() + val finalUri = mock() + val sone = createSone(insertUri) + val soneModificationDetector = mock() + whenever(soneModificationDetector.isEligibleForInsert).thenReturn(true) + whenever(freenetInterface.insertDirectory(eq(insertUri), any>(), eq("index.html"))).thenReturn(finalUri) + val soneInserter = SoneInserter(core, eventBus, freenetInterface, metricRegistry, "SoneId", soneModificationDetector, 1) + doAnswer { + soneInserter.stop() + null + }.whenever(core).touchConfiguration() + soneInserter.serviceRun() + val soneEvents = ArgumentCaptor.forClass(SoneEvent::class.java) + verify(freenetInterface).insertDirectory(eq(insertUri), any>(), eq("index.html")) + verify(eventBus, times(2)).post(soneEvents.capture()) + assertThat(soneEvents.allValues[0], instanceOf(SoneInsertingEvent::class.java)) + assertThat(soneEvents.allValues[0].sone, equalTo(sone)) + assertThat(soneEvents.allValues[1], instanceOf(SoneInsertedEvent::class.java)) + assertThat(soneEvents.allValues[1].sone, equalTo(sone)) + } + + @Test + fun `sone inserter bails out if it is stopped while inserting`() { + val insertUri = mock() + val finalUri = mock() + val sone = createSone(insertUri) + val soneModificationDetector = mock() + whenever(soneModificationDetector.isEligibleForInsert).thenReturn(true) + val soneInserter = SoneInserter(core, eventBus, freenetInterface, metricRegistry, "SoneId", soneModificationDetector, 1) + whenever(freenetInterface.insertDirectory(eq(insertUri), any>(), eq("index.html"))).thenAnswer { + soneInserter.stop() + finalUri + } + soneInserter.serviceRun() + val soneEvents = ArgumentCaptor.forClass(SoneEvent::class.java) + verify(freenetInterface).insertDirectory(eq(insertUri), any>(), eq("index.html")) + verify(eventBus, times(2)).post(soneEvents.capture()) + assertThat(soneEvents.allValues[0], instanceOf(SoneInsertingEvent::class.java)) + assertThat(soneEvents.allValues[0].sone, equalTo(sone)) + assertThat(soneEvents.allValues[1], instanceOf(SoneInsertedEvent::class.java)) + assertThat(soneEvents.allValues[1].sone, equalTo(sone)) + verify(core, never()).touchConfiguration() + } + + @Test + fun `sone inserter does not insert sone if it is not eligible`() { + val insertUri = mock() + createSone(insertUri) + val soneModificationDetector = mock() + val soneInserter = SoneInserter(core, eventBus, freenetInterface, metricRegistry, "SoneId", soneModificationDetector, 1) + Thread(Runnable { + try { + Thread.sleep(500) + } catch (ie1: InterruptedException) { + throw RuntimeException(ie1) + } + + soneInserter.stop() + }).start() + soneInserter.serviceRun() + verify(freenetInterface, never()).insertDirectory(eq(insertUri), any>(), eq("index.html")) + verify(eventBus, never()).post(argThat(org.hamcrest.Matchers.any(SoneEvent::class.java))) + } + + @Test + fun `sone inserter posts aborted event if an exception occurs`() { + val insertUri = mock() + val sone = createSone(insertUri) + val soneModificationDetector = mock() + whenever(soneModificationDetector.isEligibleForInsert).thenReturn(true) + val soneInserter = SoneInserter(core, eventBus, freenetInterface, metricRegistry, "SoneId", soneModificationDetector, 1) + val soneException = SoneException(Exception()) + whenever(freenetInterface.insertDirectory(eq(insertUri), any>(), eq("index.html"))).thenAnswer { + soneInserter.stop() + throw soneException + } + soneInserter.serviceRun() + val soneEvents = ArgumentCaptor.forClass(SoneEvent::class.java) + verify(freenetInterface).insertDirectory(eq(insertUri), any>(), eq("index.html")) + verify(eventBus, times(2)).post(soneEvents.capture()) + assertThat(soneEvents.allValues[0], instanceOf(SoneInsertingEvent::class.java)) + assertThat(soneEvents.allValues[0].sone, equalTo(sone)) + assertThat(soneEvents.allValues[1], instanceOf(SoneInsertAbortedEvent::class.java)) + assertThat(soneEvents.allValues[1].sone, equalTo(sone)) + verify(core, never()).touchConfiguration() + } + + @Test + fun `sone inserter exits if sone is unknown`() { + val soneModificationDetector = mock() + val soneInserter = SoneInserter(core, eventBus, freenetInterface, metricRegistry, "SoneId", soneModificationDetector, 1) + whenever(soneModificationDetector.isEligibleForInsert).thenReturn(true) + whenever(core.getSone("SoneId")).thenReturn(null) + soneInserter.serviceRun() + } + + @Test + fun `sone inserter catches exception and continues`() { + val soneModificationDetector = mock() + val soneInserter = SoneInserter(core, eventBus, freenetInterface, metricRegistry, "SoneId", soneModificationDetector, 1) + val stopInserterAndThrowException = Answer> { + soneInserter.stop() + throw NullPointerException() + } + whenever(soneModificationDetector.isEligibleForInsert).thenAnswer(stopInserterAndThrowException) + soneInserter.serviceRun() + } + + @Test + fun `template is rendered correctly for manifest element`() { + val soneProperties = HashMap() + soneProperties["id"] = "SoneId" + val manifestCreator = ManifestCreator(core, soneProperties) + val now = currentTimeMillis() + whenever(core.startupTime).thenReturn(now) + val manifestElement = manifestCreator.createManifestElement("test.txt", "plain/text; charset=utf-8", "sone-inserter-manifest.txt") + assertThat(manifestElement!!.name, equalTo("test.txt")) + assertThat(manifestElement.mimeTypeOverride, equalTo("plain/text; charset=utf-8")) + val templateContent = String(toByteArray(manifestElement.data.inputStream), Charsets.UTF_8) + assertThat(templateContent, containsString("Sone Version: ${SonePlugin.getPluginVersion()}\n")) + assertThat(templateContent, containsString("Core Startup: $now\n")) + assertThat(templateContent, containsString("Sone ID: SoneId\n")) + } + + @Test + fun `invalid template returns anull manifest element`() { + val soneProperties = HashMap() + val manifestCreator = ManifestCreator(core, soneProperties) + assertThat(manifestCreator.createManifestElement("test.txt", + "plain/text; charset=utf-8", + "sone-inserter-invalid-manifest.txt"), + nullValue()) + } + + @Test + fun `error while rendering template returns a null manifest element`() { + val soneProperties = HashMap() + val manifestCreator = ManifestCreator(core, soneProperties) + whenever(core.toString()).thenThrow(NullPointerException::class.java) + assertThat(manifestCreator.createManifestElement("test.txt", + "plain/text; charset=utf-8", + "sone-inserter-faulty-manifest.txt"), + nullValue()) + } + + @Test + fun `successful insert updates metrics`() { + val insertUri = mock() + val finalUri = mock() + createSone(insertUri) + val soneModificationDetector = mock() + whenever(soneModificationDetector.isEligibleForInsert).thenReturn(true) + whenever(freenetInterface.insertDirectory(eq(insertUri), any>(), eq("index.html"))).thenReturn(finalUri) + val soneInserter = SoneInserter(core, eventBus, freenetInterface, metricRegistry,"SoneId", soneModificationDetector, 1) + doAnswer { + soneInserter.stop() + null + }.whenever(core).touchConfiguration() + soneInserter.serviceRun() + val histogram = metricRegistry.histogram("sone.insert.duration") + assertThat(histogram.count, equalTo(1L)) + } + + @Test + fun `unsuccessful insert does not update histogram but records error`() { + val insertUri = mock() + createSone(insertUri) + val soneModificationDetector = mock() + whenever(soneModificationDetector.isEligibleForInsert).thenReturn(true) + val soneInserter = SoneInserter(core, eventBus, freenetInterface, metricRegistry, "SoneId", soneModificationDetector, 1) + whenever(freenetInterface.insertDirectory(eq(insertUri), any>(), eq("index.html"))).thenAnswer { + soneInserter.stop() + throw SoneException(Exception()) + } + soneInserter.serviceRun() + val histogram = metricRegistry.histogram("sone.insert.duration") + assertThat(histogram.count, equalTo(0L)) + val meter = metricRegistry.meter("sone.insert.errors") + assertThat(meter.count, equalTo(1L)) + } + +} diff --git a/src/test/kotlin/net/pterodactylus/sone/core/SoneParserTest.kt b/src/test/kotlin/net/pterodactylus/sone/core/SoneParserTest.kt new file mode 100644 index 0000000..336d855 --- /dev/null +++ b/src/test/kotlin/net/pterodactylus/sone/core/SoneParserTest.kt @@ -0,0 +1,416 @@ +package net.pterodactylus.sone.core + +import com.codahale.metrics.* +import com.google.common.base.Optional.* +import freenet.crypt.* +import freenet.keys.InsertableClientSSK.* +import net.pterodactylus.sone.data.* +import net.pterodactylus.sone.database.memory.* +import net.pterodactylus.sone.freenet.wot.* +import net.pterodactylus.sone.test.* +import net.pterodactylus.util.config.* +import org.hamcrest.MatcherAssert.* +import org.hamcrest.Matchers.* +import org.mockito.Mockito.* +import java.lang.System.* +import java.util.concurrent.TimeUnit.* +import kotlin.test.* + +/** + * Unit test for [SoneParser]. + */ +class SoneParserTest { + + private val database = MemoryDatabase(Configuration(MapConfigurationBackend())) + private val metricRegistry = MetricRegistry() + private val soneParser = SoneParser(database, metricRegistry) + private val sone = mock() + + @BeforeTest + fun setupSone() { + setupSone(this.sone, Identity::class.java) + database.storeSone(sone) + } + + private fun setupSone(sone: Sone, identityClass: Class) { + val identity = mock(identityClass) + val clientSSK = createRandom(DummyRandomSource(), "WoT") + whenever(identity.requestUri).thenReturn(clientSSK.uri.toString()) + whenever(identity.id).thenReturn("identity") + whenever(sone.id).thenReturn("identity") + whenever(sone.identity).thenReturn(identity) + whenever(sone.requestUri).thenAnswer { clientSSK.uri.setKeyType("USK").setDocName("Sone") } + whenever(sone.time).thenReturn(currentTimeMillis() - DAYS.toMillis(1)) + } + + @Test + fun `parsing a sone fails when document is not xml`() { + val inputStream = javaClass.getResourceAsStream("sone-parser-not-xml.xml") + assertThat(soneParser.parseSone(sone, inputStream), nullValue()) + } + + @Test + fun `parsing a sone fails when document has negative protocol version`() { + val inputStream = javaClass.getResourceAsStream("sone-parser-negative-protocol-version.xml") + assertThat(soneParser.parseSone(sone, inputStream), nullValue()) + } + + @Test + fun `parsing a sone fails when protocol version is too large`() { + val inputStream = javaClass.getResourceAsStream("sone-parser-too-large-protocol-version.xml") + assertThat(soneParser.parseSone(sone, inputStream), nullValue()) + } + + @Test + fun `parsing a sone fails when there is no time`() { + val inputStream = javaClass.getResourceAsStream("sone-parser-no-time.xml") + assertThat(soneParser.parseSone(sone, inputStream), nullValue()) + } + + @Test + fun `parsing a sone fails when time is not numeric`() { + val inputStream = javaClass.getResourceAsStream("sone-parser-time-not-numeric.xml") + assertThat(soneParser.parseSone(sone, inputStream), nullValue()) + } + + @Test + fun `parsing a sone fails when profile is missing`() { + val inputStream = javaClass.getResourceAsStream("sone-parser-no-profile.xml") + assertThat(soneParser.parseSone(sone, inputStream), nullValue()) + } + + @Test + fun `parsing a sone fails when profile field is missing afield name`() { + val inputStream = javaClass.getResourceAsStream("sone-parser-profile-missing-field-name.xml") + assertThat(soneParser.parseSone(sone, inputStream), nullValue()) + } + + @Test + fun `parsing a sone fails when profile field name is empty`() { + val inputStream = javaClass.getResourceAsStream("sone-parser-profile-empty-field-name.xml") + assertThat(soneParser.parseSone(sone, inputStream), nullValue()) + } + + @Test + fun `parsing a sone fails when profile field name is not unique`() { + val inputStream = javaClass.getResourceAsStream("sone-parser-profile-duplicate-field-name.xml") + assertThat(soneParser.parseSone(sone, inputStream), nullValue()) + } + + @Test + fun `parsing a sone succeeds without payload`() { + val inputStream = javaClass.getResourceAsStream("sone-parser-no-payload.xml") + assertThat(soneParser.parseSone(sone, inputStream)!!.time, equalTo(1407197508000L)) + } + + @Test + fun `parsing a local sone succeeds without payload`() { + val inputStream = javaClass.getResourceAsStream("sone-parser-no-payload.xml") + val localSone = mock() + setupSone(localSone, OwnIdentity::class.java) + whenever(localSone.isLocal).thenReturn(true) + val parsedSone = soneParser.parseSone(localSone, inputStream) + assertThat(parsedSone!!.time, equalTo(1407197508000L)) + assertThat(parsedSone.isLocal, equalTo(true)) + } + + @Test + fun `parsing a sone succeeds without protocol version`() { + val inputStream = javaClass.getResourceAsStream("sone-parser-missing-protocol-version.xml") + assertThat(soneParser.parseSone(sone, inputStream), notNullValue()) + } + + @Test + fun `parsing a sone fails with missing client name`() { + val inputStream = javaClass.getResourceAsStream("sone-parser-missing-client-name.xml") + assertThat(soneParser.parseSone(sone, inputStream), nullValue()) + } + + @Test + fun `parsing a sone fails with missing client version`() { + val inputStream = javaClass.getResourceAsStream("sone-parser-missing-client-version.xml") + assertThat(soneParser.parseSone(sone, inputStream), nullValue()) + } + + @Test + fun `parsing a sone succeeds with client info`() { + val inputStream = javaClass.getResourceAsStream("sone-parser-with-client-info.xml") + assertThat(soneParser.parseSone(sone, inputStream)!!.client, equalTo(Client("some-client", "some-version"))) + } + + @Test + fun `parsing a sone succeeds with profile`() { + val inputStream = javaClass.getResourceAsStream("sone-parser-with-profile.xml") + val profile = soneParser.parseSone(sone, inputStream)!!.profile + assertThat(profile.firstName, equalTo("first")) + assertThat(profile.middleName, equalTo("middle")) + assertThat(profile.lastName, equalTo("last")) + assertThat(profile.birthDay, equalTo(18)) + assertThat(profile.birthMonth, equalTo(12)) + assertThat(profile.birthYear, equalTo(1976)) + } + + @Test + fun `parsing a sone succeeds without profile fields`() { + val inputStream = javaClass.getResourceAsStream("sone-parser-without-fields.xml") + assertThat(soneParser.parseSone(sone, inputStream), notNullValue()) + } + + @Test + fun `parsing a sone fails without post id`() { + val inputStream = javaClass.getResourceAsStream("sone-parser-without-post-id.xml") + assertThat(soneParser.parseSone(sone, inputStream), nullValue()) + } + + @Test + fun `parsing a sone fails without post time`() { + val inputStream = javaClass.getResourceAsStream("sone-parser-without-post-time.xml") + assertThat(soneParser.parseSone(sone, inputStream), nullValue()) + } + + @Test + fun `parsing a sone fails without post text`() { + val inputStream = javaClass.getResourceAsStream("sone-parser-without-post-text.xml") + assertThat(soneParser.parseSone(sone, inputStream), nullValue()) + } + + @Test + fun `parsing a sone fails with invalid post time`() { + val inputStream = javaClass.getResourceAsStream("sone-parser-with-invalid-post-time.xml") + assertThat(soneParser.parseSone(sone, inputStream), nullValue()) + } + + @Test + fun `parsing a sone succeeds with valid post time`() { + val inputStream = javaClass.getResourceAsStream("sone-parser-with-valid-post-time.xml") + val posts = soneParser.parseSone(sone, inputStream)!!.posts + assertThat(posts, hasSize(1)) + assertThat(posts[0].sone.id, equalTo(sone.id)) + assertThat(posts[0].id, equalTo("3de12680-afef-11e9-a124-e713cf8912fe")) + assertThat(posts[0].time, equalTo(1407197508000L)) + assertThat(posts[0].recipientId, equalTo(absent())) + assertThat(posts[0].text, equalTo("text")) + } + + @Test + fun `parsing a sone succeeds with recipient`() { + val inputStream = javaClass.getResourceAsStream("sone-parser-with-recipient.xml") + val posts = soneParser.parseSone(sone, inputStream)!!.posts + assertThat(posts, hasSize(1)) + assertThat(posts[0].sone.id, equalTo(sone.id)) + assertThat(posts[0].id, equalTo("3de12680-afef-11e9-a124-e713cf8912fe")) + assertThat(posts[0].time, equalTo(1407197508000L)) + assertThat(posts[0].recipientId, equalTo(of("1234567890123456789012345678901234567890123"))) + assertThat(posts[0].text, equalTo("text")) + } + + @Test + fun `parsing a sone succeeds with invalid recipient`() { + val inputStream = javaClass.getResourceAsStream("sone-parser-with-invalid-recipient.xml") + val posts = soneParser.parseSone(sone, inputStream)!!.posts + assertThat(posts, hasSize(1)) + assertThat(posts[0].sone.id, equalTo(sone.id)) + assertThat(posts[0].id, equalTo("3de12680-afef-11e9-a124-e713cf8912fe")) + assertThat(posts[0].time, equalTo(1407197508000L)) + assertThat(posts[0].recipientId, equalTo(absent())) + assertThat(posts[0].text, equalTo("text")) + } + + @Test + fun `parsing a sone fails without post reply id`() { + val inputStream = javaClass.getResourceAsStream("sone-parser-without-post-reply-id.xml") + assertThat(soneParser.parseSone(sone, inputStream), nullValue()) + } + + @Test + fun `parsing a sone fails without post reply post id`() { + val inputStream = javaClass.getResourceAsStream("sone-parser-without-post-reply-post-id.xml") + assertThat(soneParser.parseSone(sone, inputStream), nullValue()) + } + + @Test + fun `parsing a sone fails without post reply time`() { + val inputStream = javaClass.getResourceAsStream("sone-parser-without-post-reply-time.xml") + assertThat(soneParser.parseSone(sone, inputStream), nullValue()) + } + + @Test + fun `parsing a sone fails without post reply text`() { + val inputStream = javaClass.getResourceAsStream("sone-parser-without-post-reply-text.xml") + assertThat(soneParser.parseSone(sone, inputStream), nullValue()) + } + + @Test + fun `parsing a sone fails with invalid post reply time`() { + val inputStream = javaClass.getResourceAsStream("sone-parser-with-invalid-post-reply-time.xml") + assertThat(soneParser.parseSone(sone, inputStream), nullValue()) + } + + @Test + fun `parsing a sone succeeds with valid post reply time`() { + val inputStream = javaClass.getResourceAsStream("sone-parser-with-valid-post-reply-time.xml") + val postReplies = soneParser.parseSone(sone, inputStream)!!.replies + assertThat(postReplies, hasSize(1)) + val postReply = postReplies.first() + assertThat(postReply.id, equalTo("5ccba7f4-aff0-11e9-b176-a7b9db60ce98")) + assertThat(postReply.postId, equalTo("3de12680-afef-11e9-a124-e713cf8912fe")) + assertThat(postReply.sone.id, equalTo("identity")) + assertThat(postReply.time, equalTo(1407197508000L)) + assertThat(postReply.text, equalTo("reply-text")) + } + + @Test + fun `parsing a sone succeeds without liked post ids`() { + val inputStream = javaClass.getResourceAsStream("sone-parser-without-liked-post-ids.xml") + assertThat(soneParser.parseSone(sone, inputStream), notNullValue()) + } + + @Test + fun `parsing a sone succeeds with liked post ids`() { + val inputStream = javaClass.getResourceAsStream("sone-parser-with-liked-post-ids.xml") + assertThat(soneParser.parseSone(sone, inputStream)!!.likedPostIds, equalTo(setOf("liked-post-id"))) + } + + @Test + fun `parsing a sone succeeds without liked post reply ids`() { + val inputStream = javaClass.getResourceAsStream("sone-parser-without-liked-post-reply-ids.xml") + assertThat(soneParser.parseSone(sone, inputStream), notNullValue()) + } + + @Test + fun `parsing a sone succeeds with liked post reply ids`() { + val inputStream = javaClass.getResourceAsStream("sone-parser-with-liked-post-reply-ids.xml") + assertThat(soneParser.parseSone(sone, inputStream)!!.likedReplyIds, equalTo(setOf("liked-post-reply-id"))) + } + + @Test + fun `parsing a sone succeeds without albums`() { + val inputStream = javaClass.getResourceAsStream("sone-parser-without-albums.xml") + assertThat(soneParser.parseSone(sone, inputStream), notNullValue()) + } + + @Test + fun `parsing a sone fails without album id`() { + val inputStream = javaClass.getResourceAsStream("sone-parser-without-album-id.xml") + assertThat(soneParser.parseSone(sone, inputStream), nullValue()) + } + + @Test + fun `parsing a sone fails without album title`() { + val inputStream = javaClass.getResourceAsStream("sone-parser-without-album-title.xml") + assertThat(soneParser.parseSone(sone, inputStream), nullValue()) + } + + @Test + fun `parsing a sone succeeds with nested albums`() { + val inputStream = javaClass.getResourceAsStream("sone-parser-with-multiple-albums.xml") + val parsedSone = soneParser.parseSone(sone, inputStream) + assertThat(parsedSone, notNullValue()) + assertThat(parsedSone!!.rootAlbum.albums, hasSize(1)) + val album = parsedSone.rootAlbum.albums[0] + assertThat(album.id, equalTo("album-id-1")) + assertThat(album.title, equalTo("album-title")) + assertThat(album.description, equalTo("album-description")) + assertThat(album.albums, hasSize(1)) + val nestedAlbum = album.albums[0] + assertThat(nestedAlbum.id, equalTo("album-id-2")) + assertThat(nestedAlbum.title, equalTo("album-title-2")) + assertThat(nestedAlbum.description, equalTo("album-description-2")) + assertThat(nestedAlbum.albums, hasSize(0)) + } + + @Test + fun `parsing a sone fails with invalid parent album id`() { + val inputStream = javaClass.getResourceAsStream("sone-parser-with-invalid-parent-album-id.xml") + assertThat(soneParser.parseSone(sone, inputStream), nullValue()) + } + + @Test + fun `parsing a sone succeeds without images`() { + val inputStream = javaClass.getResourceAsStream("sone-parser-without-images.xml") + assertThat(soneParser.parseSone(sone, inputStream), notNullValue()) + } + + @Test + fun `parsing a sone fails without image id`() { + val inputStream = javaClass.getResourceAsStream("sone-parser-without-image-id.xml") + assertThat(soneParser.parseSone(sone, inputStream), nullValue()) + } + + @Test + fun `parsing a sone fails without image time`() { + val inputStream = javaClass.getResourceAsStream("sone-parser-without-image-time.xml") + assertThat(soneParser.parseSone(sone, inputStream), nullValue()) + } + + @Test + fun `parsing a sone fails without image key`() { + val inputStream = javaClass.getResourceAsStream("sone-parser-without-image-key.xml") + assertThat(soneParser.parseSone(sone, inputStream), nullValue()) + } + + @Test + fun `parsing a sone fails without image title`() { + val inputStream = javaClass.getResourceAsStream("sone-parser-without-image-title.xml") + assertThat(soneParser.parseSone(sone, inputStream), nullValue()) + } + + @Test + fun `parsing a sone fails without image width`() { + val inputStream = javaClass.getResourceAsStream("sone-parser-without-image-width.xml") + assertThat(soneParser.parseSone(sone, inputStream), nullValue()) + } + + @Test + fun `parsing a sone fails without image height`() { + val inputStream = javaClass.getResourceAsStream("sone-parser-without-image-height.xml") + assertThat(soneParser.parseSone(sone, inputStream), nullValue()) + } + + @Test + fun `parsing a sone fails with invalid image width`() { + val inputStream = javaClass.getResourceAsStream("sone-parser-with-invalid-image-width.xml") + assertThat(soneParser.parseSone(sone, inputStream), nullValue()) + } + + @Test + fun `parsing a sone fails with invalid image height`() { + val inputStream = javaClass.getResourceAsStream("sone-parser-with-invalid-image-height.xml") + assertThat(soneParser.parseSone(sone, inputStream), nullValue()) + } + + @Test + fun `parsing a sone succeeds with image`() { + val inputStream = javaClass.getResourceAsStream("sone-parser-with-image.xml") + val sone = soneParser.parseSone(this.sone, inputStream) + assertThat(sone, notNullValue()) + assertThat(sone!!.rootAlbum.albums, hasSize(1)) + assertThat(sone.rootAlbum.albums[0].images, hasSize(1)) + val image = sone.rootAlbum.albums[0].images[0] + assertThat(image.id, equalTo("image-id")) + assertThat(image.creationTime, equalTo(1407197508000L)) + assertThat(image.key, equalTo("KSK@GPLv3.txt")) + assertThat(image.title, equalTo("image-title")) + assertThat(image.description, equalTo("image-description")) + assertThat(image.width, equalTo(1920)) + assertThat(image.height, equalTo(1080)) + assertThat(sone.profile.avatar, equalTo("image-id")) + } + + @Test + fun `unsuccessful parsing does not add a histogram entry`() { + val inputStream = javaClass.getResourceAsStream("sone-parser-with-invalid-image-height.xml") + assertThat(soneParser.parseSone(sone, inputStream), nullValue()) + val histogram = metricRegistry.histogram("sone.parse.duration") + assertThat(histogram.count, equalTo(0L)) + } + + @Test + fun `successful parsing adds histogram entry`() { + val inputStream = javaClass.getResourceAsStream("sone-parser-without-images.xml") + assertThat(soneParser.parseSone(sone, inputStream), notNullValue()) + val histogram = metricRegistry.histogram("sone.parse.duration") + assertThat(histogram.count, equalTo(1L)) + } + +} diff --git a/src/test/kotlin/net/pterodactylus/sone/core/UpdateCheckerTest.kt b/src/test/kotlin/net/pterodactylus/sone/core/UpdateCheckerTest.kt new file mode 100644 index 0000000..3bc8a9e --- /dev/null +++ b/src/test/kotlin/net/pterodactylus/sone/core/UpdateCheckerTest.kt @@ -0,0 +1,237 @@ +package net.pterodactylus.sone.core + +import com.google.common.eventbus.* +import freenet.client.* +import freenet.keys.* +import freenet.support.io.* +import net.pterodactylus.sone.core.FreenetInterface.* +import net.pterodactylus.sone.core.event.* +import net.pterodactylus.sone.main.* +import net.pterodactylus.sone.test.* +import net.pterodactylus.util.version.Version +import org.hamcrest.MatcherAssert.* +import org.hamcrest.Matchers.equalTo +import org.hamcrest.Matchers.instanceOf +import org.junit.* +import org.mockito.ArgumentCaptor.* +import org.mockito.ArgumentMatchers.any +import org.mockito.Mockito.never +import org.mockito.Mockito.times +import org.mockito.Mockito.verify +import org.mockito.hamcrest.MockitoHamcrest.* +import java.io.* +import kotlin.Long.Companion.MAX_VALUE + +/** + * Unit test for [UpdateChecker]. + */ +class UpdateCheckerTest { + + private val eventBus = mock() + private val freenetInterface = mock() + private val currentVersion = Version(1, 0, 0) + private val pluginHomepage = PluginHomepage("KSK@homepage") + private val updateChecker = UpdateChecker(eventBus, freenetInterface, currentVersion, pluginHomepage) + + @Before + fun startUpdateChecker() { + updateChecker.start() + } + + @Test + fun `new update checker does not have a latest version`() { + assertThat(updateChecker.hasLatestVersion(), equalTo(false)) + assertThat(updateChecker.latestVersion, equalTo(currentVersion)) + } + + @Test + fun `starting an update checker register a usk`() { + verify(freenetInterface).registerUsk(any(FreenetURI::class.java), any(Callback::class.java)) + } + + @Test + fun `stopping an update checker unregisters a usk`() { + updateChecker.stop() + verify(freenetInterface).unregisterUsk(any(FreenetURI::class.java)) + } + + @Test + fun `callback does not download if new edition is not found`() { + setupCallbackWithEdition(MAX_VALUE, false) + verify(freenetInterface, never()).fetchUri(any(FreenetURI::class.java)) + verify(eventBus, never()).post(argThat(instanceOf(UpdateFoundEvent::class.java))) + } + + private fun setupCallbackWithEdition(edition: Long, newKnownGood: Boolean, newSlot: Boolean = false) { + val uri = forClass(FreenetURI::class.java) + val callback = forClass(Callback::class.java) + verify(freenetInterface).registerUsk(uri.capture(), callback.capture()) + callback.value.editionFound(uri.value, edition, newKnownGood, newSlot) + } + + @Test + fun `callback starts if new edition is found`() { + setupFetchResult(createFutureFetchResult()) + setupCallbackWithEdition(MAX_VALUE, true) + verifyAFreenetUriIsFetched() + verifyEventIsFired(Version(99, 0, 0), 11865368297000L, false) + verifyThatUpdateCheckerKnowsLatestVersion(Version(99, 0, 0), 11865368297000L) + } + + private fun createFutureFetchResult(): FetchResult { + val clientMetadata = ClientMetadata("application/xml") + val fetched = ArrayBucket(("# MapConfigurationBackendVersion=1\n" + + "CurrentVersion/Version: 99.0.0\n" + + "CurrentVersion/ReleaseTime: 11865368297000\n" + + "DisruptiveVersion/0.1.2: true").toByteArray()) + return FetchResult(clientMetadata, fetched) + } + + private fun verifyEventIsFired(version: Version, releaseTime: Long, disruptive: Boolean) { + val updateFoundEvent = forClass(UpdateFoundEvent::class.java) + verify(eventBus, times(1)).post(updateFoundEvent.capture()) + assertThat(updateFoundEvent.value.version, equalTo(version)) + assertThat(updateFoundEvent.value.releaseTime, equalTo(releaseTime)) + assertThat(updateFoundEvent.value.isDisruptive, equalTo(disruptive)) + } + + private fun verifyThatUpdateCheckerKnowsLatestVersion(version: Version, releaseTime: Long) { + assertThat(updateChecker.latestVersion, equalTo(version)) + assertThat(updateChecker.latestVersionDate, equalTo(releaseTime)) + assertThat(updateChecker.hasLatestVersion(), equalTo(true)) + } + + @Test + fun `callback does not start if no new edition is found`() { + setupFetchResult(createPastFetchResult()) + setupCallbackWithEdition(updateChecker.latestEdition, true) + verifyAFreenetUriIsFetched() + verifyNoUpdateFoundEventIsFired() + } + + private fun setupFetchResult(pastFetchResult: FetchResult) { + whenever(freenetInterface.fetchUri(any(FreenetURI::class.java))).thenAnswer { invocation -> + val freenetUri = invocation.arguments[0] as FreenetURI + Fetched(freenetUri, pastFetchResult) + } + } + + private fun createPastFetchResult(): FetchResult { + val clientMetadata = ClientMetadata("application/xml") + val fetched = ArrayBucket(("# MapConfigurationBackendVersion=1\n" + + "CurrentVersion/Version: 0.2\n" + + "CurrentVersion/ReleaseTime: 1289417883000").toByteArray()) + return FetchResult(clientMetadata, fetched) + } + + @Test + fun `invalid update file does not start callback`() { + setupFetchResult(createInvalidFetchResult()) + setupCallbackWithEdition(MAX_VALUE, true) + verifyAFreenetUriIsFetched() + verifyNoUpdateFoundEventIsFired() + } + + private fun createInvalidFetchResult(): FetchResult { + val clientMetadata = ClientMetadata("text/plain") + val fetched = ArrayBucket("Some other data.".toByteArray()) + return FetchResult(clientMetadata, fetched) + } + + @Test + fun `non existing properties will not cause update to be found`() { + setupCallbackWithEdition(MAX_VALUE, true) + verifyAFreenetUriIsFetched() + verifyNoUpdateFoundEventIsFired() + } + + private fun verifyNoUpdateFoundEventIsFired() { + verify(eventBus, never()).post(any(UpdateFoundEvent::class.java)) + } + + private fun verifyAFreenetUriIsFetched() { + verify(freenetInterface).fetchUri(any(FreenetURI::class.java)) + } + + @Test + fun `broken bucket does not cause update to be found`() { + setupFetchResult(createBrokenBucketFetchResult()) + setupCallbackWithEdition(MAX_VALUE, true) + verifyAFreenetUriIsFetched() + verifyNoUpdateFoundEventIsFired() + } + + private fun createBrokenBucketFetchResult(): FetchResult { + val clientMetadata = ClientMetadata("text/plain") + val fetched = object : ArrayBucket("Some other data.".toByteArray()) { + override fun getInputStream() = + whenever(mock().read()).thenThrow(IOException()).getMock() + } + return FetchResult(clientMetadata, fetched) + } + + @Test + fun `invalid time does not cause an update to be found`() { + setupFetchResult(createInvalidTimeFetchResult()) + setupCallbackWithEdition(MAX_VALUE, true) + verifyAFreenetUriIsFetched() + verifyNoUpdateFoundEventIsFired() + } + + private fun createInvalidTimeFetchResult(): FetchResult { + val clientMetadata = ClientMetadata("application/xml") + val fetched = ArrayBucket(("# MapConfigurationBackendVersion=1\n" + + "CurrentVersion/Version: 0.2\n" + + "CurrentVersion/ReleaseTime: invalid").toByteArray()) + return FetchResult(clientMetadata, fetched) + } + + @Test + fun `invalid properties does not cause an update to be found`() { + setupFetchResult(createMissingTimeFetchResult()) + setupCallbackWithEdition(MAX_VALUE, true) + verifyAFreenetUriIsFetched() + verifyNoUpdateFoundEventIsFired() + } + + private fun createMissingTimeFetchResult(): FetchResult { + val clientMetadata = ClientMetadata("application/xml") + val fetched = ArrayBucket(("# MapConfigurationBackendVersion=1\nCurrentVersion/Version: 0.2\n").toByteArray()) + return FetchResult(clientMetadata, fetched) + } + + @Test + fun `invalid version does not cause an update to be found`() { + setupFetchResult(createInvalidVersionFetchResult()) + setupCallbackWithEdition(MAX_VALUE, true) + verifyAFreenetUriIsFetched() + verifyNoUpdateFoundEventIsFired() + } + + private fun createInvalidVersionFetchResult(): FetchResult { + val clientMetadata = ClientMetadata("application/xml") + val fetched = ArrayBucket(("# MapConfigurationBackendVersion=1\n" + + "CurrentVersion/Version: foo\n" + + "CurrentVersion/ReleaseTime: 1289417883000").toByteArray()) + return FetchResult(clientMetadata, fetched) + } + + @Test + fun `disruptive version gets notification`() { + setupFetchResult(createDisruptiveVersionFetchResult()) + setupCallbackWithEdition(MAX_VALUE, true) + verifyAFreenetUriIsFetched() + verifyEventIsFired(Version(1, 2, 3), 1289417883000L, true) + verifyThatUpdateCheckerKnowsLatestVersion(Version(1, 2, 3), 1289417883000L) + } + + private fun createDisruptiveVersionFetchResult(): FetchResult { + val clientMetadata = ClientMetadata("application/xml") + val fetched = ArrayBucket(("# MapConfigurationBackendVersion=1\n" + + "CurrentVersion/Version: 1.2.3\n" + + "CurrentVersion/ReleaseTime: 1289417883000\n" + + "DisruptiveVersion/1.2.3: true").toByteArray()) + return FetchResult(clientMetadata, fetched) + } + +} diff --git a/src/test/kotlin/net/pterodactylus/sone/data/AlbumsTest.kt b/src/test/kotlin/net/pterodactylus/sone/data/AlbumsTest.kt new file mode 100644 index 0000000..26af4e9 --- /dev/null +++ b/src/test/kotlin/net/pterodactylus/sone/data/AlbumsTest.kt @@ -0,0 +1,48 @@ +/** + * Sone - AlbumsTest.kt - Copyright © 2019–2020 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 . + */ + +package net.pterodactylus.sone.data + +import net.pterodactylus.sone.data.impl.* +import org.hamcrest.MatcherAssert.* +import org.hamcrest.Matchers.* +import kotlin.test.* + +/** + * Unit test for various helper method in `Albums.kt`. + */ +class AlbumsTest { + + @Test + fun `recursive list of all images for album is returned correctly`() { + val sone = IdOnlySone("sone") + val album = AlbumImpl(sone) + val firstNestedAlbum = AlbumImpl(sone) + val secondNestedAlbum = AlbumImpl(sone) + firstNestedAlbum.addImage(createImage(sone, "image-1")) + firstNestedAlbum.addImage(createImage(sone, "image-2")) + secondNestedAlbum.addImage(createImage(sone, "image-3")) + album.addImage(createImage(sone, "image-4")) + album.addAlbum(firstNestedAlbum) + album.addAlbum(secondNestedAlbum) + val images = album.allImages + assertThat(images.map(Image::id), containsInAnyOrder("image-1", "image-2", "image-3", "image-4")) + } + + private fun createImage(sone: IdOnlySone, id: String) = ImageImpl(id).modify().setSone(sone).update() + +} diff --git a/src/test/kotlin/net/pterodactylus/sone/data/ClientTest.kt b/src/test/kotlin/net/pterodactylus/sone/data/ClientTest.kt new file mode 100644 index 0000000..bad8ff8 --- /dev/null +++ b/src/test/kotlin/net/pterodactylus/sone/data/ClientTest.kt @@ -0,0 +1,15 @@ +package net.pterodactylus.sone.data + +import org.hamcrest.MatcherAssert.* +import org.hamcrest.Matchers.* +import kotlin.test.* + +class ClientTest { + + @Test + fun `toString() formats client name and version`() { + val client = Client("Test Client", "v123") + assertThat(client.toString(), equalTo("Test Client v123")) + } + +} diff --git a/src/test/kotlin/net/pterodactylus/sone/database/memory/MemoryDatabaseTest.kt b/src/test/kotlin/net/pterodactylus/sone/database/memory/MemoryDatabaseTest.kt index 1536da4..cc4778f 100644 --- a/src/test/kotlin/net/pterodactylus/sone/database/memory/MemoryDatabaseTest.kt +++ b/src/test/kotlin/net/pterodactylus/sone/database/memory/MemoryDatabaseTest.kt @@ -1,5 +1,5 @@ /* - * Sone - MemoryDatabaseTest.kt - Copyright © 2013–2019 David Roden + * Sone - MemoryDatabaseTest.kt - Copyright © 2013–2020 David 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 @@ -38,7 +38,7 @@ import kotlin.test.* */ class MemoryDatabaseTest { - private val configuration = mock() + private val configuration = deepMock() private val memoryDatabase = MemoryDatabase(configuration) private val sone = mock() @@ -50,8 +50,8 @@ class MemoryDatabaseTest { @Test fun `stored sone is made available`() { storeSone() - assertThat(memoryDatabase.getPost("post1"), isPost("post1", 1000L, "post1", absent())) - assertThat(memoryDatabase.getPost("post2"), isPost("post2", 2000L, "post2", of(RECIPIENT_ID))) + assertThat(memoryDatabase.getPost("post1"), isPost("post1", 1000L, "post1", null)) + assertThat(memoryDatabase.getPost("post2"), isPost("post2", 2000L, "post2", RECIPIENT_ID)) assertThat(memoryDatabase.getPost("post3"), nullValue()) assertThat(memoryDatabase.getPostReply("reply1"), isPostReply("reply1", "post1", 3000L, "reply1")) assertThat(memoryDatabase.getPostReply("reply2"), isPostReply("reply2", "post2", 4000L, "reply2")) @@ -409,6 +409,36 @@ class MemoryDatabaseTest { assertThat(configuration.getStringValue("KnownReplies/1/ID").value, equalTo(null)) } + @Test + @Dirty("the rate limiter should be mocked") + fun `saving the database twice in a row only saves it once`() { + memoryDatabase.save() + memoryDatabase.save() + verify(configuration.getStringValue("KnownPosts/0/ID"), times(1)).value = null + } + + @Test + @Dirty("the rate limiter should be mocked") + fun `setting posts as knows twice in a row only saves the database once`() { + prepareConfigurationValues() + val post = mock() + whenever(post.id).thenReturn("post-id") + memoryDatabase.setPostKnown(post, true) + memoryDatabase.setPostKnown(post, true) + verify(configuration, times(1)).getStringValue("KnownPosts/1/ID") + } + + @Test + @Dirty("the rate limiter should be mocked") + fun `setting post replies as knows twice in a row only saves the database once`() { + prepareConfigurationValues() + val postReply = mock() + whenever(postReply.id).thenReturn("post-reply-id") + memoryDatabase.setPostReplyKnown(postReply, true) + memoryDatabase.setPostReplyKnown(postReply, true) + verify(configuration, times(1)).getStringValue("KnownReplies/1/ID") + } + } private const val SONE_ID = "sone" diff --git a/src/test/kotlin/net/pterodactylus/sone/fcp/CreatePostCommandTest.kt b/src/test/kotlin/net/pterodactylus/sone/fcp/CreatePostCommandTest.kt index 29db41c..cfbda94 100644 --- a/src/test/kotlin/net/pterodactylus/sone/fcp/CreatePostCommandTest.kt +++ b/src/test/kotlin/net/pterodactylus/sone/fcp/CreatePostCommandTest.kt @@ -56,7 +56,7 @@ class CreatePostCommandTest : SoneCommandTest() { parameters += "Text" to "Test" whenever(core.getSone("LocalSoneId")).thenReturn(localSone) val post = mock().apply { whenever(id).thenReturn("PostId") } - whenever(core.createPost(localSone, absent(), "Test")).thenReturn(post) + whenever(core.createPost(localSone, null, "Test")).thenReturn(post) val response = command.execute(parameters) assertThat(response.replyParameters.get("Message"), equalTo("PostCreated")) assertThat(response.replyParameters.get("Post"), equalTo("PostId")) @@ -90,7 +90,7 @@ class CreatePostCommandTest : SoneCommandTest() { whenever(core.getSone("LocalSoneId")).thenReturn(localSone) whenever(core.getSone("RemoteSoneId")).thenReturn(remoteSone) val post = mock().apply { whenever(id).thenReturn("PostId") } - whenever(core.createPost(localSone, of(remoteSone), "Test")).thenReturn(post) + whenever(core.createPost(localSone, remoteSone, "Test")).thenReturn(post) val response = command.execute(parameters) assertThat(response.replyParameters.get("Message"), equalTo("PostCreated")) assertThat(response.replyParameters.get("Post"), equalTo("PostId")) diff --git a/src/test/kotlin/net/pterodactylus/sone/freenet/AsyncFreenetInterfaceTest.kt b/src/test/kotlin/net/pterodactylus/sone/freenet/AsyncFreenetInterfaceTest.kt new file mode 100644 index 0000000..472fd94 --- /dev/null +++ b/src/test/kotlin/net/pterodactylus/sone/freenet/AsyncFreenetInterfaceTest.kt @@ -0,0 +1,80 @@ +/** + * Sone - AsyncFreenetInterfaceTest.kt - Copyright © 2019–2020 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 . + */ + +package net.pterodactylus.sone.freenet + +import freenet.client.* +import freenet.keys.* +import freenet.support.io.* +import kotlinx.coroutines.* +import net.pterodactylus.sone.core.* +import org.hamcrest.MatcherAssert.* +import org.hamcrest.Matchers.* +import java.util.concurrent.atomic.* +import kotlin.test.* + +class AsyncFreenetInterfaceTest { + + @Test + fun `returned deferred is completed by success`() = runBlocking { + val result = FetchResult(ClientMetadata(), NullBucket()) + val freenetClient = object : FreenetClient { + override fun fetch(freenetKey: FreenetURI) = result + } + val freenetInterface = AsyncFreenetInterface(freenetClient) + val fetched = async { freenetInterface.fetchUri(FreenetURI("KSK@GPL.txt")) } + + withTimeout(1000) { + assertThat(fetched.await(), equalTo(Fetched(FreenetURI("KSK@GPL.txt"), result))) + } + } + + @Test + fun `permanent redircts are being followed`() = runBlocking { + val result = FetchResult(ClientMetadata(), NullBucket()) + val freenetClient = object : FreenetClient { + val redirected = AtomicBoolean(false) + override fun fetch(freenetKey: FreenetURI) = + if (redirected.compareAndSet(false, true)) + throw FetchException(FetchException.FetchExceptionMode.PERMANENT_REDIRECT, FreenetURI("KSK@GPLv3.txt")) + else result + } + val freenetInterface = AsyncFreenetInterface(freenetClient) + val fetched = async { freenetInterface.fetchUri(FreenetURI("KSK@GPL.txt")) } + + withTimeout(1000) { + assertThat(fetched.await(), equalTo(Fetched(FreenetURI("KSK@GPLv3.txt"), result))) + } + } + + @Test + fun `fetch errors are being re-thrown`() = runBlocking { + val freenetClient = object : FreenetClient { + override fun fetch(freenetKey: FreenetURI) = + throw FetchException(FetchException.FetchExceptionMode.ALL_DATA_NOT_FOUND) + } + val freenetInterface = AsyncFreenetInterface(freenetClient) + val fetched = supervisorScope { async { freenetInterface.fetchUri(FreenetURI("KSK@GPL.txt")) } } + + withTimeout(1000) { + assertFailsWith(FetchException::class) { + fetched.await() + } + } + } + +} diff --git a/src/test/kotlin/net/pterodactylus/sone/freenet/BaseL10nTranslationTest.kt b/src/test/kotlin/net/pterodactylus/sone/freenet/BaseL10nTranslationTest.kt new file mode 100644 index 0000000..a188d47 --- /dev/null +++ b/src/test/kotlin/net/pterodactylus/sone/freenet/BaseL10nTranslationTest.kt @@ -0,0 +1,30 @@ +package net.pterodactylus.sone.freenet + +import freenet.l10n.* +import net.pterodactylus.sone.test.* +import org.hamcrest.MatcherAssert.* +import org.hamcrest.Matchers.* +import org.junit.* +import java.util.* + +/** + * Test for [BaseL10nTranslation]. + */ +class BaseL10nTranslationTest { + + private val baseL10n = mock() + private val translation = BaseL10nTranslation(baseL10n) + + @Test + fun `translate method is facade for the correct method`() { + whenever(baseL10n.getString("test")).thenReturn("answer") + assertThat(translation.translate("test"), equalTo("answer")) + } + + @Test + fun `language exposes correct short code`() { + whenever(baseL10n.selectedLanguage).thenReturn(BaseL10n.LANGUAGE.ENGLISH) + assertThat(translation.currentLocale, equalTo(Locale.ENGLISH)) + } + +} diff --git a/src/test/kotlin/net/pterodactylus/sone/freenet/FreenetClientTest.kt b/src/test/kotlin/net/pterodactylus/sone/freenet/FreenetClientTest.kt new file mode 100644 index 0000000..f6d5883 --- /dev/null +++ b/src/test/kotlin/net/pterodactylus/sone/freenet/FreenetClientTest.kt @@ -0,0 +1,23 @@ +package net.pterodactylus.sone.freenet + +import freenet.client.* +import freenet.keys.* +import freenet.support.io.* +import net.pterodactylus.sone.test.* +import org.hamcrest.MatcherAssert.* +import org.hamcrest.Matchers.* +import kotlin.test.* + +class FreenetClientTest { + + private val highLevelSimpleClient = mock() + private val freenetClient = DefaultFreenetClient(highLevelSimpleClient) + + @Test + fun `fetch method calls method on hlsc`() { + val fetchResult = FetchResult(ClientMetadata(), NullBucket()) + whenever(highLevelSimpleClient.fetch(FreenetURI("KSK@GPL.txt"))).thenReturn(fetchResult) + assertThat(freenetClient.fetch(FreenetURI("KSK@GPL.txt")), equalTo(fetchResult)) + } + +} diff --git a/src/test/kotlin/net/pterodactylus/sone/freenet/FreenetURIsTest.kt b/src/test/kotlin/net/pterodactylus/sone/freenet/FreenetURIsTest.kt new file mode 100644 index 0000000..ca1fc6e --- /dev/null +++ b/src/test/kotlin/net/pterodactylus/sone/freenet/FreenetURIsTest.kt @@ -0,0 +1,24 @@ +package net.pterodactylus.sone.freenet + +import freenet.keys.* +import org.hamcrest.MatcherAssert.* +import org.hamcrest.Matchers.* +import org.junit.* + +/** + * Unit test for [Key]. + */ +class FreenetURIsTest { + + private val uri = FreenetURI("SSK@$routingKey,$cryptoKey,$extra/some-site-12/foo/bar.html") + + @Test + fun routingKeyIsExtractCorrectly() { + assertThat(uri.routingKeyString, equalTo(routingKey)) + } + +} + +private const val routingKey = "NfUYvxDwU9vqb2mh-qdT~DYJ6U0XNbxMGGoLe0aCHJs" +private const val cryptoKey = "Miglsgix0VR56ZiPl4NgjnUd~UdrnHqIvXJ3KKHmxmI" +private const val extra = "AQACAAE" diff --git a/src/test/kotlin/net/pterodactylus/sone/freenet/L10nFilterTest.kt b/src/test/kotlin/net/pterodactylus/sone/freenet/L10nFilterTest.kt index 66b1ab1..358d106 100644 --- a/src/test/kotlin/net/pterodactylus/sone/freenet/L10nFilterTest.kt +++ b/src/test/kotlin/net/pterodactylus/sone/freenet/L10nFilterTest.kt @@ -1,60 +1,51 @@ package net.pterodactylus.sone.freenet -import freenet.l10n.BaseL10n -import freenet.l10n.BaseL10n.LANGUAGE.ENGLISH -import net.pterodactylus.sone.test.mock -import net.pterodactylus.sone.test.whenever -import net.pterodactylus.util.template.TemplateContext -import org.hamcrest.MatcherAssert.assertThat -import org.hamcrest.Matchers.equalTo -import org.junit.Before -import org.junit.Test -import org.mockito.ArgumentMatchers.anyString +import org.hamcrest.MatcherAssert.* +import org.hamcrest.Matchers.* +import org.junit.* +import java.util.* +import kotlin.collections.* /** * Unit test for [L10nFilter]. */ class L10nFilterTest { - private val l10n = mock() - private val filter = L10nFilter(l10n) - private val templateContext = mock() private val translations = mutableMapOf() - - @Before - fun setupL10n() { - whenever(l10n.selectedLanguage).thenReturn(ENGLISH) - whenever(l10n.getString(anyString())).then { translations[it.arguments[0]] } + private val translation = object : Translation { + override val currentLocale = Locale.ENGLISH + override fun translate(key: String): String = translations[key] ?: "" } + private val filter = L10nFilter(translation) @Test fun `translation without parameters returns translated string`() { translations["data"] = "translated data" - assertThat(filter.format(templateContext, "data", emptyMap()), equalTo("translated data")) + assertThat(filter.format(null, "data", emptyMap()), equalTo("translated data")) } @Test fun `translation with parameters returned translated string`() { translations["data"] = "translated {0,number} {1}" - assertThat(filter.format(templateContext, "data", mapOf("0" to 4.5, "1" to "data")), equalTo("translated 4.5 data")) + assertThat(filter.format(null, "data", mapOf("0" to 4.5, "1" to "data")), equalTo("translated 4.5 data")) } @Test fun `filter processes l10n text without parameters correctly`() { translations["data"] = "translated data" - assertThat(filter.format(templateContext, L10nText("data"), emptyMap()), equalTo("translated data")) + assertThat(filter.format(null, L10nText("data"), emptyMap()), equalTo("translated data")) } @Test fun `filter processes l10n text with parameters correctly`() { translations["data"] = "translated {0,number} {1}" - assertThat(filter.format(templateContext, L10nText("data", listOf(4.5, "data")), emptyMap()), equalTo("translated 4.5 data")) + assertThat(filter.format(null, L10nText("data", listOf(4.5, "data")), emptyMap()), equalTo("translated 4.5 data")) } @Test fun `filter does not replace values if there are no parameters`() { translations["data"] = "{link}" - assertThat(filter.format(templateContext, "data", emptyMap()), equalTo("{link}")) + assertThat(filter.format(null, "data", emptyMap()), equalTo("{link}")) } } diff --git a/src/test/kotlin/net/pterodactylus/sone/freenet/plugin/FredPluginConnectorTest.kt b/src/test/kotlin/net/pterodactylus/sone/freenet/plugin/FredPluginConnectorTest.kt new file mode 100644 index 0000000..0c68bfe --- /dev/null +++ b/src/test/kotlin/net/pterodactylus/sone/freenet/plugin/FredPluginConnectorTest.kt @@ -0,0 +1,77 @@ +/** + * Sone - FredPluginConnectorTest.kt - Copyright © 2019–2020 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 . + */ + +/* Fred-based plugin stuff is mostly deprecated. ¯\_(ツ)_/¯ */ +@file:Suppress("DEPRECATION") + +package net.pterodactylus.sone.freenet.plugin + +import freenet.pluginmanager.* +import freenet.support.* +import freenet.support.api.* +import freenet.support.io.* +import kotlinx.coroutines.* +import net.pterodactylus.sone.freenet.* +import org.hamcrest.MatcherAssert.* +import org.hamcrest.Matchers.* +import org.junit.* +import org.junit.rules.* +import kotlin.concurrent.* + +class FredPluginConnectorTest { + + @Rule + @JvmField + val expectedException = ExpectedException.none()!! + + @Test + fun `connector throws exception if plugin can not be found`() = runBlocking { + val pluginConnector = FredPluginConnector(pluginRespiratorFacade) + expectedException.expect(PluginException::class.java) + pluginConnector.sendRequest("wrong.plugin", requestFields, requestData) + Unit + } + + @Test + fun `connector returns correct fields and data`() = runBlocking { + val pluginConnector = FredPluginConnector(pluginRespiratorFacade) + val reply = pluginConnector.sendRequest("test.plugin", requestFields, requestData) + assertThat(reply.fields, equalTo(responseFields)) + assertThat(reply.data, equalTo(responseData)) + } + +} + +private val requestFields = SimpleFieldSetBuilder().put("foo", "bar").get() +private val requestData: Bucket? = ArrayBucket(byteArrayOf(1, 2)) +private val responseFields = SimpleFieldSetBuilder().put("baz", "quo").get() +private val responseData: Bucket? = ArrayBucket(byteArrayOf(3, 4)) + +private val pluginRespiratorFacade = object : PluginRespiratorFacade { + override fun getPluginTalker(pluginTalker: FredPluginTalker, pluginName: String, identifier: String) = + if (pluginName == "test.plugin") { + object : PluginTalkerFacade { + override fun send(pluginParameters: SimpleFieldSet, data: Bucket?) { + if ((pluginParameters == requestFields) && (data == requestData)) { + thread { pluginTalker.onReply(pluginName, identifier, responseFields, responseData) } + } + } + } + } else { + throw PluginNotFoundException() + } +} diff --git a/src/test/kotlin/net/pterodactylus/sone/freenet/plugin/PluginRespiratorFacadeTest.kt b/src/test/kotlin/net/pterodactylus/sone/freenet/plugin/PluginRespiratorFacadeTest.kt new file mode 100644 index 0000000..4e44d5d --- /dev/null +++ b/src/test/kotlin/net/pterodactylus/sone/freenet/plugin/PluginRespiratorFacadeTest.kt @@ -0,0 +1,46 @@ +package net.pterodactylus.sone.freenet.plugin + +import freenet.pluginmanager.* +import freenet.support.* +import freenet.support.api.* +import freenet.support.io.* +import net.pterodactylus.sone.freenet.* +import net.pterodactylus.sone.test.* +import org.hamcrest.MatcherAssert.* +import org.hamcrest.Matchers.* +import org.mockito.ArgumentMatchers.* +import kotlin.test.* + +/** + * Unit test for [FredPluginRespiratorFacade] and [FredPluginTalkerFacade]. + */ +@Suppress("DEPRECATION") +class PluginRespiratorFacadeTest { + + @Test + fun `respirator facade creates correct plugin talker facade`() { + val pluginTalkerSendParameters = mutableListOf() + val originalPluginTalker = mock().apply { + whenever(send(any(), any())).then { invocation -> + pluginTalkerSendParameters += PluginTalkerSendParameters(invocation.getArgument(0), invocation.getArgument(1)) + Unit + } + } + val fredPluginTalker = FredPluginTalker { _, _, _, _ -> } + val pluginRespirator = mock().apply { + whenever(getPluginTalker(fredPluginTalker, "test.plugin", "test-request-1")).thenReturn(originalPluginTalker) + } + val pluginRespiratorFacade = FredPluginRespiratorFacade(pluginRespirator) + val pluginTalker = pluginRespiratorFacade.getPluginTalker(fredPluginTalker, "test.plugin", "test-request-1") + pluginTalker.send(fields, data) + assertThat(pluginTalkerSendParameters, hasSize(1)) + assertThat(pluginTalkerSendParameters[0].parameter, equalTo(fields)) + assertThat(pluginTalkerSendParameters[0].data, equalTo(data)) + } + +} + +private val fields = SimpleFieldSetBuilder().put("foo", "bar").get() +private val data: Bucket? = ArrayBucket(byteArrayOf(1, 2)) + +private data class PluginTalkerSendParameters(val parameter: SimpleFieldSet, val data: Bucket?) diff --git a/src/test/kotlin/net/pterodactylus/sone/freenet/wot/DefaultIdentityTest.kt b/src/test/kotlin/net/pterodactylus/sone/freenet/wot/DefaultIdentityTest.kt new file mode 100644 index 0000000..4d5c0ec --- /dev/null +++ b/src/test/kotlin/net/pterodactylus/sone/freenet/wot/DefaultIdentityTest.kt @@ -0,0 +1,142 @@ +/* + * Sone - DefaultIdentityTest.kt - Copyright © 2013–2020 David 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 . + */ + +package net.pterodactylus.sone.freenet.wot + +import com.google.common.collect.ImmutableMap.* +import net.pterodactylus.sone.test.* +import net.pterodactylus.sone.test.Matchers.* +import org.hamcrest.MatcherAssert.* +import org.hamcrest.Matchers.containsInAnyOrder +import org.hamcrest.Matchers.empty +import org.hamcrest.Matchers.equalTo +import org.hamcrest.Matchers.hasEntry +import org.hamcrest.Matchers.not +import org.hamcrest.Matchers.nullValue +import org.hamcrest.collection.IsIterableContainingInOrder.contains +import org.junit.* + +/** + * Unit test for [DefaultIdentity]. + */ +open class DefaultIdentityTest { + + protected open val identity = DefaultIdentity("Id", "Nickname", "RequestURI") + + @Test + fun `identity can be created`() { + assertThat(identity.id, equalTo("Id")) + assertThat(identity.nickname, equalTo("Nickname")) + assertThat(identity.requestUri, equalTo("RequestURI")) + assertThat(identity.contexts, empty()) + assertThat(identity.properties, equalTo(emptyMap())) + } + + @Test + fun `contexts are added correctly`() { + identity.addContext("Test") + assertThat(identity.contexts, contains("Test")) + assertThat(identity.hasContext("Test"), equalTo(true)) + } + + @Test + fun `contexts are removed correctly`() { + identity.addContext("Test") + identity.removeContext("Test") + assertThat(identity.contexts, empty()) + assertThat(identity.hasContext("Test"), equalTo(false)) + } + + @Test + fun `contexts are set correctly in bulk`() { + identity.addContext("Test") + identity.contexts = setOf("Test1", "Test2") + assertThat(identity.contexts, containsInAnyOrder("Test1", "Test2")) + assertThat(identity.hasContext("Test"), equalTo(false)) + assertThat(identity.hasContext("Test1"), equalTo(true)) + assertThat(identity.hasContext("Test2"), equalTo(true)) + } + + @Test + fun `properties are added correctly`() { + identity.setProperty("Key", "Value") + assertThat(identity.properties.size, equalTo(1)) + assertThat(identity.properties, hasEntry("Key", "Value")) + assertThat(identity.getProperty("Key"), equalTo("Value")) + } + + @Test + fun `properties are removed correctly`() { + identity.setProperty("Key", "Value") + identity.removeProperty("Key") + assertThat(identity.properties, equalTo(emptyMap())) + assertThat(identity.getProperty("Key"), nullValue()) + } + + @Test + fun `properties are set correctly in bulk`() { + identity.setProperty("Key", "Value") + identity.properties = of("Key1", "Value1", "Key2", "Value2") + assertThat(identity.properties.size, equalTo(2)) + assertThat(identity.getProperty("Key"), nullValue()) + assertThat(identity.getProperty("Key1"), equalTo("Value1")) + assertThat(identity.getProperty("Key2"), equalTo("Value2")) + } + + @Test + fun `trust relationships are added correctly`() { + val ownIdentity = mock() + val trust = mock() + identity.setTrust(ownIdentity, trust) + assertThat(identity.getTrust(ownIdentity), equalTo(trust)) + } + + @Test + fun `trust relationships are removed correctly`() { + val ownIdentity = mock() + val trust = mock() + identity.setTrust(ownIdentity, trust) + identity.removeTrust(ownIdentity) + assertThat(identity.getTrust(ownIdentity), nullValue()) + } + + @Test + fun `identities with the same id are equal`() { + val identity2 = DefaultIdentity("Id", "Nickname2", "RequestURI2") + assertThat(identity2, equalTo(identity)) + assertThat(identity, equalTo(identity2)) + } + + @Test + fun `two equal identities have the same hash code`() { + val identity2 = DefaultIdentity("Id", "Nickname2", "RequestURI2") + assertThat(identity.hashCode(), equalTo(identity2.hashCode())) + } + + @Test + fun `null does not match an identity`() { + assertThat(identity, not(equalTo(null as Any?))) + } + + @Test + fun `toString() contains id and nickname`() { + val identityString = identity.toString() + assertThat(identityString, matchesRegex(".*\\bId\\b.*")) + assertThat(identityString, matchesRegex(".*\\bNickname\\b.*")) + } + +} diff --git a/src/test/kotlin/net/pterodactylus/sone/freenet/wot/DefaultOwnIdentityTest.kt b/src/test/kotlin/net/pterodactylus/sone/freenet/wot/DefaultOwnIdentityTest.kt new file mode 100644 index 0000000..2b6fc09 --- /dev/null +++ b/src/test/kotlin/net/pterodactylus/sone/freenet/wot/DefaultOwnIdentityTest.kt @@ -0,0 +1,36 @@ +/* + * Sone - DefaultOwnIdentityTest.kt - Copyright © 2013–2020 David 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 . + */ + +package net.pterodactylus.sone.freenet.wot + +import org.hamcrest.MatcherAssert.* +import org.hamcrest.Matchers.* +import org.junit.* + +/** + * Unit test for [DefaultOwnIdentity]. + */ +class DefaultOwnIdentityTest : DefaultIdentityTest() { + + override val identity = DefaultOwnIdentity("Id", "Nickname", "RequestURI", "InsertURI") + + @Test + fun `own identity can be created`() { + assertThat((identity as OwnIdentity).insertUri, equalTo("InsertURI")) + } + +} diff --git a/src/test/kotlin/net/pterodactylus/sone/freenet/wot/Identities.kt b/src/test/kotlin/net/pterodactylus/sone/freenet/wot/Identities.kt new file mode 100644 index 0000000..d699bb2 --- /dev/null +++ b/src/test/kotlin/net/pterodactylus/sone/freenet/wot/Identities.kt @@ -0,0 +1,35 @@ +/* + * Sone - Identities.kt - Copyright © 2013–2020 David 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 . + */ + +package net.pterodactylus.sone.freenet.wot + +fun createOwnIdentity(id: String, contexts: Set, vararg properties: Pair): OwnIdentity { + val ownIdentity = DefaultOwnIdentity(id, "Nickname$id", "Request$id", "Insert$id") + setContextsAndPropertiesOnIdentity(ownIdentity, contexts, mapOf(*properties)) + return ownIdentity +} + +fun createIdentity(id: String, contexts: Set, vararg properties: Pair): Identity { + val identity = DefaultIdentity(id, "Nickname$id", "Request$id") + setContextsAndPropertiesOnIdentity(identity, contexts, mapOf(*properties)) + return identity +} + +private fun setContextsAndPropertiesOnIdentity(identity: Identity, contexts: Set, properties: Map) { + identity.contexts = contexts + identity.properties = properties +} diff --git a/src/test/kotlin/net/pterodactylus/sone/freenet/wot/IdentityChangeDetectorTest.kt b/src/test/kotlin/net/pterodactylus/sone/freenet/wot/IdentityChangeDetectorTest.kt new file mode 100644 index 0000000..c02610c --- /dev/null +++ b/src/test/kotlin/net/pterodactylus/sone/freenet/wot/IdentityChangeDetectorTest.kt @@ -0,0 +1,154 @@ +/* + * Sone - IdentityChangeDetectorTest.kt - Copyright © 2013–2020 David 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 . + */ + +package net.pterodactylus.sone.freenet.wot + +import org.hamcrest.MatcherAssert.* +import org.hamcrest.Matchers.* +import org.junit.* + +/** + * Unit test for [IdentityChangeDetector]. + */ +class IdentityChangeDetectorTest { + + private val identityChangeDetector = IdentityChangeDetector(createOldIdentities()) + private val newIdentities = mutableListOf() + private val removedIdentities = mutableListOf() + private val changedIdentities = mutableListOf() + private val unchangedIdentities = mutableListOf() + + @Before + fun setup() { + identityChangeDetector.onNewIdentity = { identity -> newIdentities.add(identity) } + identityChangeDetector.onRemovedIdentity = { identity -> removedIdentities.add(identity) } + identityChangeDetector.onChangedIdentity = { identity -> changedIdentities.add(identity) } + identityChangeDetector.onUnchangedIdentity = { identity -> unchangedIdentities.add(identity) } + } + + @Test + fun `no differences are detected when sending the old identities again`() { + identityChangeDetector.detectChanges(createOldIdentities()) + assertThat(newIdentities, empty()) + assertThat(removedIdentities, empty()) + assertThat(changedIdentities, empty()) + assertThat(unchangedIdentities, containsInAnyOrder(createIdentity1(), createIdentity2(), createIdentity3())) + } + + @Test + fun `detect that an identity was removed`() { + identityChangeDetector.detectChanges(listOf(createIdentity1(), createIdentity3())) + assertThat(newIdentities, empty()) + assertThat(removedIdentities, containsInAnyOrder(createIdentity2())) + assertThat(changedIdentities, empty()) + assertThat(unchangedIdentities, containsInAnyOrder(createIdentity1(), createIdentity3())) + } + + @Test + fun `detect that an identity was added`() { + identityChangeDetector.detectChanges(listOf(createIdentity1(), createIdentity2(), createIdentity3(), createIdentity4())) + assertThat(newIdentities, containsInAnyOrder(createIdentity4())) + assertThat(removedIdentities, empty()) + assertThat(changedIdentities, empty()) + assertThat(unchangedIdentities, containsInAnyOrder(createIdentity1(), createIdentity2(), createIdentity3())) + } + + @Test + fun `detect that a context was removed`() { + val identity2 = createIdentity2() + identity2.removeContext("Context C") + identityChangeDetector.detectChanges(listOf(createIdentity1(), identity2, createIdentity3())) + assertThat(newIdentities, empty()) + assertThat(removedIdentities, empty()) + assertThat(changedIdentities, containsInAnyOrder(identity2)) + assertThat(unchangedIdentities, containsInAnyOrder(createIdentity1(), createIdentity3())) + } + + @Test + fun `detect that a context was added`() { + val identity2 = createIdentity2() + identity2.addContext("Context C1") + identityChangeDetector.detectChanges(listOf(createIdentity1(), identity2, createIdentity3())) + assertThat(newIdentities, empty()) + assertThat(removedIdentities, empty()) + assertThat(changedIdentities, containsInAnyOrder(identity2)) + assertThat(unchangedIdentities, containsInAnyOrder(createIdentity1(), createIdentity3())) + } + + @Test + fun `detect that a property was removed`() { + val identity1 = createIdentity1() + identity1.removeProperty("Key A") + identityChangeDetector.detectChanges(listOf(identity1, createIdentity2(), createIdentity3())) + assertThat(newIdentities, empty()) + assertThat(removedIdentities, empty()) + assertThat(changedIdentities, containsInAnyOrder(identity1)) + assertThat(unchangedIdentities, containsInAnyOrder(createIdentity2(), createIdentity3())) + } + + @Test + fun `detect that a property was added`() { + val identity3 = createIdentity3() + identity3.setProperty("Key A", "Value A") + identityChangeDetector.detectChanges(listOf(createIdentity1(), createIdentity2(), identity3)) + assertThat(newIdentities, empty()) + assertThat(removedIdentities, empty()) + assertThat(changedIdentities, containsInAnyOrder(identity3)) + assertThat(unchangedIdentities, containsInAnyOrder(createIdentity1(), createIdentity2())) + } + + @Test + fun `detect that a property was changed`() { + val identity3 = createIdentity3() + identity3.setProperty("Key E", "Value F") + identityChangeDetector.detectChanges(listOf(createIdentity1(), createIdentity2(), identity3)) + assertThat(newIdentities, empty()) + assertThat(removedIdentities, empty()) + assertThat(changedIdentities, containsInAnyOrder(identity3)) + assertThat(unchangedIdentities, containsInAnyOrder(createIdentity1(), createIdentity2())) + } + + @Test + fun `no removed identities are detected without an identity processor`() { + identityChangeDetector.onRemovedIdentity = null + identityChangeDetector.detectChanges(listOf(createIdentity1(), createIdentity3())) + assertThat(removedIdentities, empty()) + } + + @Test + fun `no added identities are detected without an identity processor`() { + identityChangeDetector.onNewIdentity = null + identityChangeDetector.detectChanges(listOf(createIdentity1(), createIdentity2(), createIdentity3(), createIdentity4())) + assertThat(newIdentities, empty()) + } + + private fun createOldIdentities() = + listOf(createIdentity1(), createIdentity2(), createIdentity3()) + + private fun createIdentity1() = + createIdentity("Test1", setOf("Context A", "Context B"), "Key A" to "Value A", "Key B" to "Value B") + + private fun createIdentity2() = + createIdentity("Test2", setOf("Context C", "Context D"), "Key C" to "Value C", "Key D" to "Value D") + + private fun createIdentity3() = + createIdentity("Test3", setOf("Context E", "Context F"), "Key E" to "Value E", "Key F" to "Value F") + + private fun createIdentity4() = + createIdentity("Test4", setOf("Context G", "Context H"), "Key G" to "Value G", "Key H" to "Value H") + +} diff --git a/src/test/kotlin/net/pterodactylus/sone/freenet/wot/IdentityChangeEventSenderTest.kt b/src/test/kotlin/net/pterodactylus/sone/freenet/wot/IdentityChangeEventSenderTest.kt new file mode 100644 index 0000000..0236a3e --- /dev/null +++ b/src/test/kotlin/net/pterodactylus/sone/freenet/wot/IdentityChangeEventSenderTest.kt @@ -0,0 +1,71 @@ +/* + * Sone - IdentityChangeEventSenderTest.kt - Copyright © 2013–2020 David 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 . + */ + +package net.pterodactylus.sone.freenet.wot + +import com.google.common.eventbus.* +import net.pterodactylus.sone.freenet.wot.event.* +import net.pterodactylus.sone.test.* +import org.junit.* +import org.mockito.ArgumentMatchers.eq +import org.mockito.Mockito.verify + +/** + * Unit test for [IdentityChangeEventSender]. + */ +class IdentityChangeEventSenderTest { + + private val eventBus = mock() + private val ownIdentities = listOf( + createOwnIdentity("O1", setOf("Test"), "KeyA" to "ValueA"), + createOwnIdentity("O2", setOf("Test2"), "KeyB" to "ValueB"), + createOwnIdentity("O3", setOf("Test3"), "KeyC" to "ValueC") + ) + private val identities = listOf( + createIdentity("I1", setOf()), + createIdentity("I2", setOf()), + createIdentity("I3", setOf()), + createIdentity("I2", setOf("Test")) + ) + private val identityChangeEventSender = IdentityChangeEventSender(eventBus, createOldIdentities()) + + @Test + fun addingAnOwnIdentityIsDetectedAndReportedCorrectly() { + val newIdentities = createNewIdentities() + identityChangeEventSender.detectChanges(newIdentities) + verify(eventBus).post(eq(OwnIdentityRemovedEvent(ownIdentities[0]))) + verify(eventBus).post(eq(IdentityRemovedEvent(ownIdentities[0], identities[0]))) + verify(eventBus).post(eq(IdentityRemovedEvent(ownIdentities[0], identities[1]))) + verify(eventBus).post(eq(OwnIdentityAddedEvent(ownIdentities[2]))) + verify(eventBus).post(eq(IdentityAddedEvent(ownIdentities[2], identities[1]))) + verify(eventBus).post(eq(IdentityAddedEvent(ownIdentities[2], identities[2]))) + verify(eventBus).post(eq(IdentityRemovedEvent(ownIdentities[1], identities[0]))) + verify(eventBus).post(eq(IdentityAddedEvent(ownIdentities[1], identities[2]))) + verify(eventBus).post(eq(IdentityUpdatedEvent(ownIdentities[1], identities[1]))) + } + + private fun createNewIdentities() = mapOf( + ownIdentities[1] to listOf(identities[3], identities[2]), + ownIdentities[2] to listOf(identities[1], identities[2]) + ) + + private fun createOldIdentities() = mapOf( + ownIdentities[0] to listOf(identities[0], identities[1]), + ownIdentities[1] to listOf(identities[0], identities[1]) + ) + +} diff --git a/src/test/kotlin/net/pterodactylus/sone/freenet/wot/IdentityLoaderTest.kt b/src/test/kotlin/net/pterodactylus/sone/freenet/wot/IdentityLoaderTest.kt new file mode 100644 index 0000000..b3a8447 --- /dev/null +++ b/src/test/kotlin/net/pterodactylus/sone/freenet/wot/IdentityLoaderTest.kt @@ -0,0 +1,115 @@ +/* + * Sone - IdentityLoaderTest.kt - Copyright © 2013–2020 David 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 . + */ + +package net.pterodactylus.sone.freenet.wot + +import org.hamcrest.MatcherAssert.* +import org.hamcrest.Matchers.* +import org.junit.* + +/** + * Unit test for [IdentityLoader]. + */ +class IdentityLoaderTest { + + private val ownIdentities = createOwnIdentities() + private val webOfTrustConnector = object : TestWebOfTrustConnector() { + override fun loadAllOwnIdentities() = ownIdentities.toSet() + override fun loadTrustedIdentities(ownIdentity: OwnIdentity, context: String?) = + when (ownIdentity) { + ownIdentities[0] -> createTrustedIdentitiesForFirstOwnIdentity() + ownIdentities[1] -> createTrustedIdentitiesForSecondOwnIdentity() + ownIdentities[2] -> createTrustedIdentitiesForThirdOwnIdentity() + ownIdentities[3] -> createTrustedIdentitiesForFourthOwnIdentity() + else -> throw RuntimeException() + } + } + + @Test + fun loadingIdentities() { + val identityLoader = IdentityLoader(webOfTrustConnector, Context("Test")) + val identities = identityLoader.loadIdentities() + assertThat(identities.keys, hasSize(4)) + assertThat(identities.keys, containsInAnyOrder(ownIdentities[0], ownIdentities[1], ownIdentities[2], ownIdentities[3])) + verifyIdentitiesForOwnIdentity(identities, ownIdentities[0], createTrustedIdentitiesForFirstOwnIdentity()) + verifyIdentitiesForOwnIdentity(identities, ownIdentities[1], createTrustedIdentitiesForSecondOwnIdentity()) + verifyIdentitiesForOwnIdentity(identities, ownIdentities[2], emptySet()) + verifyIdentitiesForOwnIdentity(identities, ownIdentities[3], createTrustedIdentitiesForFourthOwnIdentity()) + } + + @Test + fun loadingIdentitiesWithoutContext() { + val identityLoaderWithoutContext = IdentityLoader(webOfTrustConnector) + val identities = identityLoaderWithoutContext.loadIdentities() + assertThat(identities.keys, hasSize(4)) + assertThat(identities.keys, containsInAnyOrder(ownIdentities[0], ownIdentities[1], ownIdentities[2], ownIdentities[3])) + verifyIdentitiesForOwnIdentity(identities, ownIdentities[0], createTrustedIdentitiesForFirstOwnIdentity()) + verifyIdentitiesForOwnIdentity(identities, ownIdentities[1], createTrustedIdentitiesForSecondOwnIdentity()) + verifyIdentitiesForOwnIdentity(identities, ownIdentities[2], createTrustedIdentitiesForThirdOwnIdentity()) + verifyIdentitiesForOwnIdentity(identities, ownIdentities[3], createTrustedIdentitiesForFourthOwnIdentity()) + } + + private fun verifyIdentitiesForOwnIdentity(identities: Map>, ownIdentity: OwnIdentity, trustedIdentities: Set) { + assertThat(identities[ownIdentity], equalTo>(trustedIdentities)) + } + +} + +private fun createOwnIdentities() = listOf( + createOwnIdentity("O1", "ON1", "OR1", "OI1", setOf("Test", "Test2"), mapOf("KeyA" to "ValueA", "KeyB" to "ValueB")), + createOwnIdentity("O2", "ON2", "OR2", "OI2", setOf("Test"), mapOf("KeyC" to "ValueC")), + createOwnIdentity("O3", "ON3", "OR3", "OI3", setOf("Test2"), mapOf("KeyE" to "ValueE", "KeyD" to "ValueD")), + createOwnIdentity("O4", "ON4", "OR$", "OI4", setOf("Test"), mapOf("KeyA" to "ValueA", "KeyD" to "ValueD")) +) + +private fun createTrustedIdentitiesForFirstOwnIdentity() = setOf( + createIdentity("I11", "IN11", "IR11", setOf("Test"), mapOf("KeyA" to "ValueA")) +) + +private fun createTrustedIdentitiesForSecondOwnIdentity() = setOf( + createIdentity("I21", "IN21", "IR21", setOf("Test", "Test2"), mapOf("KeyB" to "ValueB")) +) + +private fun createTrustedIdentitiesForThirdOwnIdentity() = setOf( + createIdentity("I31", "IN31", "IR31", setOf("Test", "Test3"), mapOf("KeyC" to "ValueC")) +) + +private fun createTrustedIdentitiesForFourthOwnIdentity(): Set = emptySet() + +private fun createOwnIdentity(id: String, nickname: String, requestUri: String, insertUri: String, contexts: Set, properties: Map): OwnIdentity = + DefaultOwnIdentity(id, nickname, requestUri, insertUri).apply { + setContexts(contexts) + this.properties = properties + } + +private fun createIdentity(id: String, nickname: String, requestUri: String, contexts: Set, properties: Map): Identity = + DefaultIdentity(id, nickname, requestUri).apply { + setContexts(contexts) + this.properties = properties + } + +private open class TestWebOfTrustConnector : WebOfTrustConnector { + + override fun loadAllOwnIdentities() = emptySet() + override fun loadTrustedIdentities(ownIdentity: OwnIdentity, context: String?) = emptySet() + override fun addContext(ownIdentity: OwnIdentity, context: String) = Unit + override fun removeContext(ownIdentity: OwnIdentity, context: String) = Unit + override fun setProperty(ownIdentity: OwnIdentity, name: String, value: String) = Unit + override fun removeProperty(ownIdentity: OwnIdentity, name: String) = Unit + override fun ping() = Unit + +} diff --git a/src/test/kotlin/net/pterodactylus/sone/freenet/wot/PluginWebOfTrustConnectorTest.kt b/src/test/kotlin/net/pterodactylus/sone/freenet/wot/PluginWebOfTrustConnectorTest.kt new file mode 100644 index 0000000..a9c0412 --- /dev/null +++ b/src/test/kotlin/net/pterodactylus/sone/freenet/wot/PluginWebOfTrustConnectorTest.kt @@ -0,0 +1,242 @@ +package net.pterodactylus.sone.freenet.wot + +import freenet.support.* +import freenet.support.api.* +import net.pterodactylus.sone.freenet.* +import net.pterodactylus.sone.freenet.plugin.* +import net.pterodactylus.sone.test.* +import org.hamcrest.* +import org.hamcrest.MatcherAssert.* +import org.hamcrest.Matchers.* +import org.hamcrest.core.* +import kotlin.test.* + +/** + * Unit test for [PluginWebOfTrustConnector]. + */ +class PluginWebOfTrustConnectorTest { + + private val ownIdentity = DefaultOwnIdentity("id", "nickname", "requestUri", "insertUri") + + @Test + fun `wot plugin can be pinged`() { + createPluginConnector("Ping") + .connect { ping() } + } + + @Test + fun `own identities are returned correctly`() { + val ownIdentities = createPluginConnector("GetOwnIdentities") { + put("Identity0", "id-0") + put("RequestURI0", "request-uri-0") + put("InsertURI0", "insert-uri-0") + put("Nickname0", "nickname-0") + put("Contexts0.Context0", "id-0-context-0") + put("Properties0.Property0.Name", "id-0-property-0-name") + put("Properties0.Property0.Value", "id-0-property-0-value") + put("Identity1", "id-1") + put("RequestURI1", "request-uri-1") + put("InsertURI1", "insert-uri-1") + put("Nickname1", "nickname-1") + put("Contexts1.Context0", "id-1-context-0") + put("Properties1.Property0.Name", "id-1-property-0-name") + put("Properties1.Property0.Value", "id-1-property-0-value") + }.connect { loadAllOwnIdentities() } + assertThat(ownIdentities, containsInAnyOrder( + isOwnIdentity("id-0", "nickname-0", "request-uri-0", "insert-uri-0", contains("id-0-context-0"), hasEntry("id-0-property-0-name", "id-0-property-0-value")), + isOwnIdentity("id-1", "nickname-1", "request-uri-1", "insert-uri-1", contains("id-1-context-0"), hasEntry("id-1-property-0-name", "id-1-property-0-value")) + )) + } + + @Test + fun `trusted identities are requested with correct own identity`() { + createPluginConnector("GetIdentitiesByScore", hasField("Truster", equalTo("id"))) + .connect { loadTrustedIdentities(ownIdentity) } + } + + @Test + fun `trusted identities are requested with correct selection parameter`() { + createPluginConnector("GetIdentitiesByScore", hasField("Selection", equalTo("+"))) + .connect { loadTrustedIdentities(ownIdentity) } + } + + @Test + fun `trusted identities are requested with empty context if null context requested`() { + createPluginConnector("GetIdentitiesByScore", hasField("Context", equalTo(""))) + .connect { loadTrustedIdentities(ownIdentity) } + } + + @Test + fun `trusted identities are requested with context if context requested`() { + createPluginConnector("GetIdentitiesByScore", hasField("Context", equalTo("TestContext"))) + .connect { loadTrustedIdentities(ownIdentity, "TestContext") } + } + + @Test + fun `trusted identities are requested with trust values`() { + createPluginConnector("GetIdentitiesByScore", hasField("WantTrustValues", equalTo("true"))) + .connect { loadTrustedIdentities(ownIdentity) } + } + + @Test + fun `empty list of trusted identities is returned correctly`() { + val trustedIdentities = createPluginConnector("GetIdentitiesByScore") + .connect { loadTrustedIdentities(ownIdentity) } + assertThat(trustedIdentities, empty()) + } + + @Test + fun `trusted identities without context, properties, or trust value are returned correctly`() { + val trustedIdentities = createPluginConnector("GetIdentitiesByScore") { + put("Identity0", "id0") + put("Nickname0", "nickname0") + put("RequestURI0", "request-uri0") + put("Identity1", "id1") + put("Nickname1", "nickname1") + put("RequestURI1", "request-uri1") + }.connect { loadTrustedIdentities(ownIdentity) } + assertThat(trustedIdentities, contains( + allOf( + isIdentity("id0", "nickname0", "request-uri0", empty(), isEmptyMap()), + isTrusted(ownIdentity, isTrust(null, null, null)) + ), + allOf( + isIdentity("id1", "nickname1", "request-uri1", empty(), isEmptyMap()), + isTrusted(ownIdentity, isTrust(null, null, null)) + ) + )) + } + + @Test + fun `trusted identity without nickname is returned correctly`() { + val trustedIdentities = createPluginConnector("GetIdentitiesByScore") { + put("Identity0", "id0") + put("RequestURI0", "request-uri0") + }.connect { loadTrustedIdentities(ownIdentity) } + assertThat(trustedIdentities, contains( + allOf( + isIdentity("id0", null, "request-uri0", empty(), isEmptyMap()), + isTrusted(ownIdentity, isTrust(null, null, null)) + ) + )) + } + + @Test + fun `trusted identity with contexts is returned correctly`() { + val trustedIdentities = createPluginConnector("GetIdentitiesByScore") { + put("Identity0", "id0") + put("Nickname0", "nickname0") + put("RequestURI0", "request-uri0") + put("Contexts0.Context0", "Context0") + put("Contexts0.Context1", "Context1") + }.connect { loadTrustedIdentities(ownIdentity) } + assertThat(trustedIdentities, contains( + isIdentity("id0", "nickname0", "request-uri0", containsInAnyOrder("Context0", "Context1"), isEmptyMap()) + )) + } + + @Test + fun `trusted identity with properties is returned correctly`() { + val trustedIdentities = createPluginConnector("GetIdentitiesByScore") { + put("Identity0", "id0") + put("Nickname0", "nickname0") + put("RequestURI0", "request-uri0") + put("Properties0.Property0.Name", "foo") + put("Properties0.Property0.Value", "bar") + put("Properties0.Property1.Name", "baz") + put("Properties0.Property1.Value", "quo") + }.connect { loadTrustedIdentities(ownIdentity) } + assertThat(trustedIdentities, contains( + isIdentity("id0", "nickname0", "request-uri0", empty(), allOf(hasEntry("foo", "bar"), hasEntry("baz", "quo"))) + )) + } + + @Test + fun `trusted identity with trust value is returned correctly`() { + val trustedIdentities = createPluginConnector("GetIdentitiesByScore") { + put("Identity0", "id0") + put("Nickname0", "nickname0") + put("RequestURI0", "request-uri0") + put("Trust0", "12") + put("Score0", "34") + put("Rank0", "56") + }.connect { loadTrustedIdentities(ownIdentity) } + assertThat(trustedIdentities, contains( + allOf( + isIdentity("id0", "nickname0", "request-uri0", empty(), isEmptyMap()), + isTrusted(ownIdentity, isTrust(12, 34, 56)) + ) + )) + } + + @Test + fun `adding a context sends the correct own identity id`() { + createPluginConnector("AddContext", hasField("Identity", equalTo(ownIdentity.id))) + .connect { addContext(ownIdentity, "TestContext") } + } + + @Test + fun `adding a context sends the correct context`() { + createPluginConnector("AddContext", hasField("Context", equalTo("TestContext"))) + .connect { addContext(ownIdentity, "TestContext") } + } + + @Test + fun `removing a context sends the correct own identity id`() { + createPluginConnector("RemoveContext", hasField("Identity", equalTo(ownIdentity.id))) + .connect { removeContext(ownIdentity, "TestContext") } + } + + @Test + fun `removing a context sends the correct context`() { + createPluginConnector("RemoveContext", hasField("Context", equalTo("TestContext"))) + .connect { removeContext(ownIdentity, "TestContext") } + } + + @Test + fun `setting a property sends the correct identity id`() { + createPluginConnector("SetProperty", hasField("Identity", equalTo(ownIdentity.id))) + .connect { setProperty(ownIdentity, "TestProperty", "TestValue") } + } + + @Test + fun `setting a property sends the correct property name`() { + createPluginConnector("SetProperty", hasField("Property", equalTo("TestProperty"))) + .connect { setProperty(ownIdentity, "TestProperty", "TestValue") } + } + + @Test + fun `setting a property sends the correct property value`() { + createPluginConnector("SetProperty", hasField("Value", equalTo("TestValue"))) + .connect { setProperty(ownIdentity, "TestProperty", "TestValue") } + } + + @Test + fun `removing a property sends the correct identity id`() { + createPluginConnector("RemoveProperty", hasField("Identity", equalTo(ownIdentity.id))) + .connect { removeProperty(ownIdentity, "TestProperty") } + } + + @Test + fun `removing a property sends the correct property name`() { + createPluginConnector("RemoveProperty", hasField("Property", equalTo("TestProperty"))) + .connect { removeProperty(ownIdentity, "TestProperty") } + } + +} + +private fun PluginConnector.connect(block: PluginWebOfTrustConnector.() -> R) = + PluginWebOfTrustConnector(this).let(block) + +fun createPluginConnector(message: String, fieldsMatcher: Matcher = IsAnything(), build: SimpleFieldSetBuilder.() -> Unit = {}) = + object : PluginConnector { + override suspend fun sendRequest(pluginName: String, fields: SimpleFieldSet, data: Bucket?) = + if ((pluginName != wotPluginName) || (fields.get("Message") != message)) { + throw PluginException() + } else { + assertThat(fields, fieldsMatcher) + PluginReply(SimpleFieldSetBuilder().apply(build).get(), null) + } + } + +private const val wotPluginName = "plugins.WebOfTrust.WebOfTrust" diff --git a/src/test/kotlin/net/pterodactylus/sone/freenet/wot/WebOfTrustPingerTest.kt b/src/test/kotlin/net/pterodactylus/sone/freenet/wot/WebOfTrustPingerTest.kt new file mode 100644 index 0000000..9cc1084 --- /dev/null +++ b/src/test/kotlin/net/pterodactylus/sone/freenet/wot/WebOfTrustPingerTest.kt @@ -0,0 +1,118 @@ +/** + * Sone - WebOfTrustPingerTest.kt - Copyright © 2019–2020 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 . + */ + +package net.pterodactylus.sone.freenet.wot + +import com.google.common.eventbus.* +import net.pterodactylus.sone.core.event.* +import net.pterodactylus.sone.freenet.plugin.* +import net.pterodactylus.sone.utils.* +import org.hamcrest.MatcherAssert.* +import org.hamcrest.Matchers.* +import java.util.concurrent.atomic.* +import java.util.function.* +import kotlin.test.* + +/** + * Unit test for [WebOfTrustPinger]. + */ +class WebOfTrustPingerTest { + + private val eventBus = EventBus() + private val webOfTrustReachable = AtomicBoolean() + private val webOfTrustReacher = Runnable { webOfTrustReachable.get().onFalse { throw PluginException() } } + private val rescheduled = AtomicBoolean() + private val reschedule: Consumer = Consumer { if (it == pinger) rescheduled.set(true) } + private val pinger = WebOfTrustPinger(eventBus, webOfTrustReacher, reschedule) + + @Test + fun `pinger sends wot appeared event when run first and wot is reachable`() { + webOfTrustReachable.set(true) + val appearedReceived = AtomicBoolean() + eventBus.register(WebOfTrustAppearedCatcher { appearedReceived.set(true) }) + pinger() + assertThat(appearedReceived.get(), equalTo(true)) + } + + @Test + fun `pinger reschedules when wot is reachable`() { + webOfTrustReachable.set(true) + pinger() + assertThat(rescheduled.get(), equalTo(true)) + } + + @Test + fun `pinger sends wot disappeared event when run first and wot is not reachable`() { + val appearedReceived = AtomicBoolean() + eventBus.register(WebOfTrustAppearedCatcher { appearedReceived.set(true) }) + pinger() + assertThat(appearedReceived.get(), equalTo(false)) + } + + @Test + fun `pinger reschedules when wot is not reachable`() { + pinger() + assertThat(rescheduled.get(), equalTo(true)) + } + + @Test + fun `pinger sends wot disappeared event when run twice and wot is not reachable on second call`() { + val disappearedReceived = AtomicBoolean() + eventBus.register(WebOfTrustDisappearedCatcher { disappearedReceived.set(true) }) + webOfTrustReachable.set(true) + pinger() + webOfTrustReachable.set(false) + pinger() + assertThat(disappearedReceived.get(), equalTo(true)) + } + + @Test + fun `pinger sends wot appeared event only once`() { + webOfTrustReachable.set(true) + val appearedReceived = AtomicBoolean() + eventBus.register(WebOfTrustAppearedCatcher { appearedReceived.set(true) }) + pinger() + appearedReceived.set(false) + pinger() + assertThat(appearedReceived.get(), equalTo(false)) + } + + @Test + fun `pinger sends wot disappeared event only once`() { + val disappearedReceived = AtomicBoolean() + eventBus.register(WebOfTrustDisappearedCatcher { disappearedReceived.set(true) }) + pinger() + disappearedReceived.set(false) + pinger() + assertThat(disappearedReceived.get(), equalTo(false)) + } + +} + +private class WebOfTrustAppearedCatcher(private val received: () -> Unit) { + @Subscribe + fun webOfTrustAppeared(@Suppress("UNUSED_PARAMETER") webOfTrustAppeared: WebOfTrustAppeared) { + received() + } +} + +private class WebOfTrustDisappearedCatcher(private val received: () -> Unit) { + @Subscribe + fun webOfTrustDisappeared(@Suppress("UNUSED_PARAMETER") webOfTrustDisappeared: WebOfTrustDisappeared) { + received() + } +} diff --git a/src/test/kotlin/net/pterodactylus/sone/main/FreenetModuleTest.kt b/src/test/kotlin/net/pterodactylus/sone/main/FreenetModuleTest.kt index 2f55d52..c6aed88 100644 --- a/src/test/kotlin/net/pterodactylus/sone/main/FreenetModuleTest.kt +++ b/src/test/kotlin/net/pterodactylus/sone/main/FreenetModuleTest.kt @@ -5,10 +5,13 @@ import freenet.client.* import freenet.clients.http.* import freenet.node.* import freenet.pluginmanager.* +import net.pterodactylus.sone.freenet.plugin.* import net.pterodactylus.sone.test.* import org.hamcrest.MatcherAssert.* import org.hamcrest.Matchers.* import org.junit.* +import org.junit.rules.* +import org.mockito.* import org.mockito.Mockito.* /** @@ -16,6 +19,10 @@ import org.mockito.Mockito.* */ class FreenetModuleTest { + @Rule + @JvmField + val expectedException = ExpectedException.none()!! + private val sessionManager = mock() private val pluginRespirator = deepMock().apply { whenever(getSessionManager("Sone")).thenReturn(sessionManager) @@ -27,20 +34,10 @@ class FreenetModuleTest { private val module = FreenetModule(pluginRespirator) private val injector = Guice.createInjector(module) - private inline fun verifySingletonInstance() { - val firstInstance = injector.getInstance() - val secondInstance = injector.getInstance() - assertThat(firstInstance, sameInstance(secondInstance)) - } - @Test - fun `plugin respirator is returned correctly`() { - assertThat(injector.getInstance(), sameInstance(pluginRespirator)) - } - - @Test - fun `plugin respirator is returned as singleton`() { - verifySingletonInstance() + fun `plugin respirator is not bound`() { + expectedException.expect(Exception::class.java) + injector.getInstance() } @Test @@ -50,7 +47,7 @@ class FreenetModuleTest { @Test fun `node is returned as singleton`() { - verifySingletonInstance() + injector.verifySingletonInstance() } @Test @@ -60,7 +57,7 @@ class FreenetModuleTest { @Test fun `high level simply client is returned as singleton`() { - verifySingletonInstance() + injector.verifySingletonInstance() } @Test @@ -70,7 +67,7 @@ class FreenetModuleTest { @Test fun `session manager is returned as singleton`() { - verifySingletonInstance() + injector.verifySingletonInstance() verify(pluginRespirator).getSessionManager("Sone") } @@ -81,7 +78,7 @@ class FreenetModuleTest { @Test fun `toadlet container is returned as singleten`() { - verifySingletonInstance() + injector.verifySingletonInstance() } @Test @@ -90,8 +87,30 @@ class FreenetModuleTest { } @Test - fun `page maker is returned as singleten`() { - verifySingletonInstance() + fun `page maker is returned as singleton`() { + injector.verifySingletonInstance() + } + + @Test + fun `plugin respirator facade is returned correctly`() { + val pluginRespiratorFacade = injector.getInstance() + pluginRespiratorFacade.getPluginTalker(mock(), "test.plugin", "test-request-1") + verify(pluginRespirator).getPluginTalker(any(), ArgumentMatchers.eq("test.plugin"), ArgumentMatchers.eq("test-request-1")) + } + + @Test + fun `plugin respirator facade is returned as singleton`() { + injector.verifySingletonInstance() + } + + @Test + fun `plugin connector is returned correctly`() { + assertThat(injector.getInstance(), notNullValue()) + } + + @Test + fun `plugin connector facade is returned as singleton`() { + injector.verifySingletonInstance() } } diff --git a/src/test/kotlin/net/pterodactylus/sone/main/SoneModuleCreatorTest.kt b/src/test/kotlin/net/pterodactylus/sone/main/SoneModuleCreatorTest.kt deleted file mode 100644 index 0286032..0000000 --- a/src/test/kotlin/net/pterodactylus/sone/main/SoneModuleCreatorTest.kt +++ /dev/null @@ -1,176 +0,0 @@ -package net.pterodactylus.sone.main - -import com.google.common.base.* -import com.google.common.eventbus.* -import com.google.inject.* -import com.google.inject.name.Names.* -import net.pterodactylus.sone.database.* -import net.pterodactylus.sone.database.memory.* -import net.pterodactylus.sone.freenet.wot.* -import net.pterodactylus.sone.test.* -import net.pterodactylus.util.config.* -import net.pterodactylus.util.version.Version -import org.hamcrest.MatcherAssert.* -import org.hamcrest.Matchers.* -import org.junit.* -import java.io.* -import java.util.concurrent.atomic.* - -class SoneModuleCreatorTest { - - private val currentDir: File = File(".") - private val pluginVersion = Version("", 0, 1, 2) - private val pluginYear = 2019 - private val pluginHomepage = "home://page" - private val sonePlugin = mock().apply { - whenever(version).thenReturn(pluginVersion.toString()) - whenever(year).thenReturn(pluginYear) - whenever(homepage).thenReturn(pluginHomepage) - } - - @After - fun removePropertiesFromCurrentDirectory() { - File(currentDir, "sone.properties").delete() - } - - @Test - fun `creator binds configuration when no file is present`() { - File(currentDir, "sone.properties").delete() - assertThat(getInstance(), notNullValue()) - } - - @Test - fun `creator binds first start to true when no file is present`() { - File(currentDir, "sone.properties").delete() - assertThat(getInstance(named("FirstStart")), equalTo(true)) - } - - @Test - fun `config file is created in current directory if not present`() { - File(currentDir, "sone.properties").delete() - val configuration = getInstance() - configuration.save() - assertThat(File(currentDir, "sone.properties").exists(), equalTo(true)) - } - - @Test - fun `creator binds configuration when file is present`() { - File(currentDir, "sone.properties").writeText("Option=old") - assertThat(getInstance().getStringValue("Option").value, equalTo("old")) - } - - @Test - fun `creator binds first start to false when file is present`() { - File(currentDir, "sone.properties").writeText("Option=old") - assertThat(getInstance(named("FirstStart")), equalTo(false)) - } - - @Test - fun `invalid config file leads to new config being created`() { - File(currentDir, "sone.properties").writeText("Option=old\nbroken") - val configuration = getInstance() - assertThat(configuration.getStringValue("Option").getValue(null), nullValue()) - } - - @Test - fun `invalid config file leads to new config being set to true`() { - File(currentDir, "sone.properties").writeText("Option=old\nbroken") - assertThat(getInstance(named("NewConfig")), equalTo(true)) - } - - @Test - fun `valid config file leads to new config being set to false`() { - File(currentDir, "sone.properties").writeText("Option=old") - assertThat(getInstance(named("NewConfig")), equalTo(false)) - } - - @Test - fun `event bus is bound`() { - assertThat(getInstance(), notNullValue()) - } - - @Test - fun `context is bound`() { - assertThat(getInstance().context, equalTo("Sone")) - } - - @Test - fun `optional context is bound`() { - assertThat(getInstance>().get().context, equalTo("Sone")) - } - - @Test - fun `sone plugin is bound`() { - assertThat(getInstance(), sameInstance(sonePlugin)) - } - - @Test - fun `version is bound`() { - assertThat(getInstance(), equalTo(pluginVersion)) - } - - @Test - fun `plugin version is bound`() { - assertThat(getInstance(), equalTo(PluginVersion(pluginVersion.toString()))) - } - - @Test - fun `plugin year is bound`() { - assertThat(getInstance(), equalTo(PluginYear(pluginYear))) - } - - @Test - fun `plugin homepage in bound`() { - assertThat(getInstance(), equalTo(PluginHomepage(pluginHomepage))) - } - - @Test - fun `database is bound correctly`() { - assertThat(getInstance(), instanceOf(MemoryDatabase::class.java)) - } - - @Test - fun `default loader is used without dev options`() { - assertThat(getInstance(), instanceOf(DefaultLoaders::class.java)) - } - - @Test - fun `default loaders are used if no path is given`() { - File(currentDir, "sone.properties").writeText("Developer.LoadFromFilesystem=true") - assertThat(getInstance(), instanceOf(DefaultLoaders::class.java)) - } - - @Test - fun `debug loaders are used if path is given`() { - File(currentDir, "sone.properties").writeText("Developer.LoadFromFilesystem=true\nDeveloper.FilesystemPath=/tmp") - assertThat(getInstance(), instanceOf(DebugLoaders::class.java)) - } - - class TestObject { - val ref: AtomicReference = AtomicReference() - @Subscribe - fun testEvent(event: Any?) { - ref.set(event) - } - } - - @Test - fun `created objects are registered with event bus`() { - val injector = createInjector() - val eventBus: EventBus = getInstance(injector = injector) - val testObject = getInstance(injector = injector) - val event = Any() - eventBus.post(event) - assertThat(testObject.ref.get(), sameInstance(event)) - } - - private fun createInjector(): Injector = SoneModuleCreator() - .createModule(sonePlugin) - .let { Guice.createInjector(it) } - - private inline fun getInstance(annotation: Annotation? = null, injector: Injector = createInjector()): R = - annotation - ?.let { injector.getInstance(Key.get(object : TypeLiteral() {}, it)) } - ?: injector.getInstance(Key.get(object : TypeLiteral() {})) - -} diff --git a/src/test/kotlin/net/pterodactylus/sone/main/SoneModuleTest.kt b/src/test/kotlin/net/pterodactylus/sone/main/SoneModuleTest.kt new file mode 100644 index 0000000..c628334 --- /dev/null +++ b/src/test/kotlin/net/pterodactylus/sone/main/SoneModuleTest.kt @@ -0,0 +1,231 @@ +package net.pterodactylus.sone.main + +import com.codahale.metrics.* +import com.google.common.base.* +import com.google.common.eventbus.* +import com.google.inject.Guice.* +import com.google.inject.name.Names.* +import freenet.l10n.* +import net.pterodactylus.sone.core.* +import net.pterodactylus.sone.database.* +import net.pterodactylus.sone.database.memory.* +import net.pterodactylus.sone.freenet.* +import net.pterodactylus.sone.freenet.plugin.* +import net.pterodactylus.sone.freenet.wot.* +import net.pterodactylus.sone.test.* +import net.pterodactylus.util.config.* +import net.pterodactylus.util.version.Version +import org.hamcrest.MatcherAssert.* +import org.hamcrest.Matchers.* +import org.junit.experimental.categories.* +import org.mockito.Mockito.* +import java.io.* +import java.util.concurrent.* +import java.util.concurrent.atomic.* +import kotlin.test.* + +const val versionString = "v80" + +@Category(NotParallel::class) +class SoneModuleTest { + + private val currentDir: File = File(".") + private val pluginVersion = Version("", 80) + private val pluginYear = 2019 + private val pluginHomepage = "home://page" + private val l10n = deepMock() + private val sonePlugin = mock().apply { + whenever(version).thenReturn(versionString) + whenever(year).thenReturn(pluginYear) + whenever(homepage).thenReturn(pluginHomepage) + whenever(l10n()).thenReturn(l10n) + } + + private val injector by lazy { + createInjector( + SoneModule(sonePlugin, EventBus()), + FreenetInterface::class.isProvidedByDeepMock(), + PluginRespiratorFacade::class.isProvidedByDeepMock(), + PluginConnector::class.isProvidedByDeepMock() + ) + } + + @AfterTest + fun removePropertiesFromCurrentDirectory() { + File(currentDir, "sone.properties").delete() + } + + @Test + fun `creator binds configuration when no file is present`() { + File(currentDir, "sone.properties").delete() + assertThat(injector.getInstance(), notNullValue()) + } + + @Test + fun `creator binds first start to true when no file is present`() { + File(currentDir, "sone.properties").delete() + assertThat(injector.getInstance(named("FirstStart")), equalTo(true)) + } + + @Test + fun `config file is created in current directory if not present`() { + File(currentDir, "sone.properties").delete() + val configuration = injector.getInstance() + configuration.save() + assertThat(File(currentDir, "sone.properties").exists(), equalTo(true)) + } + + @Test + fun `creator binds configuration when file is present`() { + File(currentDir, "sone.properties").writeText("Option=old") + assertThat(injector.getInstance().getStringValue("Option").value, equalTo("old")) + } + + @Test + fun `creator binds first start to false when file is present`() { + File(currentDir, "sone.properties").writeText("Option=old") + assertThat(injector.getInstance(named("FirstStart")), equalTo(false)) + } + + @Test + fun `invalid config file leads to new config being created`() { + File(currentDir, "sone.properties").writeText("Option=old\nbroken") + val configuration = injector.getInstance() + assertThat(configuration.getStringValue("Option").getValue(null), nullValue()) + } + + @Test + fun `invalid config file leads to new config being set to true`() { + File(currentDir, "sone.properties").writeText("Option=old\nbroken") + assertThat(injector.getInstance(named("NewConfig")), equalTo(true)) + } + + @Test + fun `valid config file leads to new config being set to false`() { + File(currentDir, "sone.properties").writeText("Option=old") + assertThat(injector.getInstance(named("NewConfig")), equalTo(false)) + } + + @Test + fun `event bus is bound`() { + assertThat(injector.getInstance(), notNullValue()) + } + + @Test + fun `context is bound`() { + assertThat(injector.getInstance().context, equalTo("Sone")) + } + + @Test + fun `optional context is bound`() { + assertThat(injector.getInstance>().get().context, equalTo("Sone")) + } + + @Test + fun `sone plugin is bound`() { + assertThat(injector.getInstance(), sameInstance(sonePlugin)) + } + + @Test + fun `version is bound`() { + assertThat(injector.getInstance(), equalTo(pluginVersion)) + } + + @Test + fun `plugin version is bound`() { + assertThat(injector.getInstance(), equalTo(PluginVersion(versionString))) + } + + @Test + fun `plugin year is bound`() { + assertThat(injector.getInstance(), equalTo(PluginYear(pluginYear))) + } + + @Test + fun `plugin homepage in bound`() { + assertThat(injector.getInstance(), equalTo(PluginHomepage(pluginHomepage))) + } + + @Test + fun `database is bound correctly`() { + assertThat(injector.getInstance(), instanceOf(MemoryDatabase::class.java)) + } + + @Test + fun `translation is bound correctly`() { + assertThat(injector.getInstance(), notNullValue()) + } + + @Test + fun `default loader is used without dev options`() { + assertThat(injector.getInstance(), instanceOf(DefaultLoaders::class.java)) + } + + @Test + fun `default loaders are used if no path is given`() { + File(currentDir, "sone.properties").writeText("Developer.LoadFromFilesystem=true") + assertThat(injector.getInstance(), instanceOf(DefaultLoaders::class.java)) + } + + @Test + fun `debug loaders are used if path is given`() { + File(currentDir, "sone.properties").writeText("Developer.LoadFromFilesystem=true\nDeveloper.FilesystemPath=/tmp") + assertThat(injector.getInstance(), instanceOf(DebugLoaders::class.java)) + } + + class TestObject { + val ref: AtomicReference = AtomicReference() + @Subscribe + fun testEvent(event: Any?) { + ref.set(event) + } + } + + @Test + fun `created objects are registered with event bus`() { + val eventBus: EventBus = injector.getInstance() + val testObject = injector.getInstance() + val event = Any() + eventBus.post(event) + assertThat(testObject.ref.get(), sameInstance(event)) + } + + @Test + fun `core is created as singleton`() { + injector.verifySingletonInstance() + } + + @Test + fun `core is registered with event bus`() { + val eventBus = mock() + val injector = createInjector( + SoneModule(sonePlugin, eventBus), + FreenetInterface::class.isProvidedByDeepMock(), + PluginRespiratorFacade::class.isProvidedByDeepMock(), + PluginConnector::class.isProvidedByDeepMock() + ) + val core = injector.getInstance() + verify(eventBus).register(core) + } + + @Test + fun `metrics registry is created as singleton`() { + injector.verifySingletonInstance() + } + + @Test + fun `wot connector is created as singleton`() { + injector.verifySingletonInstance() + } + + @Test + fun `notification ticker is created as singleton`() { + injector.verifySingletonInstance(named("notification")) + } + + @Test + fun `ticker shutdown is created as singleton`() { + injector.verifySingletonInstance() + } + +} diff --git a/src/test/kotlin/net/pterodactylus/sone/main/SonePluginTest.kt b/src/test/kotlin/net/pterodactylus/sone/main/SonePluginTest.kt index 6d57b9f..d1e2cec 100644 --- a/src/test/kotlin/net/pterodactylus/sone/main/SonePluginTest.kt +++ b/src/test/kotlin/net/pterodactylus/sone/main/SonePluginTest.kt @@ -1,19 +1,34 @@ package net.pterodactylus.sone.main -import freenet.client.async.USKManager -import freenet.l10n.BaseL10n.LANGUAGE.ENGLISH -import freenet.node.Node -import freenet.node.NodeClientCore -import freenet.pluginmanager.PluginRespirator +import com.google.common.eventbus.* +import com.google.inject.* +import freenet.client.async.* +import freenet.l10n.BaseL10n.LANGUAGE.* +import freenet.node.* +import freenet.pluginmanager.* +import net.pterodactylus.sone.core.* +import net.pterodactylus.sone.core.event.* +import net.pterodactylus.sone.fcp.* +import net.pterodactylus.sone.freenet.wot.* import net.pterodactylus.sone.test.* -import org.junit.Test +import net.pterodactylus.sone.web.* +import net.pterodactylus.sone.web.notification.* +import org.hamcrest.MatcherAssert.* +import org.hamcrest.Matchers.* +import org.junit.experimental.categories.* +import org.mockito.Mockito.* +import java.io.* +import java.util.concurrent.atomic.* +import kotlin.test.* /** * Unit test for [SonePlugin]. */ +@Dirty +@Category(NotParallel::class) class SonePluginTest { - private val sonePlugin = SonePlugin() + private var sonePlugin = SonePlugin { injector } private val pluginRespirator = deepMock() private val node = deepMock() private val clientCore = deepMock() @@ -31,4 +46,204 @@ class SonePluginTest { sonePlugin.runPlugin(pluginRespirator) } + @Test + fun `core can be created`() { + val injector: Injector = runSonePluginWithRealInjector() + assertThat(injector.getInstance(), notNullValue()) + } + + @Test + fun `fcp interface can be created`() { + val injector: Injector = runSonePluginWithRealInjector() + assertThat(injector.getInstance(), notNullValue()) + } + + @Test + fun `web interface can be created`() { + val injector: Injector = runSonePluginWithRealInjector() + assertThat(injector.getInstance(), notNullValue()) + } + + @Test + fun `web of trust connector can be created`() { + val injector: Injector = runSonePluginWithRealInjector() + assertThat(injector.getInstance(), notNullValue()) + } + + @Test + fun `notification handler can be created`() { + val injector: Injector = runSonePluginWithRealInjector() + assertThat(injector.getInstance(), notNullValue()) + } + + private fun runSonePluginWithRealInjector(injectorConsumer: (Injector) -> Unit = {}): Injector { + lateinit var injector: Injector + sonePlugin = SonePlugin { + Guice.createInjector(*it).also { + injector = it + injectorConsumer(it) + } + } + sonePlugin.setLanguage(ENGLISH) + sonePlugin.runPlugin(pluginRespirator) + return injector + } + + @Test + fun `core is being started`() { + sonePlugin.runPlugin(pluginRespirator) + val core = injector.getInstance() + verify(core).start() + } + + @Test + fun `notification handler is being requested`() { + sonePlugin.runPlugin(pluginRespirator) + assertThat(getInjected(NotificationHandler::class.java), notNullValue()) + } + + @Test + fun `ticker shutdown is being requested`() { + sonePlugin.runPlugin(pluginRespirator) + assertThat(getInjected(TickerShutdown::class.java), notNullValue()) + } + + private class FirstStartListener(private val firstStartReceived: AtomicBoolean) { + @Subscribe + fun firstStart(@Suppress("UNUSED_PARAMETER") firstStart: FirstStart) { + firstStartReceived.set(true) + } + } + + @Test + fun `first-start event is sent to event bus when first start is true`() { + File("sone.properties").delete() + val firstStartReceived = AtomicBoolean() + runSonePluginWithRealInjector { + val eventBus = it.getInstance(EventBus::class.java) + eventBus.register(FirstStartListener(firstStartReceived)) + } + assertThat(firstStartReceived.get(), equalTo(true)) + } + + @Test + fun `first-start event is not sent to event bus when first start is false`() { + File("sone.properties").deleteAfter { + writeText("# empty") + val firstStartReceived = AtomicBoolean() + runSonePluginWithRealInjector { + val eventBus = it.getInstance(EventBus::class.java) + eventBus.register(FirstStartListener(firstStartReceived)) + } + assertThat(firstStartReceived.get(), equalTo(false)) + } + } + + private class ConfigNotReadListener(private val configNotReadReceiver: AtomicBoolean) { + @Subscribe + fun configNotRead(@Suppress("UNUSED_PARAMETER") configNotRead: ConfigNotRead) { + configNotReadReceiver.set(true) + } + } + + @Test + fun `config-not-read event is sent to event bus when new config is true`() { + File("sone.properties").deleteAfter { + writeText("Invalid") + val configNotReadReceived = AtomicBoolean() + runSonePluginWithRealInjector { + val eventBus = it.getInstance(EventBus::class.java) + eventBus.register(ConfigNotReadListener(configNotReadReceived)) + } + assertThat(configNotReadReceived.get(), equalTo(true)) + } + } + + @Test + fun `config-not-read event is not sent to event bus when first start is true`() { + File("sone.properties").delete() + val configNotReadReceived = AtomicBoolean() + runSonePluginWithRealInjector { + val eventBus = it.getInstance(EventBus::class.java) + eventBus.register(ConfigNotReadListener(configNotReadReceived)) + } + assertThat(configNotReadReceived.get(), equalTo(false)) + } + + @Test + fun `config-not-read event is not sent to event bus when new config is false`() { + File("sone.properties").deleteAfter { + writeText("# comment") + val configNotReadReceived = AtomicBoolean() + runSonePluginWithRealInjector { + val eventBus = it.getInstance(EventBus::class.java) + eventBus.register(ConfigNotReadListener(configNotReadReceived)) + } + assertThat(configNotReadReceived.get(), equalTo(false)) + } + } + + private class StartupListener(private val startupReceived: () -> Unit) { + @Subscribe + fun startup(@Suppress("UNUSED_PARAMETER") startup: Startup) { + startupReceived() + } + } + + @Test + fun `startup event is sent to event bus`() { + val startupReceived = AtomicBoolean() + runSonePluginWithRealInjector { + val eventBus = it.getInstance(EventBus::class.java) + eventBus.register(StartupListener { startupReceived.set(true) }) + } + assertThat(startupReceived.get(), equalTo(true)) + } + + private class ShutdownListener(private val shutdownReceived: () -> Unit) { + @Subscribe + fun shutdown(@Suppress("UNUSED_PARAMETER") shutdown: Shutdown) { + shutdownReceived() + } + } + + @Test + fun `shutdown event is sent to event bus on terminate`() { + val shutdownReceived = AtomicBoolean() + runSonePluginWithRealInjector { + val eventBus = it.getInstance(EventBus::class.java) + eventBus.register(ShutdownListener { shutdownReceived.set(true) }) + } + sonePlugin.terminate() + assertThat(shutdownReceived.get(), equalTo(true)) + } + + private fun getInjected(clazz: Class, annotation: Annotation? = null): T? = + injected[TypeLiteral.get(clazz) to annotation] as? T + + private val injected = + mutableMapOf, Annotation?>, Any>() + + private val injector = mock().apply { + fun mockValue(clazz: Class<*>) = false.takeIf { clazz.name == java.lang.Boolean::class.java.name } ?: mock(clazz) + whenever(getInstance(any>())).then { + injected.getOrPut((it.getArgument(0) as Key<*>).let { it.typeLiteral to it.annotation }) { + it.getArgument>(0).typeLiteral.type.typeName.toClass().let(::mockValue) + } + } + whenever(getInstance(any>())).then { + injected.getOrPut(TypeLiteral.get(it.getArgument(0) as Class<*>) to null) { + it.getArgument>(0).let(::mockValue) + } + } + } + +} + +private fun String.toClass(): Class<*> = SonePlugin::class.java.classLoader.loadClass(this) + +private fun File.deleteAfter(action: File.() -> Unit) = try { + action(this) +} finally { + this.delete() } diff --git a/src/test/kotlin/net/pterodactylus/sone/main/TickerShutdownTest.kt b/src/test/kotlin/net/pterodactylus/sone/main/TickerShutdownTest.kt new file mode 100644 index 0000000..827131f --- /dev/null +++ b/src/test/kotlin/net/pterodactylus/sone/main/TickerShutdownTest.kt @@ -0,0 +1,46 @@ +/** + * Sone - TickerShutdownTest.kt - Copyright © 2019–2020 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 . + */ + +package net.pterodactylus.sone.main + +import com.google.common.eventbus.* +import net.pterodactylus.sone.core.event.* +import net.pterodactylus.sone.test.* +import org.mockito.Mockito.* +import java.util.concurrent.* +import kotlin.test.* + +/** + * Unit test for [TickerShutdown]. + */ +@Suppress("UnstableApiUsage") +class TickerShutdownTest { + + private val eventBus = EventBus() + private val notificationTicker = mock() + + init { + eventBus.register(TickerShutdown(notificationTicker)) + } + + @Test + fun `ticker is shutdown on shutdown`() { + eventBus.post(Shutdown()) + verify(notificationTicker).shutdown() + } + +} diff --git a/src/test/kotlin/net/pterodactylus/sone/notify/ListNotificationTest.kt b/src/test/kotlin/net/pterodactylus/sone/notify/ListNotificationTest.kt new file mode 100644 index 0000000..4c82035 --- /dev/null +++ b/src/test/kotlin/net/pterodactylus/sone/notify/ListNotificationTest.kt @@ -0,0 +1,125 @@ +package net.pterodactylus.sone.notify + +import net.pterodactylus.util.notify.* +import net.pterodactylus.util.template.* +import org.hamcrest.MatcherAssert.* +import org.hamcrest.Matchers.* +import org.junit.* +import java.util.concurrent.atomic.* + +/** + * Unit test for [ListNotification]. + */ +class ListNotificationTest { + + private val template = Template() + private val listNotification = ListNotification(ID, KEY, template) + + @Test + fun `creating a list notification sets empty iterable on element key in template context`() { + assertThat(template.initialContext.get(KEY) as Iterable<*>, emptyIterable()) + } + + @Test + @Suppress("UNCHECKED_CAST") + fun `list in template context gets updated when elements are added`() { + listNotification.add("a") + listNotification.add("b") + assertThat(template.initialContext.get(KEY) as Iterable, contains("a", "b")) + } + + @Test + fun `new list notification has no element`() { + assertThat(listNotification.elements, emptyIterable()) + } + + @Test + fun `new list notification is empty`() { + assertThat(listNotification.isEmpty, equalTo(true)) + } + + @Test + fun `list notification retains set elements`() { + listNotification.setElements(listOf("a", "b", "c")) + assertThat(listNotification.elements, contains("a", "b", "c")) + } + + @Test + fun `list notification deduplicates set elements`() { + listNotification.setElements(listOf("a", "b", "a")) + assertThat(listNotification.elements, contains("a", "b")) + } + + @Test + fun `list notification retains added elements`() { + listNotification.add("a") + listNotification.add("b") + listNotification.add("c") + assertThat(listNotification.elements, contains("a", "b", "c")) + } + + @Test + fun `list notification deduplicates elements`() { + listNotification.add("a") + listNotification.add("b") + listNotification.add("a") + assertThat(listNotification.elements, contains("a", "b")) + } + + @Test + fun `list notification removes correct element`() { + listNotification.setElements(listOf("a", "b", "c")) + listNotification.remove("b") + assertThat(listNotification.elements, contains("a", "c")) + } + + @Test + fun `removing the last element dismisses the notification`() { + val notificationDismissed = AtomicBoolean() + val notificationListener = NotificationListener { notificationDismissed.set(it == listNotification) } + listNotification.addNotificationListener(notificationListener) + listNotification.add("a") + listNotification.remove("a") + assertThat(notificationDismissed.get(), equalTo(true)) + } + + @Test + fun `dismissing the list notification removes all elements`() { + listNotification.setElements(listOf("a", "b", "c")) + listNotification.dismiss() + assertThat(listNotification.elements, emptyIterable()) + } + + @Test + fun `list notification with different elements is not equal`() { + val secondNotification = ListNotification(ID, KEY, template) + listNotification.add("a") + secondNotification.add("b") + assertThat(listNotification, not(equalTo(secondNotification))) + } + + @Test + fun `list notification with different key is not equal`() { + val secondNotification = ListNotification(ID, OTHER_KEY, template) + assertThat(listNotification, not(equalTo(secondNotification))) + } + + @Test + fun `copied notifications have the same hash code`() { + val secondNotification = ListNotification(listNotification) + listNotification.add("a") + secondNotification.add("a") + listNotification.setLastUpdateTime(secondNotification.lastUpdatedTime) + assertThat(listNotification.hashCode(), equalTo(secondNotification.hashCode())) + } + + @Test + fun `list notification is not equal to other objects`() { + assertThat(listNotification, not(equalTo(Any()))) + } + +} + +private const val ID = "notification-id" +private const val KEY = "element-key" +private const val OTHER_KEY = "other-key" diff --git a/src/test/kotlin/net/pterodactylus/sone/template/DurationFormatFilterTest.kt b/src/test/kotlin/net/pterodactylus/sone/template/DurationFormatFilterTest.kt new file mode 100644 index 0000000..0af4a8e --- /dev/null +++ b/src/test/kotlin/net/pterodactylus/sone/template/DurationFormatFilterTest.kt @@ -0,0 +1,98 @@ +/** + * Sone - DurationFormatFilterTest.kt - Copyright © 2019–2020 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 . + */ + +package net.pterodactylus.sone.template + +import org.hamcrest.MatcherAssert.* +import org.hamcrest.Matchers.* +import kotlin.test.* + +class DurationFormatFilterTest { + + private val filter = DurationFormatFilter() + + @Test + fun `random object is returned as it is`() { + val randomObject = Any() + assertThat(filter.format(null, randomObject, emptyMap()), sameInstance(randomObject)) + } + + @Test + fun `integer 0 is rendered as “0s”`() { + verifyDuration(0, "0s") + } + + @Test + fun `long 0 is rendered as “0s”`() { + verifyDuration(0L, "0s") + } + + @Test + fun `12 is rendered as “12_0s”`() { + verifyDuration(12, "12.0s") + } + + @Test + fun `123 is rendered as “2_1m”`() { + verifyDuration(123, "2.1m") + } + + @Test + fun `12345 is rendered as “3_4h”`() { + verifyDuration(12345, "3.4h") + } + + @Test + fun `123456 is rendered as “1_4d”`() { + verifyDuration(123456, "1.4d") + } + + @Test + fun `1234567 is rendered as “2_0w”`() { + verifyDuration(1234567, "2.0w") + } + + @Test + fun `123456789 with scale ms is rendered as “1_4d”`() { + verifyDuration(123456789, "1.4d", "ms") + } + + @Test + fun `123456789 with scale μs is rendered as “2_1m”`() { + verifyDuration(123456789, "2.1m", "μs") + } + + @Test + fun `123456789 with scale ns is rendered as “123_5ms”`() { + verifyDuration(123456789, "123.5ms", "ns") + } + + @Test + fun `123456 with scale ns is rendered as “123_5μs”`() { + verifyDuration(123456, "123.5μs", "ns") + } + + @Test + fun `123 with scale ns is rendered as “123_0ns”`() { + verifyDuration(123, "123.0ns", "ns") + } + + private fun verifyDuration(value: Any, expectedRendering: String, scale: String? = null) { + assertThat(filter.format(null, value, scale?.let { mapOf("scale" to scale) } ?: emptyMap()), equalTo(expectedRendering)) + } + +} diff --git a/src/test/kotlin/net/pterodactylus/sone/template/FilesystemTemplateTest.kt b/src/test/kotlin/net/pterodactylus/sone/template/FilesystemTemplateTest.kt new file mode 100644 index 0000000..f6c704c --- /dev/null +++ b/src/test/kotlin/net/pterodactylus/sone/template/FilesystemTemplateTest.kt @@ -0,0 +1,117 @@ +package net.pterodactylus.sone.template + +import net.pterodactylus.util.template.* +import org.hamcrest.MatcherAssert.* +import org.hamcrest.Matchers.* +import org.junit.* +import org.junit.rules.* +import java.io.* +import java.lang.Thread.* +import kotlin.test.Test + +/** + * Unit test for [FilesystemTemplate]. + */ +class FilesystemTemplateTest() { + + @Rule + @JvmField + val expectedException: ExpectedException = ExpectedException.none() + + private val tempFile = File.createTempFile("template-", ".dat") + private val filesystemTemplate: FilesystemTemplate + private val templateContext = TemplateContext() + + private val renderedString: String + get() = + StringWriter().use { stringWriter -> + filesystemTemplate.render(templateContext, stringWriter) + stringWriter + }.toString() + + init { + writeTemplate("Text") + filesystemTemplate = FilesystemTemplate(tempFile.absolutePath) + } + + private fun writeTemplate(text: String) { + tempFile.writer().use { + it.write("$text.<%foreach values value><% value><%/foreach>") + } + } + + @Before + fun setupTemplateContext() { + templateContext.set("values", listOf("a", 1)) + } + + @Test + fun `loading template from non existing file throws exception`() { + val filesystemTemplate = FilesystemTemplate("/a/b/c.dat") + expectedException.expect(FilesystemTemplate.TemplateFileNotFoundException::class.java) + filesystemTemplate.initialContext + } + + @Test + fun `template can be loaded from the filesystem`() { + assertThat(renderedString, equalTo("Text.a1")) + } + + @Test + fun `template can be reloaded`() { + assertThat(renderedString, equalTo("Text.a1")) + sleep(1000) + writeTemplate("New") + assertThat(renderedString, equalTo("New.a1")) + } + + @Test + fun `template is not reloaded if not changed`() { + assertThat(renderedString, equalTo("Text.a1")) + assertThat(renderedString, equalTo("Text.a1")) + } + + @Test + fun `initial context is copied to reloaded templates`() { + filesystemTemplate.initialContext.set("values", "test") + sleep(1000) + writeTemplate("New") + assertThat(filesystemTemplate.initialContext.get("values"), equalTo("test" as Any)) + } + + @Test + fun `parts are copied to currently loaded templates`() { + writeTemplate("New") + renderedString + filesystemTemplate.add { _, writer -> + writer.write(".Test") + } + assertThat(renderedString, equalTo("New.a1.Test")) + } + + @Test + fun `parts are copied to reloaded templates`() { + filesystemTemplate.add { _, writer -> + writer.write(".Test") + } + sleep(1000) + writeTemplate("New") + assertThat(renderedString, equalTo("New.a1.Test")) + } + + @Test + fun `column of returned template is returned as zero`() { + assertThat(filesystemTemplate.column, equalTo(0)) + } + + @Test + fun `line of returned template is returned as zero`() { + assertThat(filesystemTemplate.line, equalTo(0)) + } + + @Test + fun `template can be iterated over`() { + assertThat>(filesystemTemplate.iterator(), notNullValue()) + } + +} diff --git a/src/test/kotlin/net/pterodactylus/sone/template/HistogramRendererTest.kt b/src/test/kotlin/net/pterodactylus/sone/template/HistogramRendererTest.kt new file mode 100644 index 0000000..bc87086 --- /dev/null +++ b/src/test/kotlin/net/pterodactylus/sone/template/HistogramRendererTest.kt @@ -0,0 +1,204 @@ +/** + * Sone - HistogramRendererTest.kt - Copyright © 2019–2020 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 . + */ + +package net.pterodactylus.sone.template + +import com.codahale.metrics.* +import net.pterodactylus.sone.freenet.* +import net.pterodactylus.util.template.* +import org.hamcrest.MatcherAssert.* +import org.hamcrest.Matchers.* +import org.jsoup.* +import org.jsoup.nodes.* +import org.junit.* +import java.util.* + +/** + * Unit test for [HistogramRenderer]. + */ +class HistogramRendererTest { + + private val translation = object : Translation { + override val currentLocale = Locale.ENGLISH + override fun translate(key: String) = "Metric Name".takeIf { key == "Page.Metrics.TestHistogram.Title" } ?: "" + } + private val metricRenderer = HistogramRenderer() + private val templateContext = TemplateContext().apply { + addFilter("html", HtmlFilter()) + addFilter("duration", DurationFormatFilter()) + addFilter("l10n", L10nFilter(translation)) + } + + @Test + fun `histogram is rendered as table row`() { + createAndVerifyTableRow { + assertThat(it.nodeName(), equalTo("tr")) + } + } + + @Test + fun `histogram has eleven columns`() { + createAndVerifyTableRow { + assertThat(it.getElementsByTag("td"), hasSize(11)) + } + } + + @Test + fun `first column contains translated metric name`() { + createAndVerifyTableRow(mapOf("name" to "test.histogram")) { + assertThat(it.getElementsByTag("td")[0].text(), equalTo("Metric Name")) + } + } + + @Test + fun `second column is numeric`() { + verifyColumnIsNumeric(1) + } + + @Test + fun `second column contains count`() { + createAndVerifyTableRow { + assertThat(it.getElementsByTag("td")[1].text(), equalTo("2001")) + } + } + + @Test + fun `third column is numeric`() { + verifyColumnIsNumeric(2) + } + + @Test + fun `third column contains min value`() { + createAndVerifyTableRow { + assertThat(it.getElementsByTag("td")[2].text(), equalTo("2.0ms")) + } + } + + @Test + fun `fourth column is numeric`() { + verifyColumnIsNumeric(3) + } + + @Test + fun `fourth column contains max value`() { + createAndVerifyTableRow { + assertThat(it.getElementsByTag("td")[3].text(), equalTo("998.0ms")) + } + } + + @Test + fun `fifth column is numeric`() { + verifyColumnIsNumeric(4) + } + + @Test + fun `fifth column contains mean value`() { + createAndVerifyTableRow { + assertThat(it.getElementsByTag("td")[4].text(), equalTo("492.7ms")) + } + } + + @Test + fun `sixth column is numeric`() { + verifyColumnIsNumeric(5) + } + + @Test + fun `sixth column contains median value`() { + createAndVerifyTableRow { + assertThat(it.getElementsByTag("td")[5].text(), equalTo("483.6ms")) + } + } + + @Test + fun `seventh column is numeric`() { + verifyColumnIsNumeric(6) + } + + @Test + fun `seventh column contains 75th percentile`() { + createAndVerifyTableRow { + assertThat(it.getElementsByTag("td")[6].text(), equalTo("740.9ms")) + } + } + + @Test + fun `eighth column is numeric`() { + verifyColumnIsNumeric(7) + } + + @Test + fun `eighth column contains 95th percentile`() { + createAndVerifyTableRow { + assertThat(it.getElementsByTag("td")[7].text(), equalTo("940.9ms")) + } + } + + @Test + fun `ninth column is numeric`() { + verifyColumnIsNumeric(8) + } + + @Test + fun `ninth column contains 98th percentile`() { + createAndVerifyTableRow { + assertThat(it.getElementsByTag("td")[8].text(), equalTo("975.6ms")) + } + } + + @Test + fun `tenth column is numeric`() { + verifyColumnIsNumeric(9) + } + + @Test + fun `tenth column contains 99th percentile`() { + createAndVerifyTableRow { + assertThat(it.getElementsByTag("td")[9].text(), equalTo("991.6ms")) + } + } + + @Test + fun `eleventh column is numeric`() { + verifyColumnIsNumeric(10) + } + + @Test + fun `eleventh column contains 99,9th percentile`() { + createAndVerifyTableRow { + assertThat(it.getElementsByTag("td")[10].text(), equalTo("998.0ms")) + } + } + + private fun createAndVerifyTableRow(parameters: Map? = null, verify: (Element) -> Unit) = + metricRenderer.format(templateContext, histogram, parameters) + .let { "$it
" } + .let(Jsoup::parseBodyFragment) + .getElementById("t").child(0).child(0) + .let(verify) + + private fun verifyColumnIsNumeric(column: Int) = + createAndVerifyTableRow { + assertThat(it.getElementsByTag("td")[column].classNames(), hasItem("numeric")) + } + +} + +private val random = Random(1) +private val histogram = MetricRegistry().histogram("test.histogram") { Histogram(SlidingWindowReservoir(1028)) }.apply { + (0..2000).map { random.nextInt(1_000_000) }.forEach(this::update) +} diff --git a/src/test/kotlin/net/pterodactylus/sone/template/LinkedElementsFilterTest.kt b/src/test/kotlin/net/pterodactylus/sone/template/LinkedElementsFilterTest.kt index 1e83b85..32e8587 100644 --- a/src/test/kotlin/net/pterodactylus/sone/template/LinkedElementsFilterTest.kt +++ b/src/test/kotlin/net/pterodactylus/sone/template/LinkedElementsFilterTest.kt @@ -10,7 +10,7 @@ import net.pterodactylus.sone.data.SoneOptions.LoadExternalContent.MANUALLY_TRUS import net.pterodactylus.sone.data.SoneOptions.LoadExternalContent.TRUSTED import net.pterodactylus.sone.freenet.wot.OwnIdentity import net.pterodactylus.sone.freenet.wot.Trust -import net.pterodactylus.sone.test.mock +import net.pterodactylus.sone.test.* import net.pterodactylus.sone.text.FreenetLinkPart import net.pterodactylus.sone.text.LinkPart import net.pterodactylus.sone.text.Part @@ -21,7 +21,6 @@ import org.hamcrest.Matchers.contains import org.hamcrest.Matchers.emptyIterable import org.junit.Before import org.junit.Test -import org.mockito.Mockito.`when` /** * Unit test for [LinkedElementsFilter]. @@ -44,14 +43,14 @@ class LinkedElementsFilterTest { @Before fun setupSone() { - `when`(sone.options).thenReturn(DefaultSoneOptions()) + whenever(sone.options).thenReturn(DefaultSoneOptions()) } @Before fun setupImageLoader() { - `when`(imageLoader.loadElement("KSK@link")).thenReturn(LinkedElement("KSK@link", failed = true)) - `when`(imageLoader.loadElement("KSK@loading.png")).thenReturn(LinkedElement("KSK@loading.png", loading = true)) - `when`(imageLoader.loadElement("KSK@link.png")).thenReturn(LinkedElement("KSK@link.png")) + whenever(imageLoader.loadElement("KSK@link")).thenReturn(LinkedElement("KSK@link", failed = true)) + whenever(imageLoader.loadElement("KSK@loading.png")).thenReturn(LinkedElement("KSK@loading.png", loading = true)) + whenever(imageLoader.loadElement("KSK@link.png")).thenReturn(LinkedElement("KSK@link.png")) } @Test @@ -89,7 +88,7 @@ class LinkedElementsFilterTest { fun `filter finds images if the remote sone is local`() { sone.options.loadLinkedImages = MANUALLY_TRUSTED templateContext.set("currentSone", sone) - `when`(remoteSone.isLocal).thenReturn(true) + whenever(remoteSone.isLocal).thenReturn(true) parameters["sone"] = remoteSone verifyThatImagesArePresent() } @@ -106,7 +105,7 @@ class LinkedElementsFilterTest { fun `filter does not find images if local sone requires manual trust and remote sone has only implicit trust`() { sone.options.loadLinkedImages = MANUALLY_TRUSTED templateContext.set("currentSone", sone) - `when`(remoteSone.identity.getTrust(this.sone.identity as OwnIdentity)).thenReturn(Trust(null, 100, null)) + whenever(remoteSone.identity.getTrust(this.sone.identity as OwnIdentity)).thenReturn(Trust(null, 100, null)) parameters["sone"] = remoteSone verifyThatImagesAreNotPresent() } @@ -115,7 +114,7 @@ class LinkedElementsFilterTest { fun `filter does not find images if local sone requires manual trust and remote sone has explicit trust of zero`() { sone.options.loadLinkedImages = MANUALLY_TRUSTED templateContext.set("currentSone", sone) - `when`(remoteSone.identity.getTrust(this.sone.identity as OwnIdentity)).thenReturn(Trust(0, null, null)) + whenever(remoteSone.identity.getTrust(this.sone.identity as OwnIdentity)).thenReturn(Trust(0, null, null)) parameters["sone"] = remoteSone verifyThatImagesAreNotPresent() } @@ -124,7 +123,7 @@ class LinkedElementsFilterTest { fun `filter finds images if local sone requires manual trust and remote sone has explicit trust of one`() { sone.options.loadLinkedImages = MANUALLY_TRUSTED templateContext.set("currentSone", sone) - `when`(remoteSone.identity.getTrust(this.sone.identity as OwnIdentity)).thenReturn(Trust(1, null, null)) + whenever(remoteSone.identity.getTrust(this.sone.identity as OwnIdentity)).thenReturn(Trust(1, null, null)) parameters["sone"] = remoteSone verifyThatImagesArePresent() } @@ -140,7 +139,7 @@ class LinkedElementsFilterTest { @Test fun `filter finds images if local sone requires following and remote sone is followed`() { sone.options.loadLinkedImages = FOLLOWED - `when`(sone.hasFriend("remote-id")).thenReturn(true) + whenever(sone.hasFriend("remote-id")).thenReturn(true) templateContext["currentSone"] = sone parameters["sone"] = remoteSone verifyThatImagesArePresent() @@ -158,7 +157,7 @@ class LinkedElementsFilterTest { fun `filter finds images if following is required and remote sone is a local sone`() { sone.options.loadLinkedImages = FOLLOWED templateContext["currentSone"] = sone - `when`(remoteSone.isLocal).thenReturn(true) + whenever(remoteSone.isLocal).thenReturn(true) parameters["sone"] = remoteSone verifyThatImagesArePresent() } @@ -175,7 +174,7 @@ class LinkedElementsFilterTest { fun `filter does not find images if any trust is required and remote sone has implicit trust of zero`() { sone.options.loadLinkedImages = TRUSTED templateContext["currentSone"] = sone - `when`(remoteSone.identity.getTrust(sone.identity as OwnIdentity)).thenReturn(Trust(null, 0, null)) + whenever(remoteSone.identity.getTrust(sone.identity as OwnIdentity)).thenReturn(Trust(null, 0, null)) parameters["sone"] = remoteSone verifyThatImagesAreNotPresent() } @@ -184,7 +183,7 @@ class LinkedElementsFilterTest { fun `filter finds images if any trust is required and remote sone has implicit trust of one`() { sone.options.loadLinkedImages = TRUSTED templateContext["currentSone"] = sone - `when`(remoteSone.identity.getTrust(sone.identity as OwnIdentity)).thenReturn(Trust(null, 1, null)) + whenever(remoteSone.identity.getTrust(sone.identity as OwnIdentity)).thenReturn(Trust(null, 1, null)) parameters["sone"] = remoteSone verifyThatImagesArePresent() } @@ -193,7 +192,7 @@ class LinkedElementsFilterTest { fun `filter does not find images if any trust is required and remote sone has explicit trust of zero but implicit trust of one`() { sone.options.loadLinkedImages = TRUSTED templateContext["currentSone"] = sone - `when`(remoteSone.identity.getTrust(sone.identity as OwnIdentity)).thenReturn(Trust(0, 1, null)) + whenever(remoteSone.identity.getTrust(sone.identity as OwnIdentity)).thenReturn(Trust(0, 1, null)) parameters["sone"] = remoteSone verifyThatImagesAreNotPresent() } @@ -202,7 +201,7 @@ class LinkedElementsFilterTest { fun `filter finds images if any trust is required and remote sone has explicit trust of one but no implicit trust`() { sone.options.loadLinkedImages = TRUSTED templateContext["currentSone"] = sone - `when`(remoteSone.identity.getTrust(sone.identity as OwnIdentity)).thenReturn(Trust(1, null, null)) + whenever(remoteSone.identity.getTrust(sone.identity as OwnIdentity)).thenReturn(Trust(1, null, null)) parameters["sone"] = remoteSone verifyThatImagesArePresent() } @@ -211,7 +210,7 @@ class LinkedElementsFilterTest { fun `filter finds images if any trust is required and remote sone is a local sone`() { sone.options.loadLinkedImages = TRUSTED templateContext["currentSone"] = sone - `when`(remoteSone.isLocal).thenReturn(true) + whenever(remoteSone.isLocal).thenReturn(true) parameters["sone"] = remoteSone verifyThatImagesArePresent() } @@ -238,9 +237,9 @@ class LinkedElementsFilterTest { private fun createSone(id: String = "sone-id"): Sone { val sone = mock() - `when`(sone.id).thenReturn(id) - `when`(sone.options).thenReturn(DefaultSoneOptions()) - `when`(sone.identity).thenReturn(mock()) + whenever(sone.id).thenReturn(id) + whenever(sone.options).thenReturn(DefaultSoneOptions()) + whenever(sone.identity).thenReturn(mock()) return sone } diff --git a/src/test/kotlin/net/pterodactylus/sone/template/ParserFilterTest.kt b/src/test/kotlin/net/pterodactylus/sone/template/ParserFilterTest.kt index 0e44568..5864a6a 100644 --- a/src/test/kotlin/net/pterodactylus/sone/template/ParserFilterTest.kt +++ b/src/test/kotlin/net/pterodactylus/sone/template/ParserFilterTest.kt @@ -3,20 +3,17 @@ package net.pterodactylus.sone.template import com.google.inject.Guice import net.pterodactylus.sone.core.Core import net.pterodactylus.sone.data.Sone -import net.pterodactylus.sone.test.getInstance -import net.pterodactylus.sone.test.isProvidedByMock -import net.pterodactylus.sone.test.mock +import net.pterodactylus.sone.test.* import net.pterodactylus.sone.text.SoneTextParser import net.pterodactylus.sone.text.SoneTextParserContext import net.pterodactylus.util.template.TemplateContext import org.hamcrest.MatcherAssert.assertThat -import org.hamcrest.Matchers.`is` +import org.hamcrest.Matchers.equalTo import org.hamcrest.Matchers.emptyIterable import org.hamcrest.Matchers.notNullValue import org.hamcrest.Matchers.sameInstance import org.junit.Test import org.mockito.ArgumentCaptor.forClass -import org.mockito.Mockito.`when` import org.mockito.Mockito.eq import org.mockito.Mockito.verify @@ -38,8 +35,8 @@ class ParserFilterTest { private fun setupSone(identity: String): Sone { val sone = mock() - `when`(sone.id).thenReturn(identity) - `when`(core.getSone(identity)).thenReturn(sone) + whenever(sone.id).thenReturn(identity) + whenever(core.getSone(identity)).thenReturn(sone) return sone } @@ -63,7 +60,7 @@ class ParserFilterTest { filter.format(templateContext, "text", parameters) val context = forClass(SoneTextParserContext::class.java) verify(soneTextParser).parse(eq("text") ?: "", context.capture()) - assertThat(context.value.postingSone, `is`(sone)) + assertThat(context.value.postingSone, equalTo(sone)) } @Test diff --git a/src/test/kotlin/net/pterodactylus/sone/template/PostAccessorTest.kt b/src/test/kotlin/net/pterodactylus/sone/template/PostAccessorTest.kt new file mode 100644 index 0000000..bb92810 --- /dev/null +++ b/src/test/kotlin/net/pterodactylus/sone/template/PostAccessorTest.kt @@ -0,0 +1,197 @@ +package net.pterodactylus.sone.template + +import net.pterodactylus.sone.core.* +import net.pterodactylus.sone.data.* +import net.pterodactylus.sone.test.* +import net.pterodactylus.sone.utils.* +import net.pterodactylus.util.template.* +import org.hamcrest.MatcherAssert.* +import org.hamcrest.Matchers.* +import org.junit.* + +/** + * Unit test for [PostAccessor]. + */ +class PostAccessorTest { + + private val core = mock() + private val accessor = PostAccessor(core) + private val post = mock() + private val now = System.currentTimeMillis() + + @Before + fun setupPost() { + whenever(post.id).thenReturn("post-id") + } + + @Test + fun `accessor returns the correct replies`() { + val replies = listOf( + createPostReply(2000), + createPostReply(-1000), + createPostReply(-2000), + createPostReply(-3000), + createPostReply(-4000) + ) + whenever(core.getReplies("post-id")).thenReturn(replies) + val repliesForPost = accessor[null, post, "replies"] as Collection + assertThat(repliesForPost, contains( + replies[1], + replies[2], + replies[3], + replies[4] + )) + } + + private fun createPostReply(timeOffset: Long) = mock().apply { + whenever(time).thenReturn(now + timeOffset) + } + + @Test + fun `accessor returns the liking sones`() { + val sones = setOf() + whenever(core.getLikes(post)).thenReturn(sones) + val likingSones = accessor[null, post, "likes"] as Set + assertThat(likingSones, equalTo(sones)) + } + + @Test + fun `accessor returns whether the current sone liked a post`() { + val sone = mock() + whenever(sone.isLikedPostId("post-id")).thenReturn(true) + val templateContext = TemplateContext() + templateContext["currentSone"] = sone + assertThat(accessor[templateContext, post, "liked"], equalTo(true)) + } + + @Test + fun `accessor returns false if post is not liked`() { + val sone = mock() + val templateContext = TemplateContext() + templateContext["currentSone"] = sone + assertThat(accessor[templateContext, post, "liked"], equalTo(false)) + } + + @Test + fun `accessor returns false if there is no current sone`() { + val templateContext = TemplateContext() + assertThat(accessor[templateContext, post, "liked"], equalTo(false)) + } + + @Test + fun `accessor returns that not known post is new`() { + assertThat(accessor[null, post, "new"], equalTo(true)) + } + + @Test + fun `accessor returns that known post is not new`() { + whenever(post.isKnown).thenReturn(true) + assertThat(accessor[null, post, "new"], equalTo(false)) + } + + @Test + fun `accessor returns if post is bookmarked`() { + whenever(core.isBookmarked(post)).thenReturn(true) + assertThat(accessor[null, post, "bookmarked"], equalTo(true)) + } + + @Test + fun `reply sone for remote post without replies is current sone`() { + val post = mockPostFrom(remoteSone) + assertThat(accessor[templateContext, post, "replySone"], equalTo(currentSone)) + } + + @Test + fun `reply sone for remote post with remote replies is current sone`() { + val post = mockPostFrom(remoteSone) + val replies = listOf(mockReplyFrom(remoteSone), mockReplyFrom(remoteSone)) + whenever(core.getReplies("post-id")).thenReturn(replies) + assertThat(accessor[templateContext, post, "replySone"], equalTo(currentSone)) + } + + @Test + fun `reply sone for remote post with remote and one local replies is sone of local reply`() { + val post = mockPostFrom(remoteSone) + val localSone = mockLocalSone() + val replies = listOf(mockReplyFrom(remoteSone), mockReplyFrom(localSone)) + whenever(core.getReplies("post-id")).thenReturn(replies) + assertThat(accessor[templateContext, post, "replySone"], equalTo(localSone)) + } + + @Test + fun `reply sone for remote post with remote and several local replies is sone of latest local reply`() { + val post = mockPostFrom(remoteSone) + val localSone1 = mockLocalSone() + val localSone2 = mockLocalSone() + val replies = listOf(mockReplyFrom(remoteSone), mockReplyFrom(localSone1), mockReplyFrom(localSone2)) + whenever(core.getReplies("post-id")).thenReturn(replies) + assertThat(accessor[templateContext, post, "replySone"], equalTo(localSone2)) + } + + @Test + fun `reply sone for local post without replies is post sone`() { + val localSone = mockLocalSone() + val post = mockPostFrom(localSone) + assertThat(accessor[templateContext, post, "replySone"], equalTo(localSone)) + } + + @Test + fun `reply sone for local post with remote replies is local sone`() { + val localSone = mockLocalSone() + val post = mockPostFrom(localSone) + val replies = listOf(mockReplyFrom(remoteSone), mockReplyFrom(remoteSone)) + whenever(core.getReplies("post-id")).thenReturn(replies) + assertThat(accessor[templateContext, post, "replySone"], equalTo(localSone)) + } + + @Test + fun `reply sone for local post with remote and one local replies is local reply sone`() { + val localSone1 = mockLocalSone() + val post = mockPostFrom(localSone1) + val localSone2 = mockLocalSone() + val replies = listOf(mockReplyFrom(remoteSone), mockReplyFrom(localSone2)) + whenever(core.getReplies("post-id")).thenReturn(replies) + assertThat(accessor[templateContext, post, "replySone"], equalTo(localSone2)) + } + + @Test + fun `reply sone for local post with remote and several local replies is latest local reply sone`() { + val localSone1 = mockLocalSone() + val post = mockPostFrom(localSone1) + val localSone2 = mockLocalSone() + val localSone3 = mockLocalSone() + val replies = listOf(mockReplyFrom(remoteSone), mockReplyFrom(localSone2), mockReplyFrom(localSone3)) + whenever(core.getReplies("post-id")).thenReturn(replies) + assertThat(accessor[templateContext, post, "replySone"], equalTo(localSone3)) + } + + @Test + fun `reply sone for post directed at local sone is local sone`() { + val localSone = mockLocalSone() + val post = mockPostFrom(remoteSone, localSone) + assertThat(accessor[templateContext, post, "replySone"], equalTo(localSone)) + } + + + @Test + fun `accessor returns other properties`() { + assertThat(accessor[null, post, "hashCode"], equalTo(post.hashCode())) + } + +} + +private val currentSone = mock() +private val remoteSone = mock() +private fun mockLocalSone() = mock().apply { whenever(isLocal).thenReturn(true) } + +private val templateContext = TemplateContext().apply { + this["currentSone"] = currentSone +} + +private fun mockPostFrom(sone: Sone, recipient: Sone? = null) = mock().apply { + whenever(id).thenReturn("post-id") + whenever(this.sone).thenReturn(sone) + whenever(this.recipient).thenReturn(recipient.asOptional()) +} + +private fun mockReplyFrom(sone: Sone) = mock().apply { whenever(this.sone).thenReturn(sone) } diff --git a/src/test/kotlin/net/pterodactylus/sone/template/ProfileAccessorTest.kt b/src/test/kotlin/net/pterodactylus/sone/template/ProfileAccessorTest.kt index a2c4ad7..4513ce0 100644 --- a/src/test/kotlin/net/pterodactylus/sone/template/ProfileAccessorTest.kt +++ b/src/test/kotlin/net/pterodactylus/sone/template/ProfileAccessorTest.kt @@ -17,7 +17,7 @@ import net.pterodactylus.sone.test.mock import net.pterodactylus.sone.test.whenever import net.pterodactylus.util.template.TemplateContext import org.hamcrest.MatcherAssert.assertThat -import org.hamcrest.Matchers.`is` +import org.hamcrest.Matchers.equalTo import org.hamcrest.Matchers.nullValue import org.junit.Before import org.junit.Test @@ -86,7 +86,7 @@ class ProfileAccessorTest { @Test fun `avatar ID is returned if profile belongs to local sone`() { whenever(remoteSone.isLocal).thenReturn(true) - assertThat(accessor.get(templateContext, profile, "avatar"), `is`("avatar-id")) + assertThat(accessor.get(templateContext, profile, "avatar"), equalTo("avatar-id")) } @Test @@ -98,14 +98,14 @@ class ProfileAccessorTest { @Test fun `avatar ID is returned if sone is configure to always show avatars`() { currentSone.options.showCustomAvatars = ALWAYS - assertThat(accessor.get(templateContext, profile, "avatar"), `is`("avatar-id")) + assertThat(accessor.get(templateContext, profile, "avatar"), equalTo("avatar-id")) } @Test fun `avatar ID is returned if sone is configure to show avatars of followed sones and remote sone is followed`() { currentSone.options.showCustomAvatars = FOLLOWED whenever(currentSone.hasFriend("remote-sone")).thenReturn(true) - assertThat(accessor.get(templateContext, profile, "avatar"), `is`("avatar-id")) + assertThat(accessor.get(templateContext, profile, "avatar"), equalTo("avatar-id")) } @Test @@ -142,7 +142,7 @@ class ProfileAccessorTest { fun `avatar ID is returned if sone is configure to show avatars based on manual trust and explicit trust is one`() { currentSone.options.showCustomAvatars = MANUALLY_TRUSTED setTrust(Trust(1, null, null)) - assertThat(accessor.get(templateContext, profile, "avatar"), `is`("avatar-id")) + assertThat(accessor.get(templateContext, profile, "avatar"), equalTo("avatar-id")) } @Test @@ -156,7 +156,7 @@ class ProfileAccessorTest { fun `avatar ID is returned if sone is configure to show avatars based on trust and explicit trust is one`() { currentSone.options.showCustomAvatars = TRUSTED setTrust(Trust(1, null, null)) - assertThat(accessor.get(templateContext, profile, "avatar"), `is`("avatar-id")) + assertThat(accessor.get(templateContext, profile, "avatar"), equalTo("avatar-id")) } @Test @@ -177,12 +177,12 @@ class ProfileAccessorTest { fun `avatar ID is returned if sone is configure to show avatars based on trust and implicit trust is one`() { currentSone.options.showCustomAvatars = TRUSTED setTrust(Trust(0, 1, null)) - assertThat(accessor.get(templateContext, profile, "avatar"), `is`("avatar-id")) + assertThat(accessor.get(templateContext, profile, "avatar"), equalTo("avatar-id")) } @Test fun `accessing other members uses reflection accessor`() { - assertThat(accessor.get(templateContext, profile, "hashCode"), `is`(profile.hashCode())) + assertThat(accessor.get(templateContext, profile, "hashCode"), equalTo(profile.hashCode())) } } diff --git a/src/test/kotlin/net/pterodactylus/sone/template/ShortenFilterTest.kt b/src/test/kotlin/net/pterodactylus/sone/template/ShortenFilterTest.kt index c6990a7..efed7a3 100644 --- a/src/test/kotlin/net/pterodactylus/sone/template/ShortenFilterTest.kt +++ b/src/test/kotlin/net/pterodactylus/sone/template/ShortenFilterTest.kt @@ -2,7 +2,7 @@ package net.pterodactylus.sone.template import net.pterodactylus.sone.data.Profile import net.pterodactylus.sone.data.Sone -import net.pterodactylus.sone.test.mock +import net.pterodactylus.sone.test.* import net.pterodactylus.sone.text.FreenetLinkPart import net.pterodactylus.sone.text.Part import net.pterodactylus.sone.text.PlainTextPart @@ -10,7 +10,6 @@ import net.pterodactylus.sone.text.SonePart import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.Matchers.contains import org.junit.Test -import org.mockito.Mockito.`when` /** * Unit test for [ShortenFilter]. @@ -76,7 +75,7 @@ class ShortenFilterTest { @Test fun `sone parts are added but their length is ignored`() { val sone = mock() - `when`(sone.profile).thenReturn(Profile(sone)) + whenever(sone.profile).thenReturn(Profile(sone)) assertThat(shortenParts(15, 10, SonePart(sone), PlainTextPart("This is a long text.")), contains( SonePart(sone), PlainTextPart("This is a …") @@ -86,7 +85,7 @@ class ShortenFilterTest { @Test fun `additional sone parts are ignored`() { val sone = mock() - `when`(sone.profile).thenReturn(Profile(sone)) + whenever(sone.profile).thenReturn(Profile(sone)) assertThat(shortenParts(15, 10, PlainTextPart("This is a long text."), SonePart(sone)), contains( PlainTextPart("This is a …") )) diff --git a/src/test/kotlin/net/pterodactylus/sone/template/UnknownDateFilterTest.kt b/src/test/kotlin/net/pterodactylus/sone/template/UnknownDateFilterTest.kt index f18033e..b1c2cc5 100644 --- a/src/test/kotlin/net/pterodactylus/sone/template/UnknownDateFilterTest.kt +++ b/src/test/kotlin/net/pterodactylus/sone/template/UnknownDateFilterTest.kt @@ -1,30 +1,32 @@ package net.pterodactylus.sone.template -import freenet.l10n.BaseL10n -import net.pterodactylus.sone.test.mock -import net.pterodactylus.sone.test.whenever -import org.hamcrest.MatcherAssert.assertThat -import org.hamcrest.Matchers.equalTo -import org.junit.Test +import net.pterodactylus.sone.freenet.* +import net.pterodactylus.sone.test.* +import org.hamcrest.MatcherAssert.* +import org.hamcrest.Matchers.* +import org.junit.* +import java.util.* /** * Unit test for [UnknownDateFilter]. */ class UnknownDateFilterTest { - private val baseL10n = mock() + private val translation = object : Translation { + override val currentLocale = Locale.ENGLISH + override fun translate(key: String) = if (key == unknownKey) "translated" else "" + } private val unknownKey = "unknown.key" - private val filter = UnknownDateFilter(baseL10n, unknownKey) + private val filter = UnknownDateFilter(translation, unknownKey) @Test fun `filter returns given object for non-longs`() { - val someObject = Any() + val someObject = Any() assertThat(filter.format(null, someObject, null), equalTo(someObject)) } @Test fun `filter returns translated value of unknown key if zero is given`() { - whenever(baseL10n.getString(unknownKey)).thenReturn("translated") assertThat(filter.format(null, 0L, null), equalTo("translated")) } diff --git a/src/test/kotlin/net/pterodactylus/sone/test/Guice.kt b/src/test/kotlin/net/pterodactylus/sone/test/Guice.kt index 2fc4a96..b0bed5c 100644 --- a/src/test/kotlin/net/pterodactylus/sone/test/Guice.kt +++ b/src/test/kotlin/net/pterodactylus/sone/test/Guice.kt @@ -1,11 +1,12 @@ package net.pterodactylus.sone.test -import com.google.inject.Injector -import com.google.inject.Module +import com.google.inject.* import com.google.inject.name.* +import org.hamcrest.MatcherAssert.* +import org.hamcrest.Matchers.* import org.mockito.* import javax.inject.Provider -import kotlin.reflect.KClass +import kotlin.reflect.* fun KClass.isProvidedBy(instance: T) = Module { it.bind(this.java).toProvider(Provider { instance }) } fun KClass.withNameIsProvidedBy(instance: T, name: String) = Module { it.bind(this.java).annotatedWith(Names.named(name)).toProvider(Provider { instance }) } @@ -14,7 +15,16 @@ fun KClass.isProvidedBy(provider: KClass>) = Module inline fun KClass.isProvidedByMock() = Module { it.bind(this.java).toProvider(Provider { mock() }) } inline fun KClass.isProvidedByDeepMock() = Module { it.bind(this.java).toProvider(Provider { deepMock() }) } -inline fun Injector.getInstance() = getInstance(T::class.java)!! +inline fun Injector.getInstance(annotation: Annotation? = null): T = annotation + ?.let { getInstance(Key.get(object : TypeLiteral() {}, it)) } + ?: getInstance(Key.get(object : TypeLiteral() {})) + + +inline fun Injector.verifySingletonInstance(annotation: Annotation? = null) { + val firstInstance = getInstance(annotation) + val secondInstance = getInstance(annotation) + assertThat(firstInstance, sameInstance(secondInstance)) +} fun supply(javaClass: Class): Source = object : Source { override fun fromInstance(instance: T) = Module { it.bind(javaClass).toInstance(instance) } diff --git a/src/test/kotlin/net/pterodactylus/sone/test/Matchers.kt b/src/test/kotlin/net/pterodactylus/sone/test/Matchers.kt index c084d35..85ed9e7 100644 --- a/src/test/kotlin/net/pterodactylus/sone/test/Matchers.kt +++ b/src/test/kotlin/net/pterodactylus/sone/test/Matchers.kt @@ -1,7 +1,30 @@ package net.pterodactylus.sone.test +import freenet.support.* +import net.pterodactylus.sone.freenet.wot.* +import net.pterodactylus.sone.utils.* import net.pterodactylus.util.web.* import org.hamcrest.* +import org.hamcrest.Matchers +import org.hamcrest.Matchers.* + +/** + * Returns a [hamcrest matcher][Matcher] constructed from the given predicate. + */ +fun matches(description: String? = null, predicate: (T) -> Boolean) = object : TypeSafeDiagnosingMatcher() { + + override fun matchesSafely(item: T, mismatchDescription: Description) = + predicate(item).onFalse { + mismatchDescription.appendValue(item).appendText(" did not match predicate") + } + + override fun describeTo(description: Description) { + description.appendText("matches predicate ").appendValue(predicate) + } + +}.let { matcher -> + description?.let { describedAs(description, matcher) } ?: matcher +} fun hasHeader(name: String, value: String) = object : TypeSafeDiagnosingMatcher
() { override fun matchesSafely(item: Header, mismatchDescription: Description) = @@ -19,3 +42,123 @@ fun compare(value: T, comparison: (T) -> Boolean, onError: (T) -> Unit false.takeUnless { comparison(value) } ?.also { onError(value) } +fun isEmptyMap() = object : TypeSafeDiagnosingMatcher>() { + override fun describeTo(description: Description) { + description.appendText("empty map") + } + + override fun matchesSafely(item: Map, mismatchDescription: Description) = + item.isEmpty().onFalse { + mismatchDescription.appendText("was ").appendValue(item) + } +} + +fun isTrust(trust: Int?, score: Int?, rank: Int?) = + AttributeMatcher("trust") + .addAttribute("trust", trust, Trust::explicit) + .addAttribute("score", score, Trust::implicit) + .addAttribute("rank", rank, Trust::distance) + +fun isTrusted(ownIdentity: OwnIdentity, trust: Matcher) = object : TypeSafeDiagnosingMatcher() { + override fun matchesSafely(item: Identity, mismatchDescription: Description) = + item.getTrust(ownIdentity)?.let { foundTrust -> + trust.matches(foundTrust).onFalse { + trust.describeMismatch(foundTrust, mismatchDescription) + } + } ?: { + mismatchDescription.appendText("not trusted") + false + }() + + override fun describeTo(description: Description) { + description + .appendText("trusted by ").appendValue(ownIdentity) + .appendText(" with ").appendValue(trust) + } +} + +fun isIdentity(id: String, nickname: String?, requestUri: String, contexts: Matcher>, properties: Matcher>) = + AttributeMatcher("identity") + .addAttribute("id", id, Identity::getId) + .addAttribute("nickname", nickname, Identity::getNickname) + .addAttribute("requestUri", requestUri, Identity::getRequestUri) + .addAttribute("contexts", Identity::getContexts, contexts) + .addAttribute("properties", Identity::getProperties, properties) + +fun isOwnIdentity(id: String, nickname: String, requestUri: String, insertUri: String, contexts: Matcher>, properties: Matcher>) = + AttributeMatcher("own identity") + .addAttribute("id", id, OwnIdentity::getId) + .addAttribute("nickname", nickname, OwnIdentity::getNickname) + .addAttribute("request uri", requestUri, OwnIdentity::getRequestUri) + .addAttribute("insert uri", insertUri, OwnIdentity::getInsertUri) + .addAttribute("contexts", OwnIdentity::getContexts, contexts) + .addAttribute("properties", OwnIdentity::getProperties, properties) + +fun hasField(name: String, valueMatcher: Matcher) = object : TypeSafeDiagnosingMatcher() { + override fun matchesSafely(item: SimpleFieldSet, mismatchDescription: Description) = + valueMatcher.matches(item.get(name)).onFalse { + valueMatcher.describeMismatch(item, mismatchDescription) + } + + override fun describeTo(description: Description) { + description + .appendText("simple field set with key ").appendValue(name) + .appendText(", value ").appendValue(valueMatcher) + } +} + +/** + * [TypeSafeDiagnosingMatcher] implementation that aims to cut down boilerplate on verifying the attributes + * of typical container objects. + */ +class AttributeMatcher(private val objectName: String) : TypeSafeDiagnosingMatcher() { + + private data class AttributeToMatch( + val name: String, + val getter: (T) -> V, + val matcher: Matcher + ) + + private val attributesToMatch = mutableListOf>() + + /** + * Adds an attribute to check for equality, returning `this`. + */ + fun addAttribute(name: String, expected: V, getter: (T) -> V): AttributeMatcher = apply { + attributesToMatch.add(AttributeToMatch(name, getter, describedAs("$name %0", equalTo(expected), expected))) + } + + /** + * Adds an attribute to check with the given [hamcrest matcher][Matcher]. + */ + fun addAttribute(name: String, getter: (T) -> V, matcher: Matcher) = apply { + attributesToMatch.add(AttributeToMatch(name, getter, matcher)) + } + + override fun describeTo(description: Description) { + attributesToMatch.forEachIndexed { index, attributeToMatch -> + if (index == 0) { + description.appendText("$objectName with ") + } else { + description.appendText(", ") + } + attributeToMatch.matcher.describeTo(description) + } + } + + override fun matchesSafely(item: T, mismatchDescription: Description): Boolean = + attributesToMatch.fold(true) { matches, attributeToMatch -> + if (!matches) { + false + } else { + if (!attributeToMatch.matcher.matches(attributeToMatch.getter(item))) { + mismatchDescription.appendText("but ${attributeToMatch.name} ") + attributeToMatch.matcher.describeMismatch(attributeToMatch.getter(item), mismatchDescription) + false + } else { + true + } + } + } + +} diff --git a/src/test/kotlin/net/pterodactylus/sone/test/Mockotlin.kt b/src/test/kotlin/net/pterodactylus/sone/test/Mockotlin.kt index 85b86d5..4d345b3 100644 --- a/src/test/kotlin/net/pterodactylus/sone/test/Mockotlin.kt +++ b/src/test/kotlin/net/pterodactylus/sone/test/Mockotlin.kt @@ -3,7 +3,7 @@ package net.pterodactylus.sone.test import com.google.inject.Module import org.mockito.* import org.mockito.invocation.InvocationOnMock -import org.mockito.stubbing.OngoingStubbing +import org.mockito.stubbing.* inline fun mock(): T = Mockito.mock(T::class.java)!! inline fun mockBuilder(): T = Mockito.mock(T::class.java, Mockito.RETURNS_SELF)!! @@ -20,6 +20,7 @@ inline fun bindMock(): Module = Module { it!!.bind(T::class.java).toInstance(mock()) } inline fun whenever(methodCall: T) = Mockito.`when`(methodCall)!! +inline fun Stubber.whenever(mock: T) = `when`(mock)!! inline fun OngoingStubbing.thenReturnMock(): OngoingStubbing = this.thenReturn(mock()) diff --git a/src/test/kotlin/net/pterodactylus/sone/test/Mocks.kt b/src/test/kotlin/net/pterodactylus/sone/test/Mocks.kt new file mode 100644 index 0000000..48fb9bf --- /dev/null +++ b/src/test/kotlin/net/pterodactylus/sone/test/Mocks.kt @@ -0,0 +1,61 @@ +/** + * Sone - Mocks.kt - Copyright © 2019–2020 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 . + */ + +package net.pterodactylus.sone.test + +import com.google.common.base.* +import freenet.crypt.* +import freenet.keys.* +import net.pterodactylus.sone.data.* +import net.pterodactylus.sone.data.SoneOptions.* +import net.pterodactylus.sone.data.impl.* +import net.pterodactylus.sone.text.* +import net.pterodactylus.sone.utils.* + +val remoteSone1 = createRemoteSone() +val remoteSone2 = createRemoteSone() + +val localSone1 = createLocalSone() +val localSone2 = createLocalSone() + +fun createId() = InsertableClientSSK.createRandom(DummyRandomSource(), "").uri.routingKey.asFreenetBase64 + +fun createLocalSone(id: String? = createId()) = object : IdOnlySone(id) { + private val options = DefaultSoneOptions() + override fun getOptions() = options + override fun isLocal() = true +} +fun createRemoteSone(id: String? = createId()) = IdOnlySone(id) + +fun createPost(text: String = "", sone: Sone = remoteSone1, known: Boolean = false): Post.EmptyPost { + return object : Post.EmptyPost("post-id") { + override fun getSone() = sone + override fun getText() = text + override fun isKnown() = known + } +} + +fun emptyPostReply(text: String = "", post: Post? = createPost(), sone: Sone = remoteSone1, known: Boolean = false) = object : PostReply { + override val id = "reply-id" + override fun getSone() = sone + override fun getPostId() = post!!.id + override fun getPost(): Optional = Optional.fromNullable(post) + override fun getTime() = 1L + override fun getText() = text + override fun isKnown() = known + override fun setKnown(known: Boolean): PostReply = this +} diff --git a/src/test/kotlin/net/pterodactylus/sone/test/NotParallel.kt b/src/test/kotlin/net/pterodactylus/sone/test/NotParallel.kt new file mode 100644 index 0000000..6e00fbf --- /dev/null +++ b/src/test/kotlin/net/pterodactylus/sone/test/NotParallel.kt @@ -0,0 +1,8 @@ +package net.pterodactylus.sone.test + +/** + * Marker class for a JUnit [org.junit.experimental.categories.Category], to + * mark tests that should not be run parallel to other tests. + */ +class NotParallel + diff --git a/src/test/kotlin/net/pterodactylus/sone/test/TestLoaders.kt b/src/test/kotlin/net/pterodactylus/sone/test/TestLoaders.kt new file mode 100644 index 0000000..f71e9f5 --- /dev/null +++ b/src/test/kotlin/net/pterodactylus/sone/test/TestLoaders.kt @@ -0,0 +1,21 @@ +package net.pterodactylus.sone.test + +import net.pterodactylus.sone.main.* +import net.pterodactylus.util.template.* +import net.pterodactylus.util.web.* + +/** + * [Loaders] implementation for use in tests. Use [templates] to control what templates are + * returned by the [loadTemplate] method. + */ +class TestLoaders : Loaders { + + val templates = mutableMapOf() + + override fun loadTemplate(path: String) = templates[path] ?: Template() + + override fun loadStaticPage(basePath: String, prefix: String, mimeType: String) = TestPage() + + override fun getTemplateProvider() = TemplateProvider { _, _ -> Template() } + +} diff --git a/src/test/kotlin/net/pterodactylus/sone/test/TestPage.kt b/src/test/kotlin/net/pterodactylus/sone/test/TestPage.kt new file mode 100644 index 0000000..762f789 --- /dev/null +++ b/src/test/kotlin/net/pterodactylus/sone/test/TestPage.kt @@ -0,0 +1,14 @@ +package net.pterodactylus.sone.test + +import net.pterodactylus.util.web.* + +/** + * Dummy implementation of a [Page]. + */ +class TestPage : Page { + + override fun getPath() = "" + override fun isPrefixPage() = false + override fun handleRequest(freenetRequest: REQ, response: Response) = response + +} diff --git a/src/test/kotlin/net/pterodactylus/sone/test/TestUtils.kt b/src/test/kotlin/net/pterodactylus/sone/test/TestUtils.kt index 9996925..ef23d62 100644 --- a/src/test/kotlin/net/pterodactylus/sone/test/TestUtils.kt +++ b/src/test/kotlin/net/pterodactylus/sone/test/TestUtils.kt @@ -1,5 +1,6 @@ package net.pterodactylus.sone.test +import org.junit.rules.* import java.lang.reflect.* private val modifiers = Field::class.java.getDeclaredField("modifiers").apply { @@ -17,3 +18,5 @@ fun setField(instance: Any, name: String, value: Any?) { field.set(instance, value) } } + +inline fun ExpectedException.expect() = expect(T::class.java) diff --git a/src/test/kotlin/net/pterodactylus/sone/text/FreenetLinkPartTest.kt b/src/test/kotlin/net/pterodactylus/sone/text/FreenetLinkPartTest.kt index fcf6e67..aeba2c7 100644 --- a/src/test/kotlin/net/pterodactylus/sone/text/FreenetLinkPartTest.kt +++ b/src/test/kotlin/net/pterodactylus/sone/text/FreenetLinkPartTest.kt @@ -1,7 +1,7 @@ package net.pterodactylus.sone.text import org.hamcrest.MatcherAssert.assertThat -import org.hamcrest.Matchers.`is` +import org.hamcrest.Matchers.equalTo import org.junit.Test /** @@ -11,7 +11,7 @@ class FreenetLinkPartTest { @Test fun linkIsUsedAsTitleIfNoTextIsGiven() { - assertThat(FreenetLinkPart("link", "text", true).title, `is`("link")) + assertThat(FreenetLinkPart("link", "text", true).title, equalTo("link")) } } diff --git a/src/test/kotlin/net/pterodactylus/sone/text/LinkPartTest.kt b/src/test/kotlin/net/pterodactylus/sone/text/LinkPartTest.kt index 373a848..e38343c 100644 --- a/src/test/kotlin/net/pterodactylus/sone/text/LinkPartTest.kt +++ b/src/test/kotlin/net/pterodactylus/sone/text/LinkPartTest.kt @@ -1,7 +1,7 @@ package net.pterodactylus.sone.text import org.hamcrest.MatcherAssert.assertThat -import org.hamcrest.Matchers.`is` +import org.hamcrest.Matchers.equalTo import org.junit.Test /** @@ -11,7 +11,7 @@ class LinkPartTest { @Test fun linkIsUsedAsTitleIfNoTitleIsGiven() { - assertThat(LinkPart("link", "text").title, `is`("link")) + assertThat(LinkPart("link", "text").title, equalTo("link")) } } diff --git a/src/test/kotlin/net/pterodactylus/sone/text/SoneMentionDetectorTest.kt b/src/test/kotlin/net/pterodactylus/sone/text/SoneMentionDetectorTest.kt new file mode 100644 index 0000000..552b7ee --- /dev/null +++ b/src/test/kotlin/net/pterodactylus/sone/text/SoneMentionDetectorTest.kt @@ -0,0 +1,263 @@ +/** + * Sone - SoneMentionDetectorTest.kt - Copyright © 2019–2020 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 . + */ + +package net.pterodactylus.sone.text + +import com.google.common.eventbus.* +import net.pterodactylus.sone.core.event.* +import net.pterodactylus.sone.data.* +import net.pterodactylus.sone.database.* +import net.pterodactylus.sone.test.* +import org.hamcrest.MatcherAssert.* +import org.hamcrest.Matchers.* +import kotlin.test.* + +/** + * Unit test for [SoneMentionDetector]. + */ +@Suppress("UnstableApiUsage") +class SoneMentionDetectorTest { + + private val caughtExceptions = mutableListOf() + private val eventBus = EventBus { exception, _ -> caughtExceptions += exception } + private val soneProvider = TestSoneProvider() + private val postProvider = TestPostProvider() + private val soneTextParser = SoneTextParser(soneProvider, postProvider) + private val capturedFoundEvents = mutableListOf() + private val capturedRemovedEvents = mutableListOf() + private val postReplyProvider = TestPostReplyProvider() + + init { + eventBus.register(SoneMentionDetector(eventBus, soneTextParser, postReplyProvider)) + eventBus.register(object : Any() { + @Subscribe + fun captureFoundEvent(mentionOfLocalSoneFoundEvent: MentionOfLocalSoneFoundEvent) { + capturedFoundEvents += mentionOfLocalSoneFoundEvent + } + + @Subscribe + fun captureRemovedEvent(event: MentionOfLocalSoneRemovedEvent) { + capturedRemovedEvents += event + } + }) + } + + @Test + fun `detector does not emit event on post that does not contain any sones`() { + val post = createPost() + eventBus.post(NewPostFoundEvent(post)) + assertThat(capturedFoundEvents, emptyIterable()) + } + + @Test + fun `detector does not emit event on post that does contain two remote sones`() { + val post = createPost("text mentions sone://${remoteSone1.id} and sone://${remoteSone2.id}.") + eventBus.post(NewPostFoundEvent(post)) + assertThat(capturedFoundEvents, emptyIterable()) + } + + @Test + fun `detector emits event on post that contains links to a remote and a local sone`() { + val post = createPost("text mentions sone://${localSone1.id} and sone://${remoteSone2.id}.") + eventBus.post(NewPostFoundEvent(post)) + assertThat(capturedFoundEvents, contains(MentionOfLocalSoneFoundEvent(post))) + } + + @Test + fun `detector emits one event on post that contains two links to the same local sone`() { + val post = createPost("text mentions sone://${localSone1.id} and sone://${localSone1.id}.") + eventBus.post(NewPostFoundEvent(post)) + assertThat(capturedFoundEvents, contains(MentionOfLocalSoneFoundEvent(post))) + } + + @Test + fun `detector emits one event on post that contains links to two local sones`() { + val post = createPost("text mentions sone://${localSone1.id} and sone://${localSone2.id}.") + eventBus.post(NewPostFoundEvent(post)) + assertThat(capturedFoundEvents, contains(MentionOfLocalSoneFoundEvent(post))) + } + + @Test + fun `detector does not emit event for post by local sone`() { + val post = createPost("text mentions sone://${localSone1.id} and sone://${localSone2.id}.", localSone1) + eventBus.post(NewPostFoundEvent(post)) + assertThat(capturedFoundEvents, emptyIterable()) + } + + @Test + fun `detector does not emit event for reply that contains no sones`() { + val reply = emptyPostReply() + eventBus.post(NewPostReplyFoundEvent(reply)) + assertThat(capturedFoundEvents, emptyIterable()) + } + + @Test + fun `detector does not emit event for reply that contains two links to remote sones`() { + val reply = emptyPostReply("text mentions sone://${remoteSone1.id} and sone://${remoteSone2.id}.") + eventBus.post(NewPostReplyFoundEvent(reply)) + assertThat(capturedFoundEvents, emptyIterable()) + } + + @Test + fun `detector emits event on reply that contains links to a remote and a local sone`() { + val post = createPost() + val reply = emptyPostReply("text mentions sone://${remoteSone1.id} and sone://${localSone1.id}.", post) + eventBus.post(NewPostReplyFoundEvent(reply)) + assertThat(capturedFoundEvents, contains(MentionOfLocalSoneFoundEvent(post))) + } + + @Test + fun `detector emits one event on reply that contains two links to the same local sone`() { + val post = createPost() + val reply = emptyPostReply("text mentions sone://${localSone1.id} and sone://${localSone1.id}.", post) + eventBus.post(NewPostReplyFoundEvent(reply)) + assertThat(capturedFoundEvents, contains(MentionOfLocalSoneFoundEvent(post))) + } + + @Test + fun `detector emits one event on reply that contains two links to local sones`() { + val post = createPost() + val reply = emptyPostReply("text mentions sone://${localSone1.id} and sone://${localSone2.id}.", post) + eventBus.post(NewPostReplyFoundEvent(reply)) + assertThat(capturedFoundEvents, contains(MentionOfLocalSoneFoundEvent(post))) + } + + @Test + fun `detector does not emit event for reply by local sone`() { + val reply = emptyPostReply("text mentions sone://${localSone1.id} and sone://${localSone2.id}.", sone = localSone1) + eventBus.post(NewPostReplyFoundEvent(reply)) + assertThat(capturedFoundEvents, emptyIterable()) + } + + @Test + fun `detector does not emit event for reply without post`() { + val reply = emptyPostReply("text mentions sone://${localSone1.id} and sone://${localSone2.id}.", post = null) + eventBus.post(NewPostReplyFoundEvent(reply)) + assertThat(caughtExceptions, emptyIterable()) + assertThat(capturedFoundEvents, emptyIterable()) + } + + @Test + fun `detector does not emit removed event when a post without mention is removed`() { + val post = createPost() + eventBus.post(PostRemovedEvent(post)) + assertThat(capturedRemovedEvents, emptyIterable()) + } + + @Test + fun `detector does emit removed event when post with mention is removed`() { + val post = createPost("sone://${localSone1.id}") + eventBus.post(NewPostFoundEvent(post)) + eventBus.post(PostRemovedEvent(post)) + assertThat(capturedRemovedEvents, contains(MentionOfLocalSoneRemovedEvent(post))) + } + + @Test + fun `detector does not emit removed event when a post without mention is marked as known`() { + val post = createPost() + eventBus.post(MarkPostKnownEvent(post)) + assertThat(capturedRemovedEvents, emptyIterable()) + } + + @Test + fun `detector does emit removed event when post with mention is marked as known`() { + val post = createPost("sone://${localSone1.id}") + eventBus.post(NewPostFoundEvent(post)) + eventBus.post(MarkPostKnownEvent(post)) + assertThat(capturedRemovedEvents, contains(MentionOfLocalSoneRemovedEvent(post))) + } + + @Test + fun `detector does emit removed event when reply with mention is removed and no more mentions in that post exist`() { + val post = createPost() + val reply = emptyPostReply("sone://${localSone1.id}", post) + postReplyProvider.postReplies[post.id] = listOf(reply) + eventBus.post(NewPostReplyFoundEvent(reply)) + eventBus.post(PostReplyRemovedEvent(reply)) + assertThat(capturedRemovedEvents, contains(MentionOfLocalSoneRemovedEvent(post))) + } + + @Test + fun `detector does not emit removed event when reply with mention is removed and post mentions local sone`() { + val post = createPost("sone://${localSone1.id}") + val reply = emptyPostReply("sone://${localSone1.id}", post) + eventBus.post(NewPostReplyFoundEvent(reply)) + eventBus.post(PostReplyRemovedEvent(reply)) + assertThat(capturedRemovedEvents, emptyIterable()) + } + + @Test + fun `detector does emit removed event when reply with mention is removed and post mentions local sone but is known`() { + val post = createPost("sone://${localSone1.id}", known = true) + val reply = emptyPostReply("sone://${localSone1.id}", post) + eventBus.post(NewPostReplyFoundEvent(reply)) + eventBus.post(PostReplyRemovedEvent(reply)) + assertThat(capturedRemovedEvents, contains(MentionOfLocalSoneRemovedEvent(post))) + } + + @Test + fun `detector does not emit removed event when reply with mention is removed and post has other replies with mentions`() { + val post = createPost() + val reply1 = emptyPostReply("sone://${localSone1.id}", post) + val reply2 = emptyPostReply("sone://${localSone1.id}", post) + postReplyProvider.postReplies[post.id] = listOf(reply1, reply2) + eventBus.post(NewPostReplyFoundEvent(reply1)) + eventBus.post(PostReplyRemovedEvent(reply1)) + assertThat(capturedRemovedEvents, emptyIterable()) + } + + @Test + fun `detector does emit removed event when reply with mention is removed and post has other replies with mentions which are known`() { + val post = createPost() + val reply1 = emptyPostReply("sone://${localSone1.id}", post) + val reply2 = emptyPostReply("sone://${localSone1.id}", post, known = true) + postReplyProvider.postReplies[post.id] = listOf(reply1, reply2) + eventBus.post(NewPostReplyFoundEvent(reply1)) + eventBus.post(PostReplyRemovedEvent(reply1)) + assertThat(capturedRemovedEvents, contains(MentionOfLocalSoneRemovedEvent(post))) + } + +} + +private class TestSoneProvider : SoneProvider { + + override val sones: Collection get() = remoteSones + localSones + override val localSones: Collection get() = setOf(localSone1, localSone2) + override val remoteSones: Collection get() = setOf(remoteSone1, remoteSone2) + override val soneLoader: (String) -> Sone? get() = this::getSone + override fun getSone(soneId: String): Sone? = + localSones.firstOrNull { it.id == soneId } ?: remoteSones.firstOrNull { it.id == soneId } + +} + +private class TestPostProvider : PostProvider { + + override fun getPost(postId: String): Post? = null + override fun getPosts(soneId: String): Collection = emptyList() + override fun getDirectedPosts(recipientId: String): Collection = emptyList() + +} + +private class TestPostReplyProvider : PostReplyProvider { + + val replies = mutableMapOf() + val postReplies = mutableMapOf>() + + override fun getPostReply(id: String) = replies[id] + override fun getReplies(postId: String) = postReplies[postId] ?: emptyList() + +} diff --git a/src/test/kotlin/net/pterodactylus/sone/text/SonePartTest.kt b/src/test/kotlin/net/pterodactylus/sone/text/SonePartTest.kt index 31fca48..ef1b1c0 100644 --- a/src/test/kotlin/net/pterodactylus/sone/text/SonePartTest.kt +++ b/src/test/kotlin/net/pterodactylus/sone/text/SonePartTest.kt @@ -1,12 +1,10 @@ package net.pterodactylus.sone.text -import net.pterodactylus.sone.data.Profile import net.pterodactylus.sone.data.Sone -import net.pterodactylus.sone.test.mock +import net.pterodactylus.sone.test.* import org.hamcrest.MatcherAssert.assertThat -import org.hamcrest.Matchers.`is` +import org.hamcrest.Matchers.equalTo import org.junit.Test -import org.mockito.Mockito.`when` /** * Unit test for [SonePart]. @@ -16,15 +14,15 @@ class SonePartTest { private val sone = mock() init { - `when`(sone.profile).thenReturn(mock()) - `when`(sone.name).thenReturn("sone") + whenever(sone.profile).thenReturn(mock()) + whenever(sone.name).thenReturn("sone") } private val part = SonePart(sone) @Test fun textIsConstructedFromSonesNiceName() { - assertThat(part.text, `is`("sone")) + assertThat(part.text, equalTo("sone")) } } diff --git a/src/test/kotlin/net/pterodactylus/sone/text/SoneTextParserTest.kt b/src/test/kotlin/net/pterodactylus/sone/text/SoneTextParserTest.kt index b20225a..3367faf 100644 --- a/src/test/kotlin/net/pterodactylus/sone/text/SoneTextParserTest.kt +++ b/src/test/kotlin/net/pterodactylus/sone/text/SoneTextParserTest.kt @@ -1,5 +1,5 @@ /* - * Sone - SoneTextParserTest.kt - Copyright © 2011–2019 David Roden + * Sone - SoneTextParserTest.kt - Copyright © 2011–2020 David 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 diff --git a/src/test/kotlin/net/pterodactylus/sone/utils/BooleansTest.kt b/src/test/kotlin/net/pterodactylus/sone/utils/BooleansTest.kt index 56627c3..bd81f08 100644 --- a/src/test/kotlin/net/pterodactylus/sone/utils/BooleansTest.kt +++ b/src/test/kotlin/net/pterodactylus/sone/utils/BooleansTest.kt @@ -1,9 +1,8 @@ package net.pterodactylus.sone.utils -import org.hamcrest.MatcherAssert.assertThat -import org.hamcrest.Matchers.equalTo -import org.hamcrest.Matchers.nullValue -import org.junit.Test +import org.hamcrest.MatcherAssert.* +import org.hamcrest.Matchers.* +import kotlin.test.* /** * Unit test for [Booleans]. @@ -30,4 +29,44 @@ class BooleansTest { assertThat(true.ifFalse { true }, nullValue()) } + @Test + fun `onTrue returns true on true`() { + assertThat(true.onTrue {}, equalTo(true)) + } + + @Test + fun `onTrue returns false on false`() { + assertThat(false.onTrue {}, equalTo(false)) + } + + @Test + fun `onTrue is not executed on false`() { + assertThat(false.onTrue { throw RuntimeException() }, equalTo(false)) + } + + @Test(expected = RuntimeException::class) + fun `onTrue is executed on true`() { + true.onTrue { throw RuntimeException() } + } + + @Test + fun `onFalse returns true on true`() { + assertThat(true.onFalse {}, equalTo(true)) + } + + @Test + fun `onFalse returns false on false`() { + assertThat(false.onFalse {}, equalTo(false)) + } + + @Test + fun `onFalse is not executed on true`() { + assertThat(true.onFalse { throw RuntimeException() }, equalTo(true)) + } + + @Test(expected = RuntimeException::class) + fun `onFalse is executed on false`() { + false.onFalse { throw RuntimeException() } + } + } diff --git a/src/test/kotlin/net/pterodactylus/sone/utils/ObjectsTest.kt b/src/test/kotlin/net/pterodactylus/sone/utils/ObjectsTest.kt index 9e96bfe..76cc723 100644 --- a/src/test/kotlin/net/pterodactylus/sone/utils/ObjectsTest.kt +++ b/src/test/kotlin/net/pterodactylus/sone/utils/ObjectsTest.kt @@ -1,8 +1,9 @@ package net.pterodactylus.sone.utils -import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.MatcherAssert.* import org.hamcrest.Matchers.* -import org.junit.Test +import java.util.concurrent.* +import kotlin.test.* /** * Unit test for Object utils. @@ -19,9 +20,11 @@ class ObjectsTest { assertThat(null.asList(), empty()) } - @Test(expected = IllegalArgumentException::class) + @Test fun `exception is thrown for null and true condition`() { - null.throwOnNullIf(true) { IllegalArgumentException() } + assertFailsWith(IllegalArgumentException::class) { + null.throwOnNullIf(true) { IllegalArgumentException() } + } } @Test @@ -41,4 +44,29 @@ class ObjectsTest { assertThat(any.throwOnNullIf(false) { IllegalArgumentException() }, equalTo(any)) } + @Test + fun `onNull is executed on null`() { + val called = CountDownLatch(1) + null.onNull { called.countDown() } + assertThat(called.count, equalTo(0L)) + } + + @Test + fun `onNull returns null when called on null`() { + assertThat(null.onNull {}, nullValue()) + } + + @Test + fun `onNull is not executed on non-null`() { + val called = CountDownLatch(1) + Any().onNull { called.countDown() } + assertThat(called.count, equalTo(1L)) + } + + @Test + fun `onNull returns object when called on non-null`() { + val any = Any() + assertThat(any.onNull {}, sameInstance(any)) + } + } diff --git a/src/test/kotlin/net/pterodactylus/sone/utils/RenderablesTest.kt b/src/test/kotlin/net/pterodactylus/sone/utils/RenderablesTest.kt new file mode 100644 index 0000000..c2facce --- /dev/null +++ b/src/test/kotlin/net/pterodactylus/sone/utils/RenderablesTest.kt @@ -0,0 +1,36 @@ +/** + * Sone - RenderablesTest.kt - Copyright © 2019–2020 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 . + */ + +package net.pterodactylus.sone.utils + +import net.pterodactylus.util.io.* +import org.hamcrest.MatcherAssert.* +import org.hamcrest.Matchers.* +import kotlin.test.* + +/** + * Unit tests for tools in `Renderables.kt`. + */ +class RenderablesTest { + + @Test + fun `render method renders notification`() { + val notification = Renderable { writer -> writer.use { it.append("Test!\n") } } + assertThat(notification.render(), equalTo("Test!\n")) + } + +} diff --git a/src/test/kotlin/net/pterodactylus/sone/web/AllPagesTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/AllPagesTest.kt index cf6ab15..842feb4 100644 --- a/src/test/kotlin/net/pterodactylus/sone/web/AllPagesTest.kt +++ b/src/test/kotlin/net/pterodactylus/sone/web/AllPagesTest.kt @@ -102,11 +102,6 @@ class AllPagesTest { } @Test - fun `distrust page can be injected`() { - assertThat(allPages.distrustPage, instanceOf()) - } - - @Test fun `edit album page can be injected`() { assertThat(allPages.editAlbumPage, instanceOf()) } @@ -197,11 +192,6 @@ class AllPagesTest { } @Test - fun `trust page can be injected`() { - assertThat(allPages.trustPage, instanceOf()) - } - - @Test fun `unbookmark page can be injected`() { assertThat(allPages.unbookmarkPage, instanceOf()) } @@ -222,11 +212,6 @@ class AllPagesTest { } @Test - fun `untrust page can be injected`() { - assertThat(allPages.untrustPage, instanceOf()) - } - - @Test fun `upload image page can be injected`() { assertThat(allPages.uploadImagePage, instanceOf()) } diff --git a/src/test/kotlin/net/pterodactylus/sone/web/PageToadletRegistryTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/PageToadletRegistryTest.kt index d242b32..2134661 100644 --- a/src/test/kotlin/net/pterodactylus/sone/web/PageToadletRegistryTest.kt +++ b/src/test/kotlin/net/pterodactylus/sone/web/PageToadletRegistryTest.kt @@ -5,10 +5,10 @@ import freenet.clients.http.* import net.pterodactylus.sone.main.* import net.pterodactylus.sone.test.* import net.pterodactylus.sone.web.page.* -import net.pterodactylus.util.web.* import org.junit.* import org.junit.rules.* import org.mockito.Mockito.* +import kotlin.test.Test class PageToadletRegistryTest { @@ -35,7 +35,7 @@ class PageToadletRegistryTest { verify(pageMaker).addNavigationCategory("/Sone/index.html", "Navigation.Menu.Sone.Name", "Navigation.Menu.Sone.Tooltip", sonePlugin) } - private val page = TestPage() + private val page = TestPage() @Test fun `adding a page without menuname will add it correctly`() { @@ -80,16 +80,70 @@ class PageToadletRegistryTest { verify(toadletContainer).unregister(toadletWithMenuname) } + @Test + fun `adding a debug page will not add it to the container`() { + val toadlet = createPageToadlet() + whenever(pageToadletFactory.createPageToadlet(page)).thenReturn(toadlet) + pageToadletRegistry.addDebugPage(page) + pageToadletRegistry.registerToadlets() + verify(toadletContainer, never()).register(toadlet, null, "/Sone/", true, false) + } + + @Test + fun `adding a debug page and activating debug mode will add it to the container`() { + val toadlet = createPageToadlet() + whenever(pageToadletFactory.createPageToadlet(page)).thenReturn(toadlet) + pageToadletRegistry.addDebugPage(page) + pageToadletRegistry.registerToadlets() + pageToadletRegistry.activateDebugMode() + verify(toadletContainer).register(toadlet, null, "/Sone/", true, false) + } + + @Test + fun `adding a debug page and activating debug mode twice will add it to the container once`() { + val toadlet = createPageToadlet() + whenever(pageToadletFactory.createPageToadlet(page)).thenReturn(toadlet) + pageToadletRegistry.addDebugPage(page) + pageToadletRegistry.registerToadlets() + pageToadletRegistry.activateDebugMode() + pageToadletRegistry.activateDebugMode() + verify(toadletContainer, times(1)).register(toadlet, null, "/Sone/", true, false) + } + + @Test + fun `debug pages are ungegistered from the container`() { + val toadlet = createPageToadlet() + whenever(pageToadletFactory.createPageToadlet(page)).thenReturn(toadlet) + pageToadletRegistry.addDebugPage(page) + pageToadletRegistry.registerToadlets() + pageToadletRegistry.activateDebugMode() + pageToadletRegistry.unregisterToadlets() + verify(toadletContainer).unregister(toadlet) + } + + @Test + fun `inactive debug pages are not ungegistered from the container`() { + val toadlet = createPageToadlet() + whenever(pageToadletFactory.createPageToadlet(page)).thenReturn(toadlet) + pageToadletRegistry.addDebugPage(page) + pageToadletRegistry.registerToadlets() + pageToadletRegistry.unregisterToadlets() + verify(toadletContainer, never()).unregister(toadlet) + } + + @Test + fun `debug page can not be added after registering`() { + val toadlet = createPageToadlet() + whenever(pageToadletFactory.createPageToadlet(page)).thenReturn(toadlet) + pageToadletRegistry.registerToadlets() + expectedException.expect(IllegalStateException::class.java) + pageToadletRegistry.addDebugPage(page) + } + private fun createPageToadlet(menuName: String? = null) = mock().apply { whenever(this.path()).thenReturn("/Sone/") whenever(this.menuName).thenReturn(menuName) } - private class TestPage : Page { - override fun getPath() = "" - override fun isPrefixPage() = false - override fun handleRequest(freenetRequest: FreenetRequest, response: Response) = response - } - } diff --git a/src/test/kotlin/net/pterodactylus/sone/web/WebInterfaceModuleTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/WebInterfaceModuleTest.kt index 9137da3..8840caf 100644 --- a/src/test/kotlin/net/pterodactylus/sone/web/WebInterfaceModuleTest.kt +++ b/src/test/kotlin/net/pterodactylus/sone/web/WebInterfaceModuleTest.kt @@ -3,7 +3,6 @@ package net.pterodactylus.sone.web import com.google.inject.Guice.* import freenet.client.* import freenet.clients.http.* -import freenet.l10n.* import freenet.support.api.* import net.pterodactylus.sone.core.* import net.pterodactylus.sone.data.* @@ -14,22 +13,28 @@ import net.pterodactylus.sone.main.* import net.pterodactylus.sone.template.* import net.pterodactylus.sone.test.* import net.pterodactylus.sone.text.* +import net.pterodactylus.sone.web.notification.* import net.pterodactylus.sone.web.page.* +import net.pterodactylus.util.notify.* import net.pterodactylus.util.template.* import net.pterodactylus.util.web.* import org.hamcrest.MatcherAssert.* import org.hamcrest.Matchers.* -import org.junit.* +import java.util.* +import kotlin.test.* class WebInterfaceModuleTest { private val webInterfaceModule = WebInterfaceModule() - private val l10n = mock() private val loaders = mock() + private val translation = object : Translation { + override val currentLocale = Locale.ENGLISH + override fun translate(key: String) = if (key == "View.Sone.Text.UnknownDate") "unknown" else key + } private val additionalModules = arrayOf( Core::class.isProvidedByMock(), SoneProvider::class.isProvidedByMock(), - BaseL10n::class.isProvidedBy(l10n), + Translation::class.isProvidedBy(translation), SoneTextParser::class.isProvidedByMock(), ElementLoader::class.isProvidedByMock(), Loaders::class.isProvidedBy(loaders), @@ -190,7 +195,6 @@ class WebInterfaceModuleTest { @Test fun `unknown date filter uses correct l10n key`() { - whenever(l10n.getString("View.Sone.Text.UnknownDate")).thenReturn("unknown") assertThat(getFilter("unknown")!!.format(null, 0L, emptyMap()), equalTo("unknown")) } @@ -200,6 +204,11 @@ class WebInterfaceModuleTest { } @Test + fun `template context contains duration format filter`() { + verifyFilter("duration") + } + + @Test fun `template context contains collection sort filter`() { verifyFilter("sort") } @@ -234,6 +243,11 @@ class WebInterfaceModuleTest { verifyFilter("paginate") } + @Test + fun `template context histogram renderer`() { + verifyFilter("render-histogram") + } + private inline fun verifyFilter(name: String) { assertThat(getFilter(name), instanceOf(F::class.java)) } @@ -242,9 +256,7 @@ class WebInterfaceModuleTest { @Test fun `template context factory is created as singleton`() { - val factory1 = injector.getInstance() - val factory2 = injector.getInstance() - assertThat(factory1, sameInstance(factory2)) + injector.verifySingletonInstance() } @Test @@ -266,7 +278,12 @@ class WebInterfaceModuleTest { @Test fun `page toadlet factory is created with correct prefix`() { val page = mock>() - assertThat(injector.getInstance().createPageToadlet(page).path(), startsWith("/Sone/")) + assertThat(injector.getInstance().createPageToadlet(page).path(), startsWith("/Sone/")) + } + + @Test + fun `notification manager is created as singleton`() { + injector.verifySingletonInstance() } } diff --git a/src/test/kotlin/net/pterodactylus/sone/web/ajax/CreatePostAjaxPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/ajax/CreatePostAjaxPageTest.kt index 821198f..b79e6df 100644 --- a/src/test/kotlin/net/pterodactylus/sone/web/ajax/CreatePostAjaxPageTest.kt +++ b/src/test/kotlin/net/pterodactylus/sone/web/ajax/CreatePostAjaxPageTest.kt @@ -40,7 +40,7 @@ class CreatePostAjaxPageTest : JsonPageTest("createPost.ajax", pageSupplier = :: fun `request with valid data creates post`() { addRequestParameter("text", "test") val post = createPost() - whenever(core.createPost(currentSone, Optional.absent(), "test")).thenReturn(post) + whenever(core.createPost(currentSone, null, "test")).thenReturn(post) assertThatJsonIsSuccessful() assertThat(json["postId"]?.asText(), equalTo("id")) assertThat(json["sone"]?.asText(), equalTo(currentSone.id)) @@ -52,7 +52,7 @@ class CreatePostAjaxPageTest : JsonPageTest("createPost.ajax", pageSupplier = :: addRequestParameter("text", "test") addRequestParameter("recipient", "invalid") val post = createPost() - whenever(core.createPost(currentSone, Optional.absent(), "test")).thenReturn(post) + whenever(core.createPost(currentSone, null, "test")).thenReturn(post) assertThatJsonIsSuccessful() assertThat(json["postId"]?.asText(), equalTo("id")) assertThat(json["sone"]?.asText(), equalTo(currentSone.id)) @@ -66,7 +66,7 @@ class CreatePostAjaxPageTest : JsonPageTest("createPost.ajax", pageSupplier = :: val recipient = mock().apply { whenever(id).thenReturn("valid") } addSone(recipient) val post = createPost("valid") - whenever(core.createPost(currentSone, Optional.of(recipient), "test")).thenReturn(post) + whenever(core.createPost(currentSone, recipient, "test")).thenReturn(post) assertThatJsonIsSuccessful() assertThat(json["postId"]?.asText(), equalTo("id")) assertThat(json["sone"]?.asText(), equalTo(currentSone.id)) @@ -78,7 +78,7 @@ class CreatePostAjaxPageTest : JsonPageTest("createPost.ajax", pageSupplier = :: addRequestParameter("text", "Link http://freenet.test:8888/KSK@foo is filtered") addRequestHeader("Host", "freenet.test:8888") val post = createPost() - whenever(core.createPost(currentSone, Optional.absent(), "Link KSK@foo is filtered")).thenReturn(post) + whenever(core.createPost(currentSone, null, "Link KSK@foo is filtered")).thenReturn(post) assertThatJsonIsSuccessful() assertThat(json["postId"]?.asText(), equalTo("id")) assertThat(json["sone"]?.asText(), equalTo(currentSone.id)) diff --git a/src/test/kotlin/net/pterodactylus/sone/web/ajax/DistrustAjaxPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/ajax/DistrustAjaxPageTest.kt deleted file mode 100644 index 868819e..0000000 --- a/src/test/kotlin/net/pterodactylus/sone/web/ajax/DistrustAjaxPageTest.kt +++ /dev/null @@ -1,53 +0,0 @@ -package net.pterodactylus.sone.web.ajax - -import net.pterodactylus.sone.data.Sone -import net.pterodactylus.sone.test.getInstance -import net.pterodactylus.sone.test.mock -import net.pterodactylus.sone.web.baseInjector -import org.hamcrest.MatcherAssert.assertThat -import org.hamcrest.Matchers.equalTo -import org.hamcrest.Matchers.notNullValue -import org.junit.Test -import org.mockito.Mockito.verify - -/** - * Unit test for [DistrustAjaxPage]. - */ -class DistrustAjaxPageTest : JsonPageTest("distrustSone.ajax", pageSupplier = ::DistrustAjaxPage) { - - @Test - fun `request with missing sone results in invalid-sone-id`() { - assertThatJsonFailed("invalid-sone-id") - } - - @Test - fun `request with invalid sone results in invalid-sone-id`() { - addRequestParameter("sone", "invalid-sone") - assertThatJsonFailed("invalid-sone-id") - } - - @Test - fun `request with valid sone results in distrusted sone`() { - val sone = mock() - addSone(sone, "sone-id") - addRequestParameter("sone", "sone-id") - assertThatJsonIsSuccessful() - verify(core).distrustSone(currentSone, sone) - } - - @Test - fun `request with valid sone results in correct trust value being sent back`() { - core.preferences.newNegativeTrust = -33 - val sone = mock() - addSone(sone, "sone-id") - addRequestParameter("sone", "sone-id") - assertThatJsonIsSuccessful() - assertThat(json["trustValue"]?.asInt(), equalTo(-33)) - } - - @Test - fun `page can be created by dependency injection`() { - assertThat(baseInjector.getInstance(), notNullValue()) - } - -} diff --git a/src/test/kotlin/net/pterodactylus/sone/web/ajax/TestObjects.kt b/src/test/kotlin/net/pterodactylus/sone/web/ajax/TestObjects.kt index a42e5ba..f902533 100644 --- a/src/test/kotlin/net/pterodactylus/sone/web/ajax/TestObjects.kt +++ b/src/test/kotlin/net/pterodactylus/sone/web/ajax/TestObjects.kt @@ -3,7 +3,6 @@ package net.pterodactylus.sone.web.ajax import com.fasterxml.jackson.databind.ObjectMapper import com.google.common.eventbus.EventBus import freenet.clients.http.ToadletContext -import freenet.l10n.BaseL10n import freenet.support.SimpleReadOnlyArrayBucket import freenet.support.api.HTTPRequest import net.pterodactylus.sone.core.Core @@ -20,6 +19,7 @@ import net.pterodactylus.sone.data.Sone import net.pterodactylus.sone.data.Sone.SoneStatus import net.pterodactylus.sone.data.Sone.SoneStatus.idle import net.pterodactylus.sone.data.SoneOptions.DefaultSoneOptions +import net.pterodactylus.sone.freenet.* import net.pterodactylus.sone.test.deepMock import net.pterodactylus.sone.test.get import net.pterodactylus.sone.test.mock @@ -32,7 +32,7 @@ import net.pterodactylus.util.template.TemplateContextFactory import net.pterodactylus.util.web.Method.GET import net.pterodactylus.util.web.Method.POST import org.mockito.ArgumentMatchers -import java.util.NoSuchElementException +import java.util.* import javax.naming.SizeLimitExceededException /** @@ -44,7 +44,6 @@ open class TestObjects { val webInterface = mock() var formPassword = "form-password" - val l10n = mock() val core = mock() val eventBus = mock() val preferences = Preferences(eventBus) @@ -74,6 +73,11 @@ open class TestObjects { val images = mutableMapOf() val translations = mutableMapOf() + private val translation = object : Translation { + override val currentLocale = Locale.ENGLISH + override fun translate(key: String) = translations[key] ?: "" + } + init { whenever(webInterface.templateContextFactory).thenReturn(TemplateContextFactory()) whenever(webInterface.getCurrentSone(ArgumentMatchers.eq(toadletContext), ArgumentMatchers.anyBoolean())).thenReturn(currentSone) @@ -85,9 +89,7 @@ open class TestObjects { whenever(webInterface.getNotification(ArgumentMatchers.anyString())).then { notifications[it[0]].asOptional() } whenever(webInterface.getNewPosts(currentSone)).thenAnswer { newPosts.values } whenever(webInterface.getNewReplies(currentSone)).thenAnswer { newReplies.values } - whenever(webInterface.l10n).thenReturn(l10n) - - whenever(l10n.getString(ArgumentMatchers.anyString())).then { translations[it[0]] } + whenever(webInterface.translation).thenReturn(translation) whenever(core.preferences).thenReturn(preferences) whenever(core.updateChecker).thenReturn(updateChecker) diff --git a/src/test/kotlin/net/pterodactylus/sone/web/ajax/TrustAjaxPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/ajax/TrustAjaxPageTest.kt deleted file mode 100644 index d557790..0000000 --- a/src/test/kotlin/net/pterodactylus/sone/web/ajax/TrustAjaxPageTest.kt +++ /dev/null @@ -1,47 +0,0 @@ -package net.pterodactylus.sone.web.ajax - -import net.pterodactylus.sone.data.Sone -import net.pterodactylus.sone.test.getInstance -import net.pterodactylus.sone.test.mock -import net.pterodactylus.sone.web.baseInjector -import org.hamcrest.MatcherAssert.assertThat -import org.hamcrest.Matchers.equalTo -import org.hamcrest.Matchers.notNullValue -import org.junit.Test -import org.mockito.Mockito.verify - -/** - * Unit test for [TrustAjaxPage]. - */ -class TrustAjaxPageTest : JsonPageTest("trustSone.ajax", requiresLogin = true, needsFormPassword = true, pageSupplier = ::TrustAjaxPage) { - - private val sone = mock() - - @Test - fun `request with invalid sone results in invalid-sone-id`() { - assertThatJsonFailed("invalid-sone-id") - } - - @Test - fun `request with valid sone trust sone`() { - addSone(sone, "sone-id") - addRequestParameter("sone", "sone-id") - assertThatJsonIsSuccessful() - verify(core).trustSone(currentSone, sone) - } - - @Test - fun `request with valid sone returns positive trust value`() { - addSone(sone, "sone-id") - addRequestParameter("sone", "sone-id") - core.preferences.newPositiveTrust = 31 - assertThatJsonIsSuccessful() - assertThat(json["trustValue"]?.asInt(), equalTo(31)) - } - - @Test - fun `page can be created by dependency injection`() { - assertThat(baseInjector.getInstance(), notNullValue()) - } - -} diff --git a/src/test/kotlin/net/pterodactylus/sone/web/ajax/UntrustAjaxPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/ajax/UntrustAjaxPageTest.kt deleted file mode 100644 index b693194..0000000 --- a/src/test/kotlin/net/pterodactylus/sone/web/ajax/UntrustAjaxPageTest.kt +++ /dev/null @@ -1,50 +0,0 @@ -package net.pterodactylus.sone.web.ajax - -import net.pterodactylus.sone.data.* -import net.pterodactylus.sone.test.* -import net.pterodactylus.sone.web.* -import org.hamcrest.MatcherAssert.* -import org.hamcrest.Matchers.* -import org.junit.* -import org.mockito.Mockito.* - -/** - * Unit test for [UntrustAjaxPage]. - */ -class UntrustAjaxPageTest : JsonPageTest("untrustSone.ajax", pageSupplier = ::UntrustAjaxPage) { - - @Test - fun `request without sone results in invalid-sone-id`() { - assertThatJsonFailed("invalid-sone-id") - } - - @Test - fun `request with invalid sone results in invalid-sone-id`() { - addRequestParameter("sone", "invalid") - assertThatJsonFailed("invalid-sone-id") - } - - @Test - fun `request with valid sone results in sone being untrusted`() { - val sone = mock() - addSone(sone, "sone-id") - addRequestParameter("sone", "sone-id") - assertThatJsonIsSuccessful() - verify(core).untrustSone(currentSone, sone) - } - - @Test - fun `request with valid sone results in null trust value being returned`() { - val sone = mock() - addSone(sone, "sone-id") - addRequestParameter("sone", "sone-id") - assertThatJsonIsSuccessful() - assertThat(json["trustValue"], nullValue()) - } - - @Test - fun `page can be created by dependency injection`() { - assertThat(baseInjector.getInstance(), notNullValue()) - } - -} diff --git a/src/test/kotlin/net/pterodactylus/sone/web/notification/ConfigNotReadHandlerTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/notification/ConfigNotReadHandlerTest.kt new file mode 100644 index 0000000..7f3169b --- /dev/null +++ b/src/test/kotlin/net/pterodactylus/sone/web/notification/ConfigNotReadHandlerTest.kt @@ -0,0 +1,48 @@ +/** + * Sone - ConfigNotReadHandlerTest.kt - Copyright © 2019–2020 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 . + */ + +package net.pterodactylus.sone.web.notification + +import com.google.common.eventbus.* +import net.pterodactylus.sone.core.event.* +import net.pterodactylus.util.notify.* +import net.pterodactylus.util.template.* +import org.hamcrest.MatcherAssert.* +import org.hamcrest.Matchers.* +import org.junit.* + +/** + * Unit test for [ConfigNotReadHandler]. + */ +@Suppress("UnstableApiUsage") +class ConfigNotReadHandlerTest { + + private val eventBus = EventBus() + private val notificationManager = NotificationManager() + private val notification = TemplateNotification("", Template()) + + init { + eventBus.register(ConfigNotReadHandler(notificationManager, notification)) + } + + @Test + fun `handler adds notification to manager when config was not read`() { + eventBus.post(ConfigNotRead()) + assertThat(notificationManager.notifications, contains(notification)) + } + +} diff --git a/src/test/kotlin/net/pterodactylus/sone/web/notification/FirstStartHandlerTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/notification/FirstStartHandlerTest.kt new file mode 100644 index 0000000..356eeea --- /dev/null +++ b/src/test/kotlin/net/pterodactylus/sone/web/notification/FirstStartHandlerTest.kt @@ -0,0 +1,53 @@ +/** + * Sone - FirstStartHandlerTest.kt - Copyright © 2019–2020 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 . + */ + +package net.pterodactylus.sone.web.notification + +import com.google.common.eventbus.* +import net.pterodactylus.sone.core.event.* +import net.pterodactylus.util.notify.* +import net.pterodactylus.util.template.* +import org.hamcrest.MatcherAssert.* +import org.hamcrest.Matchers.* +import kotlin.test.* + +/** + * Unit test for [FirstStartHandler]. + */ +@Suppress("UnstableApiUsage") +class FirstStartHandlerTest { + + private val eventBus = EventBus() + private val notificationManager = NotificationManager() + private val notification = TemplateNotification(Template()) + + init { + eventBus.register(FirstStartHandler(notificationManager, notification)) + } + + @Test + fun `handler can be created`() { + FirstStartHandler(notificationManager, notification) + } + + @Test + fun `handler adds notification to manager on first start event`() { + eventBus.post(FirstStart()) + assertThat(notificationManager.notifications, contains(notification)) + } + +} diff --git a/src/test/kotlin/net/pterodactylus/sone/web/notification/ImageInsertHandlerTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/notification/ImageInsertHandlerTest.kt new file mode 100644 index 0000000..a50b4ae --- /dev/null +++ b/src/test/kotlin/net/pterodactylus/sone/web/notification/ImageInsertHandlerTest.kt @@ -0,0 +1,107 @@ +/** + * Sone - ImageInsertHandlerTest.kt - Copyright © 2019–2020 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 . + */ + +package net.pterodactylus.sone.web.notification + +import com.google.common.eventbus.* +import freenet.keys.FreenetURI.* +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 kotlin.test.* + +/** + * Unit test for [ImageInsertHandler]. + */ +@Suppress("UnstableApiUsage") +class ImageInsertHandlerTest { + + private val eventBus = EventBus() + private val notificationManager = NotificationManager() + private val imageInsertingNotification = ListNotification("", "", Template()) + private val imageFailedNotification = ListNotification("", "", Template()) + private val imageInsertedNotification = ListNotification("", "", Template()) + + init { + eventBus.register(ImageInsertHandler(notificationManager, imageInsertingNotification, imageFailedNotification, imageInsertedNotification)) + } + + @Test + fun `handler adds notification when image insert starts`() { + eventBus.post(ImageInsertStartedEvent(image)) + assertThat(notificationManager.notifications, contains(imageInsertingNotification)) + } + + @Test + fun `handler adds image to notification when image insert starts`() { + eventBus.post(ImageInsertStartedEvent(image)) + assertThat(imageInsertingNotification.elements, contains(image)) + } + + @Test + fun `handler removes image from inserting notification when insert is aborted`() { + eventBus.post(ImageInsertStartedEvent(image)) + eventBus.post(ImageInsertAbortedEvent(image)) + assertThat(imageInsertingNotification.elements, emptyIterable()) + } + + @Test + fun `handler removes image from inserting notification when insert fails`() { + eventBus.post(ImageInsertStartedEvent(image)) + eventBus.post(ImageInsertFailedEvent(image, Throwable())) + assertThat(imageInsertingNotification.elements, emptyIterable()) + } + + @Test + fun `handler adds image to insert-failed notification when insert fails`() { + eventBus.post(ImageInsertFailedEvent(image, Throwable())) + assertThat(imageFailedNotification.elements, contains(image)) + } + + @Test + fun `handler adds insert-failed notification to manager when insert fails`() { + eventBus.post(ImageInsertFailedEvent(image, Throwable())) + assertThat(notificationManager.notifications, contains(imageFailedNotification)) + } + + @Test + fun `handler removes image from inserting notification when insert succeeds`() { + eventBus.post(ImageInsertStartedEvent(image)) + eventBus.post(ImageInsertFinishedEvent(image, EMPTY_CHK_URI)) + assertThat(imageInsertingNotification.elements, emptyIterable()) + } + + @Test + fun `handler adds image to inserted notification when insert succeeds`() { + eventBus.post(ImageInsertFinishedEvent(image, EMPTY_CHK_URI)) + assertThat(imageInsertedNotification.elements, contains(image)) + } + + @Test + fun `handler adds inserted notification to manager when insert succeeds`() { + eventBus.post(ImageInsertFinishedEvent(image, EMPTY_CHK_URI)) + assertThat(notificationManager.notifications, contains(imageInsertedNotification)) + } + +} + +private val image: Image = ImageImpl() diff --git a/src/test/kotlin/net/pterodactylus/sone/web/notification/LocalPostHandlerTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/notification/LocalPostHandlerTest.kt new file mode 100644 index 0000000..22648b5 --- /dev/null +++ b/src/test/kotlin/net/pterodactylus/sone/web/notification/LocalPostHandlerTest.kt @@ -0,0 +1,118 @@ +/** + * Sone - LocalPostHandlerTest.kt - Copyright © 2019–2020 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 . + */ + +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 kotlin.test.* + +/** + * Unit test for [LocalPostHandler]. + */ +class LocalPostHandlerTest { + + private val eventBus = EventBus() + private val notificationManager = NotificationManager() + private val notification = ListNotification("", "", Template()) + + init { + eventBus.register(LocalPostHandler(notificationManager, notification)) + } + + @Test + fun `handler adds post by local sone to notification`() { + eventBus.post(NewPostFoundEvent(localPost)) + assertThat(notification.elements, contains(localPost)) + } + + @Test + fun `handler does not add post by remote sone to notification`() { + eventBus.post(NewPostFoundEvent(remotePost)) + assertThat(notification.elements, emptyIterable()) + } + + @Test + fun `handler does not add notification to manager for post by remote sone`() { + eventBus.post(NewPostFoundEvent(remotePost)) + assertThat(notificationManager.notifications, not(hasItem(notification))) + } + + @Test + fun `handler adds notification to manager`() { + eventBus.post(NewPostFoundEvent(localPost)) + assertThat(notificationManager.notifications, contains(notification)) + } + + @Test + fun `handler does not add notification during first start`() { + notificationManager.firstStart() + eventBus.post(NewPostFoundEvent(localPost)) + assertThat(notificationManager.notifications, not(hasItem(notification))) + } + + @Test + fun `handler removes post from notification when post is removed`() { + notification.add(localPost) + notificationManager.addNotification(notification) + eventBus.post(PostRemovedEvent(localPost)) + assertThat(notification.elements, emptyIterable()) + } + + @Test + fun `handler does not remove remote post from notification when post is removed`() { + notification.add(remotePost) + notificationManager.addNotification(notification) + eventBus.post(PostRemovedEvent(remotePost)) + assertThat(notification.elements, contains(remotePost)) + } + + @Test + fun `handler removes post from notification when post is marked as known`() { + notification.add(localPost) + notificationManager.addNotification(notification) + eventBus.post(MarkPostKnownEvent(localPost)) + assertThat(notification.elements, emptyIterable()) + } + + @Test + fun `handler does not remove remote post from notification when post is marked as known`() { + notification.add(remotePost) + notificationManager.addNotification(notification) + eventBus.post(MarkPostKnownEvent(remotePost)) + assertThat(notification.elements, contains(remotePost)) + } + +} + +private val localSone: Sone = object : IdOnlySone("local-sone") { + override fun isLocal() = true +} +private val localPost: Post = object : Post.EmptyPost("local-post") { + override fun getSone() = localSone +} +private val remoteSone: Sone = IdOnlySone("remote-sone") +private val remotePost: Post = object : Post.EmptyPost("remote-post") { + override fun getSone() = remoteSone +} diff --git a/src/test/kotlin/net/pterodactylus/sone/web/notification/LocalReplyHandlerTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/notification/LocalReplyHandlerTest.kt new file mode 100644 index 0000000..3532e77 --- /dev/null +++ b/src/test/kotlin/net/pterodactylus/sone/web/notification/LocalReplyHandlerTest.kt @@ -0,0 +1,85 @@ +/** + * Sone - LocalReplyHandlerTest.kt - Copyright © 2019–2020 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 . + */ + +package net.pterodactylus.sone.web.notification + +import net.pterodactylus.sone.core.event.* +import net.pterodactylus.sone.data.* +import net.pterodactylus.sone.notify.* +import net.pterodactylus.sone.test.* +import net.pterodactylus.util.template.* +import org.hamcrest.MatcherAssert.* +import org.hamcrest.Matchers.* +import kotlin.test.* + +/** + * Unit test for [LocalReplyHandler]. + */ +class LocalReplyHandlerTest { + + private val notification = ListNotification("", "", Template()) + private val localReplyHandlerTester = NotificationHandlerTester { LocalReplyHandler(it, notification) } + + @Test + fun `handler does not add reply to notification`() { + localReplyHandlerTester.sendEvent(NewPostReplyFoundEvent(remoteReply)) + assertThat(notification.elements, emptyIterable()) + } + + @Test + fun `handler does add local reply to notification`() { + localReplyHandlerTester.sendEvent(NewPostReplyFoundEvent(localReply)) + assertThat(notification.elements, contains(localReply)) + } + + @Test + fun `handler adds notification to manager`() { + localReplyHandlerTester.sendEvent(NewPostReplyFoundEvent(localReply)) + assertThat(localReplyHandlerTester.notifications, hasItem(notification)) + } + + @Test + fun `handler does not add notification to manager for remote reply`() { + localReplyHandlerTester.sendEvent(NewPostReplyFoundEvent(remoteReply)) + assertThat(localReplyHandlerTester.notifications, not(hasItem(notification))) + } + + @Test + fun `handler does not add notification to manager during first start`() { + localReplyHandlerTester.firstStart() + localReplyHandlerTester.sendEvent(NewPostReplyFoundEvent(localReply)) + assertThat(localReplyHandlerTester.notifications, not(hasItem(notification))) + } + + @Test + fun `handler removes reply from notification if reply is removed`() { + notification.add(localReply) + localReplyHandlerTester.sendEvent(PostReplyRemovedEvent(localReply)) + assertThat(notification.elements, not(hasItem(localReply))) + } + + @Test + fun `handler removes reply from notification if reply is marked as known`() { + notification.add(localReply) + localReplyHandlerTester.sendEvent(MarkPostReplyKnownEvent(localReply)) + assertThat(notification.elements, not(hasItem(localReply))) + } + +} + +private val localReply = emptyPostReply(sone = localSone1) +private val remoteReply = emptyPostReply() diff --git a/src/test/kotlin/net/pterodactylus/sone/web/notification/MarkPostKnownDuringFirstStartHandlerTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/notification/MarkPostKnownDuringFirstStartHandlerTest.kt new file mode 100644 index 0000000..5b8e41a --- /dev/null +++ b/src/test/kotlin/net/pterodactylus/sone/web/notification/MarkPostKnownDuringFirstStartHandlerTest.kt @@ -0,0 +1,59 @@ +/** + * Sone - MarkPostKnownDuringFirstStartHandlerTest.kt - Copyright © 2019–2020 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 . + */ + +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.util.notify.* +import org.hamcrest.MatcherAssert.* +import org.hamcrest.Matchers.* +import java.util.function.* +import kotlin.test.* + +/** + * Unit test for [MarkPostKnownDuringFirstStartHandler]. + */ +@Suppress("UnstableApiUsage") +class MarkPostKnownDuringFirstStartHandlerTest { + + private val eventBus = EventBus() + private val notificationManager = NotificationManager() + private val markedPosts = mutableListOf() + private val handler = MarkPostKnownDuringFirstStartHandler(notificationManager, Consumer { markedPosts += it }) + + init { + eventBus.register(handler) + } + + @Test + fun `post is not marked as known if not during first start`() { + eventBus.post(NewPostFoundEvent(post)) + assertThat(markedPosts, emptyIterable()) + } + + @Test + fun `new post is marked as known during first start`() { + notificationManager.firstStart() + eventBus.post(NewPostFoundEvent(post)) + assertThat(markedPosts, contains(post)) + } + +} + +private val post: Post = Post.EmptyPost("post") diff --git a/src/test/kotlin/net/pterodactylus/sone/web/notification/MarkPostReplyKnownDuringFirstStartHandlerTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/notification/MarkPostReplyKnownDuringFirstStartHandlerTest.kt new file mode 100644 index 0000000..3cb463f --- /dev/null +++ b/src/test/kotlin/net/pterodactylus/sone/web/notification/MarkPostReplyKnownDuringFirstStartHandlerTest.kt @@ -0,0 +1,50 @@ +/** + * Sone - MarkPostReplyKnownDuringFirstStartHandlerTest.kt - Copyright © 2020 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 . + */ + +package net.pterodactylus.sone.web.notification + +import net.pterodactylus.sone.core.event.* +import net.pterodactylus.sone.data.* +import net.pterodactylus.sone.test.* +import org.hamcrest.MatcherAssert.* +import org.hamcrest.Matchers.* +import java.util.function.* +import kotlin.test.* + +/** + * Unit test for [MarkPostReplyKnownDuringFirstStartHandler]. + */ +class MarkPostReplyKnownDuringFirstStartHandlerTest { + + private val markedAsKnown = mutableListOf() + private val notificationTester = NotificationHandlerTester { MarkPostReplyKnownDuringFirstStartHandler(it, Consumer { markedAsKnown += it }) } + private val postReply = emptyPostReply() + + @Test + fun `post reply is marked as known on new reply during first start`() { + notificationTester.firstStart() + notificationTester.sendEvent(NewPostReplyFoundEvent(postReply)) + assertThat(markedAsKnown, contains(postReply)) + } + + @Test + fun `post reply is not marked as known on new reply if not during first start`() { + notificationTester.sendEvent(NewPostReplyFoundEvent(postReply)) + assertThat(markedAsKnown, not(hasItem(postReply))) + } + +} diff --git a/src/test/kotlin/net/pterodactylus/sone/web/notification/NewRemotePostHandlerTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/notification/NewRemotePostHandlerTest.kt new file mode 100644 index 0000000..0f24aff --- /dev/null +++ b/src/test/kotlin/net/pterodactylus/sone/web/notification/NewRemotePostHandlerTest.kt @@ -0,0 +1,97 @@ +/** + * Sone - NewRemotePostHandlerTest.kt - Copyright © 2019–2020 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 . + */ + +package net.pterodactylus.sone.web.notification + +import net.pterodactylus.sone.core.event.* +import net.pterodactylus.sone.data.* +import net.pterodactylus.sone.data.Post.* +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 kotlin.test.* + +/** + * Unit test for [NewRemotePostHandler]. + */ +@Suppress("UnstableApiUsage") +class NewRemotePostHandlerTest { + + private val notification = ListNotification("", "", Template()) + private val remotePostHandlerTest = NotificationHandlerTester { NewRemotePostHandler(it, notification) } + + @Test + fun `handler adds remote post to new-post notification`() { + remotePostHandlerTest.sendEvent(NewPostFoundEvent(remotePost)) + assertThat(notification.elements, contains(remotePost)) + } + + @Test + fun `handler does not add local post to new-post notification`() { + remotePostHandlerTest.sendEvent(NewPostFoundEvent(localPost)) + assertThat(notification.elements, emptyIterable()) + } + + @Test + fun `handler adds notification for remote post to notification manager`() { + remotePostHandlerTest.sendEvent(NewPostFoundEvent(remotePost)) + assertThat(remotePostHandlerTest.notifications, contains(notification)) + } + + @Test + fun `handler does not add notification for local post to notification manager`() { + remotePostHandlerTest.sendEvent(NewPostFoundEvent(localPost)) + assertThat(remotePostHandlerTest.notifications, emptyIterable()) + } + + @Test + fun `handler does not add notification to notification manager during first start`() { + remotePostHandlerTest.firstStart() + remotePostHandlerTest.sendEvent(NewPostFoundEvent(remotePost)) + assertThat(remotePostHandlerTest.notifications, not(hasItem(notification))) + } + + @Test + fun `handler removes post from notification if post is removed`() { + notification.add(remotePost) + remotePostHandlerTest.sendEvent(PostRemovedEvent(remotePost)) + assertThat(notification.elements, not(hasItem(remotePost))) + } + + @Test + fun `handler removes post from notification if post is marked as known`() { + notification.add(remotePost) + remotePostHandlerTest.sendEvent(MarkPostKnownEvent(remotePost)) + assertThat(notification.elements, not(hasItem(remotePost))) + } + +} + +private val remoteSone: Sone = IdOnlySone("remote-sone") +private val remotePost: Post = object : EmptyPost("remote-post") { + override fun getSone() = remoteSone +} + +private val localSone: Sone = object : IdOnlySone("local-sone") { + override fun isLocal() = true +} +private val localPost: Post = object : EmptyPost("local-post") { + override fun getSone() = localSone +} diff --git a/src/test/kotlin/net/pterodactylus/sone/web/notification/NewSoneHandlerTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/notification/NewSoneHandlerTest.kt new file mode 100644 index 0000000..e099974 --- /dev/null +++ b/src/test/kotlin/net/pterodactylus/sone/web/notification/NewSoneHandlerTest.kt @@ -0,0 +1,78 @@ +/** + * Sone - NewSoneHandlerTest.kt - Copyright © 2019–2020 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 . + */ + +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 kotlin.test.* + +class NewSoneHandlerTest { + + @Suppress("UnstableApiUsage") + private val eventBus = EventBus() + private val notificationManager = NotificationManager() + private val notification = ListNotification("", "", Template()) + private val handler = NewSoneHandler(notificationManager, notification) + + init { + eventBus.register(handler) + } + + @Test + fun `handler adds notification if new sone event is fired`() { + eventBus.post(NewSoneFoundEvent(sone)) + assertThat(notificationManager.notifications, contains(notification)) + } + + @Test + fun `handler adds sone to notification`() { + eventBus.post(NewSoneFoundEvent(sone)) + assertThat(notification.elements, contains(sone)) + } + + @Test + fun `handler does not add notification on new sone event if first-start notification is present`() { + notificationManager.firstStart() + eventBus.post(NewSoneFoundEvent(sone)) + assertThat(notificationManager.notifications, not(contains(notification))) + } + + @Test + fun `handler removes sone from notification if sone is marked as known`() { + notification.add(sone) + eventBus.post(MarkSoneKnownEvent(sone)) + assertThat(notification.elements, emptyIterable()) + } + + @Test + fun `handler removes sone from notification if sone is removed`() { + notification.add(sone) + eventBus.post(SoneRemovedEvent(sone)) + assertThat(notification.elements, emptyIterable()) + } + +} + +private val sone: Sone = IdOnlySone("sone-id") diff --git a/src/test/kotlin/net/pterodactylus/sone/web/notification/NewVersionHandlerTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/notification/NewVersionHandlerTest.kt new file mode 100644 index 0000000..6eb145a --- /dev/null +++ b/src/test/kotlin/net/pterodactylus/sone/web/notification/NewVersionHandlerTest.kt @@ -0,0 +1,69 @@ +/** + * Sone - NewVersionHandlerTest.kt - Copyright © 2019–2020 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 . + */ + +package net.pterodactylus.sone.web.notification + +import com.google.common.eventbus.* +import net.pterodactylus.sone.core.event.* +import net.pterodactylus.util.notify.* +import net.pterodactylus.util.template.* +import net.pterodactylus.util.version.* +import org.hamcrest.MatcherAssert.* +import org.hamcrest.Matchers.* +import kotlin.test.* + +/** + * Unit test for [NewVersionHandler]. + */ +@Suppress("UnstableApiUsage") +class NewVersionHandlerTest { + + private val eventBus = EventBus() + private val notificationManager = NotificationManager() + private val notification = TemplateNotification(Template()) + + init { + eventBus.register(NewVersionHandler(notificationManager, notification)) + eventBus.post(UpdateFoundEvent(Version(1, 2, 3), 1000L, 2000L, true)) + } + + @Test + fun `new-version handler adds notification to manager on new version`() { + assertThat(notificationManager.notifications, contains(notification)) + } + + @Test + fun `handler sets version in notification`() { + assertThat(notification.get("latestVersion"), equalTo(Version(1, 2, 3))) + } + + @Test + fun `handler sets release time in notification`() { + assertThat(notification.get("releaseTime"), equalTo(1000L)) + } + + @Test + fun `handler sets edition in notification`() { + assertThat(notification.get("latestEdition"), equalTo(2000L)) + } + + @Test + fun `handler sets disruptive flag in notification`() { + assertThat(notification.get("disruptive"), equalTo(true)) + } + +} diff --git a/src/test/kotlin/net/pterodactylus/sone/web/notification/NotificationHandlerModuleTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/notification/NotificationHandlerModuleTest.kt new file mode 100644 index 0000000..f22e23a --- /dev/null +++ b/src/test/kotlin/net/pterodactylus/sone/web/notification/NotificationHandlerModuleTest.kt @@ -0,0 +1,611 @@ +/** + * Sone - NotificationHandlerModuleTest.kt - Copyright © 2019–2020 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 . + */ + +package net.pterodactylus.sone.web.notification + +import com.google.inject.* +import com.google.inject.Guice.* +import com.google.inject.name.Names.* +import net.pterodactylus.sone.core.* +import net.pterodactylus.sone.core.event.* +import net.pterodactylus.sone.data.* +import net.pterodactylus.sone.data.Post.* +import net.pterodactylus.sone.data.impl.* +import net.pterodactylus.sone.database.* +import net.pterodactylus.sone.freenet.wot.* +import net.pterodactylus.sone.main.* +import net.pterodactylus.sone.notify.* +import net.pterodactylus.sone.test.* +import net.pterodactylus.sone.text.* +import net.pterodactylus.sone.utils.* +import net.pterodactylus.util.notify.* +import org.hamcrest.MatcherAssert.* +import org.hamcrest.Matchers.* +import org.mockito.* +import org.mockito.Mockito.* +import java.util.concurrent.* +import java.util.concurrent.TimeUnit.* +import java.util.function.* +import kotlin.test.* + +/** + * Unit test for [NotificationHandlerModule]. + */ +class NotificationHandlerModuleTest { + + private val core = mock() + private val webOfTrustConnector = mock() + private val ticker = mock() + private val notificationManager = NotificationManager() + private val loaders = TestLoaders() + private val injector: Injector = createInjector( + Core::class.isProvidedBy(core), + NotificationManager::class.isProvidedBy(notificationManager), + Loaders::class.isProvidedBy(loaders), + WebOfTrustConnector::class.isProvidedBy(webOfTrustConnector), + ScheduledExecutorService::class.withNameIsProvidedBy(ticker, "notification"), + SoneTextParser::class.isProvidedByMock(), + PostReplyProvider::class.isProvidedByMock(), + NotificationHandlerModule() + ) + + @Test + fun `notification handler is created as singleton`() { + injector.verifySingletonInstance() + } + + @Test + fun `mark-post-known-during-first-start handler is created as singleton`() { + injector.verifySingletonInstance() + } + + @Test + fun `mark-post-known-during-first-start handler is created with correct action`() { + notificationManager.firstStart() + val handler = injector.getInstance() + val post = mock() + handler.newPostFound(NewPostFoundEvent(post)) + verify(core).markPostKnown(post) + } + + @Test + fun `mark-post-reply-known-during-first-start handler is created as singleton`() { + injector.verifySingletonInstance() + } + + @Test + fun `mark-post-reply-known-during-first-start handler is created with correct action`() { + notificationManager.firstStart() + val handler = injector.getInstance() + val postReply = mock() + handler.newPostReply(NewPostReplyFoundEvent(postReply)) + verify(core).markReplyKnown(postReply) + } + + @Test + fun `sone-locked-on-startup handler is created as singleton`() { + injector.verifySingletonInstance() + } + + @Test + fun `module can create sone-locked-on-startup notification with correct id`() { + val notification = injector.getInstance>(named("soneLockedOnStartup")) + assertThat(notification.id, equalTo("sone-locked-on-startup")) + } + + @Test + fun `sone-locked-on-startup notification is created as singleton`() { + injector.verifySingletonInstance>(named("soneLockedOnStartup")) + } + + @Test + fun `module can create sone-locked-on-startup notification with correct template and key`() { + loaders.templates += "/templates/notify/soneLockedOnStartupNotification.html" to "<% sones>".asTemplate() + val notification = injector.getInstance>(named("soneLockedOnStartup")) + val sone1 = IdOnlySone("sone1") + val sone2 = IdOnlySone("sone2") + notification.add(sone1) + notification.add(sone2) + assertThat(notification.render(), equalTo(listOf(sone1, sone2).toString())) + } + + @Test + fun `sone-locked-on-startup notification is dismissable`() { + assertThat(injector.getInstance>(named("soneLockedOnStartup")).isDismissable, equalTo(true)) + } + + @Test + fun `new-sone handler is created as singleton`() { + injector.verifySingletonInstance() + } + + @Test + fun `new-sone notification has correct ID`() { + assertThat(injector.getInstance>(named("newSone")).id, equalTo("new-sone-notification")) + } + + @Test + fun `new-sone notification has correct key and template`() { + loaders.templates += "/templates/notify/newSoneNotification.html" to "<% sones>".asTemplate() + val notification = injector.getInstance>(named("newSone")) + val sones = listOf(IdOnlySone("sone1"), IdOnlySone("sone2")) + sones.forEach(notification::add) + assertThat(notification.render(), equalTo(sones.toString())) + } + + @Test + fun `new-sone notification is not dismissable`() { + assertThat(injector.getInstance>(named("newSone")).isDismissable, equalTo(false)) + } + + @Test + fun `new-remote-post handler is created as singleton`() { + injector.verifySingletonInstance() + } + + @Test + fun `new-remote-post notification is created as singleton`() { + injector.verifySingletonInstance>(named("newRemotePost")) + } + + @Test + fun `new-remote-post notification has correct ID`() { + assertThat(injector.getInstance>(named("newRemotePost")).id, equalTo("new-post-notification")) + } + + @Test + fun `new-remote-post notification is not dismissable`() { + assertThat(injector.getInstance>(named("newRemotePost")).isDismissable, equalTo(false)) + } + + @Test + fun `new-remote-post notification has correct key and template`() { + loaders.templates += "/templates/notify/newPostNotification.html" to "<% posts>".asTemplate() + val notification = injector.getInstance>(named("newRemotePost")) + val posts = listOf(EmptyPost("post1"), EmptyPost("post2")) + posts.forEach(notification::add) + assertThat(notification.render(), equalTo(posts.toString())) + } + + @Test + fun `remote-post handler is created as singleton`() { + injector.verifySingletonInstance() + } + + @Test + fun `new-remote-post-reply notification is created as singleton`() { + injector.verifySingletonInstance>(named("newRemotePostReply")) + } + + @Test + fun `new-remote-post-reply notification has correct ID`() { + assertThat(injector.getInstance>(named("newRemotePostReply")).id, equalTo("new-reply-notification")) + } + + @Test + fun `new-remote-post-reply notification is not dismissable`() { + assertThat(injector.getInstance>(named("newRemotePostReply")).isDismissable, equalTo(false)) + } + + @Test + fun `new-remote-post-reply notification has correct key and template`() { + loaders.templates += "/templates/notify/newReplyNotification.html" to "<% replies>".asTemplate() + val notification = injector.getInstance>(named("newRemotePostReply")) + val postReplies = listOf(emptyPostReply(), emptyPostReply()) + postReplies.forEach(notification::add) + assertThat(notification.render(), equalTo(postReplies.toString())) + } + + @Test + fun `sone-locked notification is created as singleton`() { + injector.verifySingletonInstance>(named("soneLocked")) + } + + @Test + fun `sone-locked notification is dismissable`() { + assertThat(injector.getInstance>(named("soneLocked")).isDismissable, equalTo(true)) + } + + @Test + fun `sone-locked notification has correct ID`() { + assertThat(injector.getInstance>(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>(named("soneLocked")) + val sones = listOf(IdOnlySone("sone1"), IdOnlySone("sone2")) + sones.forEach(notification::add) + assertThat(notification.render(), equalTo(sones.toString())) + } + + @Test + fun `sone-locked handler is created as singleton`() { + injector.verifySingletonInstance() + } + + @Test + fun `local-post notification is not dismissable`() { + assertThat(injector.getInstance>(named("localPost")).isDismissable, equalTo(false)) + } + + @Test + fun `local-post notification has correct ID`() { + assertThat(injector.getInstance>(named("localPost")).id, equalTo("local-post-notification")) + } + + @Test + fun `local-post notification has correct key and template`() { + loaders.templates += "/templates/notify/newPostNotification.html" to "<% posts>".asTemplate() + val notification = injector.getInstance>(named("localPost")) + val posts = listOf(EmptyPost("post1"), EmptyPost("post2")) + posts.forEach(notification::add) + assertThat(notification.render(), equalTo(posts.toString())) + } + + @Test + fun `local-post notification is created as singleton`() { + injector.verifySingletonInstance>(named("localPost")) + } + + @Test + fun `local-post handler is created as singleton`() { + injector.verifySingletonInstance() + } + + @Test + fun `local-reply notification is not dismissable`() { + assertThat(injector.getInstance>(named("localReply")).isDismissable, equalTo(false)) + } + + @Test + fun `local-reply notification has correct ID`() { + assertThat(injector.getInstance>(named("localReply")).id, equalTo("local-reply-notification")) + } + + @Test + fun `local-reply notification has correct key and template`() { + loaders.templates += "/templates/notify/newReplyNotification.html" to "<% replies>".asTemplate() + val notification = injector.getInstance>(named("localReply")) + val replies = listOf(emptyPostReply("reply1"), emptyPostReply("reply2")) + replies.forEach(notification::add) + assertThat(notification.render(), equalTo(replies.toString())) + } + + @Test + fun `local-reply notification is created as singleton`() { + injector.verifySingletonInstance>(named("localReply")) + } + + @Test + fun `local-reply handler is created as singleton`() { + injector.verifySingletonInstance() + } + + @Test + fun `new-version notification is created as singleton`() { + injector.verifySingletonInstance(named("newVersion")) + } + + @Test + fun `new-version notification has correct ID`() { + assertThat(injector.getInstance(named("newVersion")).id, equalTo("new-version-notification")) + } + + @Test + fun `new-version notification is dismissable`() { + assertThat(injector.getInstance(named("newVersion")).isDismissable, equalTo(true)) + } + + @Test + fun `new-version notification loads correct template`() { + loaders.templates += "/templates/notify/newVersionNotification.html" to "1".asTemplate() + val notification = injector.getInstance(named("newVersion")) + assertThat(notification.render(), equalTo("1")) + } + + @Test + fun `new-version handler is created as singleton`() { + injector.verifySingletonInstance() + } + + @Test + fun `inserting-image notification is created as singleton`() { + injector.verifySingletonInstance>(named("imageInserting")) + } + + @Test + fun `inserting-image notification has correct ID`() { + assertThat(injector.getInstance>(named("imageInserting")).id, equalTo("inserting-images-notification")) + } + + @Test + fun `inserting-image notification is dismissable`() { + assertThat(injector.getInstance>(named("imageInserting")).isDismissable, equalTo(true)) + } + + @Test + fun `inserting-image notification loads correct template`() { + loaders.templates += "/templates/notify/inserting-images-notification.html" to "<% images>".asTemplate() + val notification = injector.getInstance>(named("imageInserting")) + val images = listOf(ImageImpl(), ImageImpl()).onEach(notification::add) + assertThat(notification.render(), equalTo(images.toString())) + } + + @Test + fun `inserting-image-failed notification is created as singleton`() { + injector.verifySingletonInstance>(named("imageFailed")) + } + + @Test + fun `inserting-image-failed notification has correct ID`() { + assertThat(injector.getInstance>(named("imageFailed")).id, equalTo("image-insert-failed-notification")) + } + + @Test + fun `inserting-image-failed notification is dismissable`() { + assertThat(injector.getInstance>(named("imageFailed")).isDismissable, equalTo(true)) + } + + @Test + fun `inserting-image-failed notification loads correct template`() { + loaders.templates += "/templates/notify/image-insert-failed-notification.html" to "<% images>".asTemplate() + val notification = injector.getInstance>(named("imageFailed")) + val images = listOf(ImageImpl(), ImageImpl()).onEach(notification::add) + assertThat(notification.render(), equalTo(images.toString())) + } + + @Test + fun `inserted-image notification is created as singleton`() { + injector.verifySingletonInstance>(named("imageInserted")) + } + + @Test + fun `inserted-image notification has correct ID`() { + assertThat(injector.getInstance>(named("imageInserted")).id, equalTo("inserted-images-notification")) + } + + @Test + fun `inserted-image notification is dismissable`() { + assertThat(injector.getInstance>(named("imageInserted")).isDismissable, equalTo(true)) + } + + @Test + fun `inserted-image notification loads correct template`() { + loaders.templates += "/templates/notify/inserted-images-notification.html" to "<% images>".asTemplate() + val notification = injector.getInstance>(named("imageInserted")) + val images = listOf(ImageImpl(), ImageImpl()).onEach(notification::add) + assertThat(notification.render(), equalTo(images.toString())) + } + + @Test + fun `image insert handler is created as singleton`() { + injector.verifySingletonInstance() + } + + @Test + fun `first-start notification is created as singleton`() { + injector.verifySingletonInstance(named("firstStart")) + } + + @Test + fun `first-start notification has correct ID`() { + assertThat(injector.getInstance(named("firstStart")).id, equalTo("first-start-notification")) + } + + @Test + fun `first-start notification is dismissable`() { + assertThat(injector.getInstance(named("firstStart")).isDismissable, equalTo(true)) + } + + @Test + fun `first-start notification loads correct template`() { + loaders.templates += "/templates/notify/firstStartNotification.html" to "1".asTemplate() + val notification = injector.getInstance(named("firstStart")) + assertThat(notification.render(), equalTo("1")) + } + + @Test + fun `first-start handler is created as singleton`() { + injector.verifySingletonInstance() + } + + @Test + fun `config-not-read notification is created as singleton`() { + injector.verifySingletonInstance(named("configNotRead")) + } + + @Test + fun `config-not-read notification has correct ID `() { + assertThat(injector.getInstance(named("configNotRead")).id, equalTo("config-not-read-notification")) + } + + @Test + fun `config-not-read notification is dismissable`() { + assertThat(injector.getInstance(named("configNotRead")).isDismissable, equalTo(true)) + } + + @Test + fun `config-not-read notification loads correct template`() { + loaders.templates += "/templates/notify/configNotReadNotification.html" to "1".asTemplate() + val notification = injector.getInstance(named("configNotRead")) + assertThat(notification.render(), equalTo("1")) + } + + @Test + fun `config-not-read handler is created as singleton`() { + injector.verifySingletonInstance() + } + + @Test + fun `startup notification can be created`() { + injector.verifySingletonInstance(named("startup")) + } + + @Test + fun `startup notification has correct ID`() { + assertThat(injector.getInstance(named("startup")).id, equalTo("startup-notification")) + } + + @Test + fun `startup notification is dismissable`() { + assertThat(injector.getInstance(named("startup")).isDismissable, equalTo(true)) + } + + @Test + fun `startup notification loads correct template`() { + loaders.templates += "/templates/notify/startupNotification.html" to "1".asTemplate() + val notification = injector.getInstance(named("startup")) + assertThat(notification.render(), equalTo("1")) + } + + @Test + fun `startup handler is created as singleton`() { + injector.verifySingletonInstance() + } + + @Test + fun `web-of-trust notification is created as singleton`() { + injector.verifySingletonInstance(named("webOfTrust")) + } + + @Test + fun `web-of-trust notification has correct ID`() { + assertThat(injector.getInstance(named("webOfTrust")).id, equalTo("wot-missing-notification")) + } + + @Test + fun `web-of-trust notification is dismissable`() { + assertThat(injector.getInstance(named("webOfTrust")).isDismissable, equalTo(true)) + } + + @Test + fun `web-of-trust notification loads correct template`() { + loaders.templates += "/templates/notify/wotMissingNotification.html" to "1".asTemplate() + val notification = injector.getInstance(named("webOfTrust")) + assertThat(notification.render(), equalTo("1")) + } + + @Test + fun `web-of-trust handler is created as singleton`() { + injector.verifySingletonInstance(named("webOfTrust")) + } + + @Test + fun `web-of-trust reacher is created as singleton`() { + injector.verifySingletonInstance(named("webOfTrustReacher")) + } + + @Test + fun `web-of-trust reacher access the wot connector`() { + injector.getInstance(named("webOfTrustReacher")).run() + verify(webOfTrustConnector).ping() + } + + @Test + fun `web-of-trust reschedule is created as singleton`() { + injector.verifySingletonInstance>(named("webOfTrustReschedule")) + } + + @Test + fun `web-of-trust reschedule schedules at the correct delay`() { + val webOfTrustPinger = injector.getInstance() + injector.getInstance>(named("webOfTrustReschedule"))(webOfTrustPinger) + verify(ticker).schedule(ArgumentMatchers.eq(webOfTrustPinger), ArgumentMatchers.eq(15L), ArgumentMatchers.eq(SECONDS)) + } + + @Test + fun `sone mention detector is created as singleton`() { + assertThat(injector.getInstance(), notNullValue()) + } + + @Test + fun `sone-mentioned notification is created as singleton`() { + injector.verifySingletonInstance>(named("soneMentioned")) + } + + @Test + fun `sone-mentioned notification has correct ID`() { + assertThat(injector.getInstance>(named("soneMentioned")).id, equalTo("mention-notification")) + } + + @Test + fun `sone-mentioned notification is not dismissable`() { + assertThat(injector.getInstance>(named("soneMentioned")).isDismissable, equalTo(false)) + } + + @Test + fun `sone-mentioned notification loads correct template`() { + loaders.templates += "/templates/notify/mentionNotification.html" to "<% posts>".asTemplate() + val notification = injector.getInstance>(named("soneMentioned")) + val posts = listOf(EmptyPost("1"), EmptyPost("2")).onEach(notification::add) + assertThat(notification.render(), equalTo(posts.toString())) + } + + @Test + fun `sone-mentioned handler is created as singleton`() { + injector.verifySingletonInstance() + } + + @Test + fun `sone insert notification supplier is created as singleton`() { + injector.verifySingletonInstance() + } + + @Test + fun `sone insert notification template is loaded correctly`() { + loaders.templates += "/templates/notify/soneInsertNotification.html" to "foo".asTemplate() + injector.getInstance() + .invoke(createRemoteSone()) + .render() + .let { assertThat(it, equalTo("foo")) } + } + + @Test + fun `sone notification supplier returns different notifications for different sones`() { + val supplier = injector.getInstance() + listOf(createRemoteSone(), createRemoteSone(), createRemoteSone()) + .map(supplier) + .distinct() + .let { assertThat(it, hasSize(3)) } + } + + @Test + fun `sone notification supplier caches notifications for a sone`() { + val supplier = injector.getInstance() + val sone = createRemoteSone() + listOf(sone, sone, sone) + .map(supplier) + .distinct() + .let { assertThat(it, hasSize(1)) } + } + + @Test + fun `sone notification supplier sets sone in notification template`() { + val supplier = injector.getInstance() + val sone = createRemoteSone() + val templateNotification = supplier(sone) + assertThat(templateNotification["insertSone"], sameInstance(sone)) + } + + @Test + fun `sone insert handler is created as singleton`() { + injector.verifySingletonInstance() + } + +} diff --git a/src/test/kotlin/net/pterodactylus/sone/web/notification/NotificationHandlerTester.kt b/src/test/kotlin/net/pterodactylus/sone/web/notification/NotificationHandlerTester.kt new file mode 100644 index 0000000..6a06ef8 --- /dev/null +++ b/src/test/kotlin/net/pterodactylus/sone/web/notification/NotificationHandlerTester.kt @@ -0,0 +1,58 @@ +/** + * Sone - NotificationHandlerTester.kt - Copyright © 2019–2020 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 . + */ + +package net.pterodactylus.sone.web.notification + +import com.google.common.eventbus.* +import net.pterodactylus.util.notify.* + +/** + * Helper for testing event handlers that deal with notifications. It contains + * a notification manager and an [event bus][EventBus] and automatically + * registers the created handler on the event bus. + * + * ``` + * val notification = SomeNotification() + * val notificationTester = NotificationTester { SomeHandler(it, notification) } + * + * fun test() { + * notificationTester.sendEvent(SomeEvent()) + * assertThat(notificationTester.elements, hasItem(notification)) + * } + * ``` + */ +@Suppress("UnstableApiUsage") +class NotificationHandlerTester(createHandler: (NotificationManager) -> Any) { + + private val eventBus = EventBus() + private val notificationManager = NotificationManager() + + /** Returns all notifications of the notification manager. */ + val notifications: Set + get() = notificationManager.notifications + + init { + eventBus.register(createHandler(notificationManager)) + } + + /** Sends an event to the event bus. */ + fun sendEvent(event: Any) = eventBus.post(event) + + /** Sets the first-start notification on the notification manager. */ + fun firstStart() = notificationManager.firstStart() + +} diff --git a/src/test/kotlin/net/pterodactylus/sone/web/notification/RemotePostReplyHandlerTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/notification/RemotePostReplyHandlerTest.kt new file mode 100644 index 0000000..e4eb14f --- /dev/null +++ b/src/test/kotlin/net/pterodactylus/sone/web/notification/RemotePostReplyHandlerTest.kt @@ -0,0 +1,93 @@ +/** + * Sone - RemotePostReplyHandlerTest.kt - Copyright © 2020 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 . + */ + +package net.pterodactylus.sone.web.notification + +import net.pterodactylus.sone.core.event.* +import net.pterodactylus.sone.data.* +import net.pterodactylus.sone.notify.* +import net.pterodactylus.sone.test.* +import net.pterodactylus.util.notify.* +import net.pterodactylus.util.template.* +import org.hamcrest.MatcherAssert.* +import org.hamcrest.Matchers.* +import kotlin.test.* + +/** + * Unit test for [RemotePostReplyHandler]. + */ +class RemotePostReplyHandlerTest { + + private val notification = ListNotification("", "", Template()) + private val notificationHandlerTester = NotificationHandlerTester { RemotePostReplyHandler(it, notification) } + private val postReply = emptyPostReply() + + @Test + fun `reply is added to notification on new reply`() { + notificationHandlerTester.sendEvent(NewPostReplyFoundEvent(postReply)) + assertThat(notification.elements, hasItem(postReply)) + } + + @Test + fun `notification is added to manager on new reply`() { + notificationHandlerTester.sendEvent(NewPostReplyFoundEvent(postReply)) + assertThat(notificationHandlerTester.notifications, hasItem(notification)) + } + + @Test + fun `reply is not added to notification on new reply during first start`() { + notificationHandlerTester.firstStart() + notificationHandlerTester.sendEvent(NewPostReplyFoundEvent(postReply)) + assertThat(notification.elements, not(hasItem(postReply))) + } + + @Test + fun `notification is not added to manager on new reply during first start`() { + notificationHandlerTester.firstStart() + notificationHandlerTester.sendEvent(NewPostReplyFoundEvent(postReply)) + assertThat(notificationHandlerTester.notifications, not(hasItem(notification))) + } + + @Test + fun `reply is not added to notification on new local reply`() { + val postReply = emptyPostReply(sone = localSone1) + notificationHandlerTester.sendEvent(NewPostReplyFoundEvent(postReply)) + assertThat(notification.elements, not(hasItem(postReply))) + } + + @Test + fun `notification is not added to manager on new local reply`() { + val postReply = emptyPostReply(sone = localSone1) + notificationHandlerTester.sendEvent(NewPostReplyFoundEvent(postReply)) + assertThat(notificationHandlerTester.notifications, not(hasItem(notification))) + } + + @Test + fun `reply is removed from notification when removed`() { + notification.add(postReply) + notificationHandlerTester.sendEvent(PostReplyRemovedEvent(postReply)) + assertThat(notification.elements, not(hasItem(postReply))) + } + + @Test + fun `reply is removed from notification when marked as known`() { + notification.add(postReply) + notificationHandlerTester.sendEvent(MarkPostReplyKnownEvent(postReply)) + assertThat(notification.elements, not(hasItem(postReply))) + } + +} diff --git a/src/test/kotlin/net/pterodactylus/sone/web/notification/SoneInsertHandlerTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/notification/SoneInsertHandlerTest.kt new file mode 100644 index 0000000..6a11a90 --- /dev/null +++ b/src/test/kotlin/net/pterodactylus/sone/web/notification/SoneInsertHandlerTest.kt @@ -0,0 +1,112 @@ +/** + * Sone - SoneInsertHandlerTest.kt - Copyright © 2020 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 . + */ + +package net.pterodactylus.sone.web.notification + +import net.pterodactylus.sone.core.event.* +import net.pterodactylus.sone.test.* +import net.pterodactylus.util.notify.* +import net.pterodactylus.util.template.* +import org.hamcrest.MatcherAssert.* +import org.hamcrest.Matchers.* +import kotlin.test.* + +/** + * Unit test for [SoneInsertHandler]. + */ +class SoneInsertHandlerTest { + + private val localSone = createLocalSone() + private val notification1 = TemplateNotification(Template()) + private val notification2 = TemplateNotification(Template()) + private val soneInsertHandlerTester = NotificationHandlerTester { + SoneInsertHandler(it) { sone -> + if (sone == localSone) notification1 else notification2 + } + } + + @Test + fun `handler adds notification to manager when sone insert starts`() { + localSone.options.isSoneInsertNotificationEnabled = true + soneInsertHandlerTester.sendEvent(SoneInsertingEvent(localSone)) + assertThat(soneInsertHandlerTester.notifications, hasItem(notification1)) + } + + @Test + fun `handler sets sone status in notification when sone insert starts`() { + localSone.options.isSoneInsertNotificationEnabled = true + soneInsertHandlerTester.sendEvent(SoneInsertingEvent(localSone)) + assertThat(notification1.get("soneStatus"), equalTo("inserting")) + } + + @Test + fun `handler does not add notification to manager if option is disabled`() { + localSone.options.isSoneInsertNotificationEnabled = false + soneInsertHandlerTester.sendEvent(SoneInsertingEvent(localSone)) + assertThat(soneInsertHandlerTester.notifications, not(hasItem(notification1))) + } + + @Test + fun `handler adds notification to manager when sone insert finishes`() { + localSone.options.isSoneInsertNotificationEnabled = true + soneInsertHandlerTester.sendEvent(SoneInsertedEvent(localSone, 123456, "")) + assertThat(soneInsertHandlerTester.notifications, hasItem(notification1)) + } + + @Test + fun `handler sets sone status in notification when sone insert finishes`() { + localSone.options.isSoneInsertNotificationEnabled = true + soneInsertHandlerTester.sendEvent(SoneInsertedEvent(localSone, 123456, "")) + assertThat(notification1.get("soneStatus"), equalTo("inserted")) + } + + @Test + fun `handler sets insert duration in notification when sone insert finishes`() { + localSone.options.isSoneInsertNotificationEnabled = true + soneInsertHandlerTester.sendEvent(SoneInsertedEvent(localSone, 123456, "")) + assertThat(notification1.get("insertDuration"), equalTo(123L)) + } + + @Test + fun `handler does not add notification for finished insert to manager if option is disabled`() { + localSone.options.isSoneInsertNotificationEnabled = false + soneInsertHandlerTester.sendEvent(SoneInsertedEvent(localSone, 123456, "")) + assertThat(soneInsertHandlerTester.notifications, not(hasItem(notification1))) + } + + @Test + fun `handler adds notification to manager when sone insert aborts`() { + localSone.options.isSoneInsertNotificationEnabled = true + soneInsertHandlerTester.sendEvent(SoneInsertAbortedEvent(localSone, Exception())) + assertThat(soneInsertHandlerTester.notifications, hasItem(notification1)) + } + + @Test + fun `handler sets sone status in notification when sone insert aborts`() { + localSone.options.isSoneInsertNotificationEnabled = true + soneInsertHandlerTester.sendEvent(SoneInsertAbortedEvent(localSone, Exception())) + assertThat(notification1.get("soneStatus"), equalTo("insert-aborted")) + } + + @Test + fun `handler does not add notification for aborted insert to manager if option is disabled`() { + localSone.options.isSoneInsertNotificationEnabled = false + soneInsertHandlerTester.sendEvent(SoneInsertAbortedEvent(localSone, Exception())) + assertThat(soneInsertHandlerTester.notifications, not(hasItem(notification1))) + } + +} 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 index 0000000..4003dbb --- /dev/null +++ b/src/test/kotlin/net/pterodactylus/sone/web/notification/SoneLockedHandlerTest.kt @@ -0,0 +1,116 @@ +/** + * Sone - SoneLockedHandlerTest.kt - Copyright © 2019–2020 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 . + */ + +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.sone.utils.* +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("", "", Template()) + private val executor = TestScheduledThreadPoolExecutor() + + init { + SoneLockedHandler(notificationManager, notification, executor).also(eventBus::register) + } + + @AfterTest + fun shutdownExecutor() = executor.shutdown() + + @Test + fun `notification is not added before the command is run`() { + eventBus.post(SoneLockedEvent(sone)) + assertThat(notificationManager.notifications, emptyIterable()) + } + + @Test + fun `sone is added to notification immediately`() { + eventBus.post(SoneLockedEvent(sone)) + assertThat(notification.elements, contains(sone)) + } + + @Test + fun `notification is added to notification manager from command`() { + eventBus.post(SoneLockedEvent(sone)) + executor.scheduleds.single().command() + assertThat(notificationManager.notifications, contains(notification)) + } + + @Test + fun `command is registered with a delay of five minutes`() { + eventBus.post(SoneLockedEvent(sone)) + with(executor.scheduleds.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.scheduleds.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.scheduleds.single().command() + 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.scheduleds.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.scheduleds[1], notNullValue()) + } + +} + +private val sone: Sone = IdOnlySone("sone") diff --git a/src/test/kotlin/net/pterodactylus/sone/web/notification/SoneLockedOnStartupHandlerTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/notification/SoneLockedOnStartupHandlerTest.kt new file mode 100644 index 0000000..5ffac88 --- /dev/null +++ b/src/test/kotlin/net/pterodactylus/sone/web/notification/SoneLockedOnStartupHandlerTest.kt @@ -0,0 +1,59 @@ +/** + * Sone - SoneLockedOnStartupHandlerTest.kt - Copyright © 2019–2020 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 . + */ + +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 kotlin.test.* + +/** + * Unit test for [SoneLockedOnStartupHandler]. + */ +class SoneLockedOnStartupHandlerTest { + + @Suppress("UnstableApiUsage") + private val eventBus = EventBus() + private val manager = NotificationManager() + private val notification = ListNotification("", "", Template()) + + init { + SoneLockedOnStartupHandler(manager, notification).also(eventBus::register) + } + + @Test + fun `handler adds sone to notification when event is posted`() { + eventBus.post(SoneLockedOnStartup(sone)) + assertThat(notification.elements, contains(sone)) + } + + @Test + fun `handler adds notification to manager`() { + eventBus.post(SoneLockedOnStartup(sone)) + assertThat(manager.notifications, contains(notification)) + } + +} + +private val sone = IdOnlySone("sone-id") diff --git a/src/test/kotlin/net/pterodactylus/sone/web/notification/SoneMentionedHandlerTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/notification/SoneMentionedHandlerTest.kt new file mode 100644 index 0000000..4e91aa5 --- /dev/null +++ b/src/test/kotlin/net/pterodactylus/sone/web/notification/SoneMentionedHandlerTest.kt @@ -0,0 +1,87 @@ +/** + * Sone - SoneMentionedHandlerTest.kt - Copyright © 2020 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 . + */ + +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.Post.* +import net.pterodactylus.sone.notify.* +import net.pterodactylus.util.notify.* +import net.pterodactylus.util.template.* +import org.hamcrest.MatcherAssert.* +import org.hamcrest.Matchers.* +import kotlin.test.* + +/** + * Unit test for [SoneMentionedHandler]. + */ +@Suppress("UnstableApiUsage") +class SoneMentionedHandlerTest { + + private val notificationManager = NotificationManager() + private val notification = ListNotification("", "", Template()) + private val eventBus = EventBus() + + init { + eventBus.register(SoneMentionedHandler(notificationManager, notification)) + } + + @Test + fun `handler adds notification to manager on event`() { + eventBus.post(MentionOfLocalSoneFoundEvent(post)) + assertThat(notificationManager.notifications, contains(notification)) + } + + @Test + fun `handler adds post to notification on event`() { + eventBus.post(MentionOfLocalSoneFoundEvent(post)) + assertThat(notification.elements, contains(post)) + } + + @Test + fun `handler does not add notification during first start`() { + notificationManager.firstStart() + eventBus.post(MentionOfLocalSoneFoundEvent(post)) + assertThat(notificationManager.notifications, not(hasItem(notification))) + } + + @Test + fun `handler does not add post to notification during first start`() { + notificationManager.firstStart() + eventBus.post(MentionOfLocalSoneFoundEvent(post)) + assertThat(notification.elements, not(hasItem(post))) + } + + @Test + fun `handler removes post from notification`() { + notification.add(post) + eventBus.post(MentionOfLocalSoneRemovedEvent(post)) + assertThat(notification.elements, not(hasItem(post))) + } + + @Test + fun `handler removes notification from manager`() { + notificationManager.addNotification(notification) + eventBus.post(MentionOfLocalSoneRemovedEvent(post)) + assertThat(notificationManager.notifications, not(hasItem(notification))) + } + +} + +private val post = EmptyPost("") diff --git a/src/test/kotlin/net/pterodactylus/sone/web/notification/StartupHandlerTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/notification/StartupHandlerTest.kt new file mode 100644 index 0000000..aaeea90 --- /dev/null +++ b/src/test/kotlin/net/pterodactylus/sone/web/notification/StartupHandlerTest.kt @@ -0,0 +1,66 @@ +/** + * Sone - StartupHandlerTest.kt - Copyright © 2019–2020 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 . + */ + +package net.pterodactylus.sone.web.notification + +import com.google.common.eventbus.* +import net.pterodactylus.sone.core.event.* +import net.pterodactylus.sone.utils.* +import net.pterodactylus.util.notify.* +import net.pterodactylus.util.template.* +import org.hamcrest.MatcherAssert.* +import org.hamcrest.Matchers.* +import java.util.concurrent.TimeUnit.* +import kotlin.test.* + +/** + * Unit test for [StartupHandler]. + */ +class StartupHandlerTest { + + private val eventBus = EventBus() + private val notificationManager = NotificationManager() + private val notification = TemplateNotification("", Template()) + private val executor = TestScheduledThreadPoolExecutor() + + init { + eventBus.register(StartupHandler(notificationManager, notification, executor)) + } + + @AfterTest + fun shutdownExecutor() = executor.shutdown() + + @Test + fun `handler adds notification to manager on startup`() { + eventBus.post(Startup()) + assertThat(notificationManager.notifications, contains(notification)) + } + + @Test + fun `handler registers command on with 2-minute delay`() { + eventBus.post(Startup()) + assertThat(with(executor.scheduleds.single()) { timeUnit.toNanos(delay) }, equalTo(MINUTES.toNanos(2))) + } + + @Test + fun `registered command removes notification from manager`() { + eventBus.post(Startup()) + executor.scheduleds.single().command() + assertThat(notificationManager.notifications, emptyIterable()) + } + +} diff --git a/src/test/kotlin/net/pterodactylus/sone/web/notification/Testing.kt b/src/test/kotlin/net/pterodactylus/sone/web/notification/Testing.kt new file mode 100644 index 0000000..68bb103 --- /dev/null +++ b/src/test/kotlin/net/pterodactylus/sone/web/notification/Testing.kt @@ -0,0 +1,45 @@ +/** + * Sone - Testing.kt - Copyright © 2019–2020 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 . + */ + +package net.pterodactylus.sone.web.notification + +import net.pterodactylus.util.notify.* +import java.io.* +import java.util.concurrent.* + +/** Information about a scheduled runnable. */ +data class Scheduled(val command: Runnable, val delay: Long, val timeUnit: TimeUnit, val future: ScheduledFuture<*>) + +/** + * [ScheduledThreadPoolExecutor] extension that stores parameters and return + * values for the [ScheduledThreadPoolExecutor.schedule] method. + */ +class TestScheduledThreadPoolExecutor : ScheduledThreadPoolExecutor(1) { + + val scheduleds = mutableListOf() + + override fun schedule(command: Runnable, delay: Long, unit: TimeUnit): ScheduledFuture<*> = + super.schedule(command, delay, unit) + .also { scheduleds += Scheduled(command, delay, unit, it) } + +} + +fun NotificationManager.firstStart() { + addNotification(object : AbstractNotification("first-start-notification") { + override fun render(writer: Writer?) = Unit + }) +} diff --git a/src/test/kotlin/net/pterodactylus/sone/web/notification/WebOfTrustHandlerTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/notification/WebOfTrustHandlerTest.kt new file mode 100644 index 0000000..8981a47 --- /dev/null +++ b/src/test/kotlin/net/pterodactylus/sone/web/notification/WebOfTrustHandlerTest.kt @@ -0,0 +1,54 @@ +/** + * Sone - WebOfTrustHandlerTest.kt - Copyright © 2019–2020 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 . + */ + +package net.pterodactylus.sone.web.notification + +import com.google.common.eventbus.* +import net.pterodactylus.sone.core.event.* +import net.pterodactylus.util.notify.* +import net.pterodactylus.util.template.* +import org.hamcrest.MatcherAssert.* +import org.hamcrest.Matchers.* +import kotlin.test.* + +/** + * Unit test for [WebOfTrustHandler]. + */ +class WebOfTrustHandlerTest { + + private val eventBus = EventBus() + private val notificationManager = NotificationManager() + private val notification = TemplateNotification("", Template()) + + init { + eventBus.register(WebOfTrustHandler(notificationManager, notification)) + } + + @Test + fun `handler adds notification if wot goes down`() { + eventBus.post(WebOfTrustDisappeared()) + assertThat(notificationManager.notifications, contains(notification)) + } + + @Test + fun `handler removes notification if wot appears`() { + notificationManager.addNotification(notification) + eventBus.post(WebOfTrustAppeared()) + assertThat(notificationManager.notifications, emptyIterable()) + } + +} diff --git a/src/test/kotlin/net/pterodactylus/sone/web/page/FreenetRequestTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/page/FreenetRequestTest.kt index e05ad45..3295bdd 100644 --- a/src/test/kotlin/net/pterodactylus/sone/web/page/FreenetRequestTest.kt +++ b/src/test/kotlin/net/pterodactylus/sone/web/page/FreenetRequestTest.kt @@ -2,7 +2,6 @@ package net.pterodactylus.sone.web.page import freenet.clients.http.* import freenet.clients.http.SessionManager.* -import freenet.l10n.* import freenet.support.api.* import net.pterodactylus.sone.test.* import net.pterodactylus.util.web.* @@ -19,9 +18,8 @@ class FreenetRequestTest { private val method = Method.GET private val httpRequest = mock(HTTPRequest::class.java) private val toadletContext = mock(ToadletContext::class.java) - private val l10n = mock() private val sessionManager = mock() - private val request = FreenetRequest(uri, method, httpRequest, toadletContext, l10n, sessionManager) + private val request = FreenetRequest(uri, method, httpRequest, toadletContext, sessionManager) @Test fun `uri is retained correctly`() { @@ -44,11 +42,6 @@ class FreenetRequestTest { } @Test - fun `l10n is retained correctly`() { - assertThat(request.l10n, equalTo(l10n)) - } - - @Test fun `null is returned if no session exists`() { assertThat(request.existingSession, nullValue()) } diff --git a/src/test/kotlin/net/pterodactylus/sone/web/page/SoneRequestTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/page/SoneRequestTest.kt index 50c6ce7..5be6ff1 100644 --- a/src/test/kotlin/net/pterodactylus/sone/web/page/SoneRequestTest.kt +++ b/src/test/kotlin/net/pterodactylus/sone/web/page/SoneRequestTest.kt @@ -19,11 +19,10 @@ class SoneRequestTest { private val method = Method.GET private val httpRequest = Mockito.mock(HTTPRequest::class.java) private val toadletContext = Mockito.mock(ToadletContext::class.java) - private val l10n = mock() private val sessionManager = mock() private val core = mock() private val webInterface = mock() - private val soneRequest = SoneRequest(uri, method, httpRequest, toadletContext, l10n, sessionManager, core, webInterface) + private val soneRequest = SoneRequest(uri, method, httpRequest, toadletContext, sessionManager, core, webInterface) @Test fun `freenet request properties are retained correctly`() { @@ -31,7 +30,6 @@ class SoneRequestTest { assertThat(soneRequest.method, equalTo(method)) assertThat(soneRequest.httpRequest, equalTo(httpRequest)) assertThat(soneRequest.toadletContext, equalTo(toadletContext)) - assertThat(soneRequest.l10n, equalTo(l10n)) assertThat(soneRequest.sessionManager, equalTo(sessionManager)) } @@ -47,13 +45,12 @@ class SoneRequestTest { @Test fun `freenet request is wrapped correctly`() { - val freenetRequest = FreenetRequest(uri, method, httpRequest, toadletContext, l10n, sessionManager) + val freenetRequest = FreenetRequest(uri, method, httpRequest, toadletContext, sessionManager) val wrappedSoneRequest = freenetRequest.toSoneRequest(core, webInterface) assertThat(wrappedSoneRequest.uri, equalTo(uri)) assertThat(wrappedSoneRequest.method, equalTo(method)) assertThat(wrappedSoneRequest.httpRequest, equalTo(httpRequest)) assertThat(wrappedSoneRequest.toadletContext, equalTo(toadletContext)) - assertThat(wrappedSoneRequest.l10n, equalTo(l10n)) assertThat(wrappedSoneRequest.sessionManager, equalTo(sessionManager)) assertThat(wrappedSoneRequest.core, sameInstance(core)) assertThat(wrappedSoneRequest.webInterface, sameInstance(webInterface)) diff --git a/src/test/kotlin/net/pterodactylus/sone/web/pages/CreatePostPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/pages/CreatePostPageTest.kt index 2bf0c05..6212c90 100644 --- a/src/test/kotlin/net/pterodactylus/sone/web/pages/CreatePostPageTest.kt +++ b/src/test/kotlin/net/pterodactylus/sone/web/pages/CreatePostPageTest.kt @@ -40,7 +40,7 @@ class CreatePostPageTest : WebPageTest(::CreatePostPage) { addHttpRequestPart("returnPage", "return.html") addHttpRequestPart("text", "post text") verifyRedirect("return.html") { - verify(core).createPost(currentSone, absent(), "post text") + verify(core).createPost(currentSone, null, "post text") } } @@ -62,7 +62,7 @@ class CreatePostPageTest : WebPageTest(::CreatePostPage) { val sender = mock() addLocalSone("sender-id", sender) verifyRedirect("return.html") { - verify(core).createPost(sender, absent(), "post text") + verify(core).createPost(sender, null, "post text") } } @@ -75,7 +75,7 @@ class CreatePostPageTest : WebPageTest(::CreatePostPage) { val recipient = mock() addSone("recipient-id", recipient) verifyRedirect("return.html") { - verify(core).createPost(currentSone, recipient.asOptional(), "post text") + verify(core).createPost(currentSone, recipient, "post text") } } @@ -86,7 +86,7 @@ class CreatePostPageTest : WebPageTest(::CreatePostPage) { addHttpRequestPart("text", "post http://localhost:12345/KSK@foo text") addHttpRequestHeader("Host", "localhost:12345") verifyRedirect("return.html") { - verify(core).createPost(currentSone, absent(), "post KSK@foo text") + verify(core).createPost(currentSone, null, "post KSK@foo text") } } diff --git a/src/test/kotlin/net/pterodactylus/sone/web/pages/DebugPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/pages/DebugPageTest.kt new file mode 100644 index 0000000..2424b2d --- /dev/null +++ b/src/test/kotlin/net/pterodactylus/sone/web/pages/DebugPageTest.kt @@ -0,0 +1,69 @@ +/** + * Sone - DebugPageTest.kt - Copyright © 2019–2020 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 . + */ + +package net.pterodactylus.sone.web.pages + +import net.pterodactylus.sone.test.* +import net.pterodactylus.sone.web.* +import net.pterodactylus.sone.web.WebTestUtils.* +import net.pterodactylus.sone.web.page.FreenetTemplatePage.* +import org.hamcrest.MatcherAssert.* +import org.hamcrest.Matchers.* +import org.junit.Rule +import org.junit.rules.* +import org.junit.rules.ExpectedException.* +import org.mockito.Mockito.* +import kotlin.test.* + +class DebugPageTest : WebPageTest(::DebugPage) { + + @Rule + @JvmField + val expectedException: ExpectedException = none() + + @Test + fun `page returns correct path`() { + assertThat(page.path, equalTo("debug")) + } + + @Test + fun `page does not require login`() { + assertThat(page.requiresLogin(), equalTo(false)) + } + + @Test + fun `page can be created by dependency injection`() { + assertThat(baseInjector.getInstance(), notNullValue()) + } + + @Test + fun `get request activates debug mode`() { + try { + page.handleRequest(soneRequest, templateContext) + } catch (_: RedirectException) { + } + verify(core).setDebug() + } + + @Test + fun `get request redirects to index`() { + expectedException.expect(redirectsTo("./")) + page.handleRequest(soneRequest, templateContext) + } + +} + diff --git a/src/test/kotlin/net/pterodactylus/sone/web/pages/DeleteSonePageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/pages/DeleteSonePageTest.kt index 21cfdfc..5bcdd6a 100644 --- a/src/test/kotlin/net/pterodactylus/sone/web/pages/DeleteSonePageTest.kt +++ b/src/test/kotlin/net/pterodactylus/sone/web/pages/DeleteSonePageTest.kt @@ -26,7 +26,7 @@ class DeleteSonePageTest : WebPageTest(::DeleteSonePage) { @Test fun `page returns correct title`() { - whenever(l10n.getString("Page.DeleteSone.Title")).thenReturn("delete sone page") + addTranslation("Page.DeleteSone.Title", "delete sone page") assertThat(page.getPageTitle(soneRequest), equalTo("delete sone page")) } diff --git a/src/test/kotlin/net/pterodactylus/sone/web/pages/DismissNotificationPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/pages/DismissNotificationPageTest.kt index e8583df..7893210 100644 --- a/src/test/kotlin/net/pterodactylus/sone/web/pages/DismissNotificationPageTest.kt +++ b/src/test/kotlin/net/pterodactylus/sone/web/pages/DismissNotificationPageTest.kt @@ -28,7 +28,7 @@ class DismissNotificationPageTest : WebPageTest(::DismissNotificationPage) { @Test fun `page returns correct title`() { - whenever(l10n.getString("Page.DismissNotification.Title")).thenReturn("dismiss notification page") + addTranslation("Page.DismissNotification.Title", "dismiss notification page") assertThat(page.getPageTitle(soneRequest), equalTo("dismiss notification page")) } diff --git a/src/test/kotlin/net/pterodactylus/sone/web/pages/DistrustPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/pages/DistrustPageTest.kt deleted file mode 100644 index d706e05..0000000 --- a/src/test/kotlin/net/pterodactylus/sone/web/pages/DistrustPageTest.kt +++ /dev/null @@ -1,62 +0,0 @@ -package net.pterodactylus.sone.web.pages - -import net.pterodactylus.sone.data.* -import net.pterodactylus.sone.test.* -import net.pterodactylus.sone.web.* -import net.pterodactylus.util.web.Method.* -import org.hamcrest.MatcherAssert.* -import org.hamcrest.Matchers.* -import org.junit.* -import org.mockito.Mockito.* - -/** - * Unit test for [DistrustPage]. - */ -class DistrustPageTest : WebPageTest(::DistrustPage) { - - @Test - fun `page returns correct path`() { - assertThat(page.path, equalTo("distrust.html")) - } - - @Test - fun `page requires login`() { - assertThat(page.requiresLogin(), equalTo(true)) - } - - @Test - fun `page returns correct title`() { - whenever(l10n.getString("Page.Distrust.Title")).thenReturn("distrust page title") - assertThat(page.getPageTitle(soneRequest), equalTo("distrust page title")) - } - - @Test - fun `get request does not redirect`() { - page.processTemplate(freenetRequest, templateContext) - } - - @Test - fun `post request with invalid sone redirects to return page`() { - setMethod(POST) - addHttpRequestPart("returnPage", "return.html") - verifyRedirect("return.html") - } - - @Test - fun `post request with valid sone distrusts sone and redirects to return page`() { - setMethod(POST) - val remoteSone = mock() - addSone("remote-sone-id", remoteSone) - addHttpRequestPart("returnPage", "return.html") - addHttpRequestPart("sone", "remote-sone-id") - verifyRedirect("return.html") { - verify(core).distrustSone(currentSone, remoteSone) - } - } - - @Test - fun `page can be created by dependency injection`() { - assertThat(baseInjector.getInstance(), notNullValue()) - } - -} diff --git a/src/test/kotlin/net/pterodactylus/sone/web/pages/EditAlbumPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/pages/EditAlbumPageTest.kt index 4d435b4..72e814b 100644 --- a/src/test/kotlin/net/pterodactylus/sone/web/pages/EditAlbumPageTest.kt +++ b/src/test/kotlin/net/pterodactylus/sone/web/pages/EditAlbumPageTest.kt @@ -45,7 +45,7 @@ class EditAlbumPageTest : WebPageTest(::EditAlbumPage) { @Test fun `page returns correct title`() { - whenever(l10n.getString("Page.EditAlbum.Title")).thenReturn("edit album page") + addTranslation("Page.EditAlbum.Title", "edit album page") assertThat(page.getPageTitle(soneRequest), equalTo("edit album page")) } diff --git a/src/test/kotlin/net/pterodactylus/sone/web/pages/EditImagePageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/pages/EditImagePageTest.kt index 0961622..e43551c 100644 --- a/src/test/kotlin/net/pterodactylus/sone/web/pages/EditImagePageTest.kt +++ b/src/test/kotlin/net/pterodactylus/sone/web/pages/EditImagePageTest.kt @@ -43,7 +43,7 @@ class EditImagePageTest : WebPageTest(::EditImagePage) { @Test fun `page returns correct title`() { - whenever(l10n.getString("Page.EditImage.Title")).thenReturn("edit image page title") + addTranslation("Page.EditImage.Title", "edit image page title") assertThat(page.getPageTitle(soneRequest), equalTo("edit image page title")) } diff --git a/src/test/kotlin/net/pterodactylus/sone/web/pages/EditProfileFieldPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/pages/EditProfileFieldPageTest.kt index 7731fa2..4a84644 100644 --- a/src/test/kotlin/net/pterodactylus/sone/web/pages/EditProfileFieldPageTest.kt +++ b/src/test/kotlin/net/pterodactylus/sone/web/pages/EditProfileFieldPageTest.kt @@ -35,7 +35,7 @@ class EditProfileFieldPageTest : WebPageTest(::EditProfileFieldPage) { @Test fun `page returns correct title`() { - whenever(l10n.getString("Page.EditProfileField.Title")).thenReturn("edit profile field title") + addTranslation("Page.EditProfileField.Title", "edit profile field title") assertThat(page.getPageTitle(soneRequest), equalTo("edit profile field title")) } diff --git a/src/test/kotlin/net/pterodactylus/sone/web/pages/EditProfilePageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/pages/EditProfilePageTest.kt index a31f04c..74c3851 100644 --- a/src/test/kotlin/net/pterodactylus/sone/web/pages/EditProfilePageTest.kt +++ b/src/test/kotlin/net/pterodactylus/sone/web/pages/EditProfilePageTest.kt @@ -46,7 +46,7 @@ class EditProfilePageTest : WebPageTest(::EditProfilePage) { @Test fun `page returns correct title`() { - whenever(l10n.getString("Page.EditProfile.Title")).thenReturn("edit profile page title") + addTranslation("Page.EditProfile.Title", "edit profile page title") assertThat(page.getPageTitle(soneRequest), equalTo("edit profile page title")) } diff --git a/src/test/kotlin/net/pterodactylus/sone/web/pages/FollowSonePageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/pages/FollowSonePageTest.kt index ba1dc42..3873434 100644 --- a/src/test/kotlin/net/pterodactylus/sone/web/pages/FollowSonePageTest.kt +++ b/src/test/kotlin/net/pterodactylus/sone/web/pages/FollowSonePageTest.kt @@ -30,7 +30,7 @@ class FollowSonePageTest : WebPageTest(::FollowSonePage) { @Test fun `page returns correct title`() { - whenever(l10n.getString("Page.FollowSone.Title")).thenReturn("follow sone page title") + addTranslation("Page.FollowSone.Title", "follow sone page title") assertThat(page.getPageTitle(soneRequest), equalTo("follow sone page title")) } diff --git a/src/test/kotlin/net/pterodactylus/sone/web/pages/ImageBrowserPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/pages/ImageBrowserPageTest.kt index a0e9372..1533f4b 100644 --- a/src/test/kotlin/net/pterodactylus/sone/web/pages/ImageBrowserPageTest.kt +++ b/src/test/kotlin/net/pterodactylus/sone/web/pages/ImageBrowserPageTest.kt @@ -26,7 +26,7 @@ class ImageBrowserPageTest : WebPageTest(::ImageBrowserPage) { @Test fun `page returns correct title`() { - whenever(l10n.getString("Page.ImageBrowser.Title")).thenReturn("image browser page title") + addTranslation("Page.ImageBrowser.Title", "image browser page title") assertThat(page.getPageTitle(soneRequest), equalTo("image browser page title")) } diff --git a/src/test/kotlin/net/pterodactylus/sone/web/pages/IndexPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/pages/IndexPageTest.kt index 9104127..8c86365 100644 --- a/src/test/kotlin/net/pterodactylus/sone/web/pages/IndexPageTest.kt +++ b/src/test/kotlin/net/pterodactylus/sone/web/pages/IndexPageTest.kt @@ -34,7 +34,7 @@ class IndexPageTest : WebPageTest({ webInterface, loaders, templateRenderer -> I @Test fun `page returns correct title`() { - whenever(l10n.getString("Page.Index.Title")).thenReturn("index page title") + addTranslation("Page.Index.Title", "index page title") assertThat(page.getPageTitle(soneRequest), equalTo("index page title")) } diff --git a/src/test/kotlin/net/pterodactylus/sone/web/pages/KnownSonesPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/pages/KnownSonesPageTest.kt index 37e0e95..14cae74 100644 --- a/src/test/kotlin/net/pterodactylus/sone/web/pages/KnownSonesPageTest.kt +++ b/src/test/kotlin/net/pterodactylus/sone/web/pages/KnownSonesPageTest.kt @@ -73,7 +73,7 @@ class KnownSonesPageTest : WebPageTest(::KnownSonesPage) { @Test fun `page returns correct title`() { - whenever(l10n.getString("Page.KnownSones.Title")).thenReturn("known sones page title") + addTranslation("Page.KnownSones.Title", "known sones page title") assertThat(page.getPageTitle(soneRequest), equalTo("known sones page title")) } diff --git a/src/test/kotlin/net/pterodactylus/sone/web/pages/MetricsPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/pages/MetricsPageTest.kt new file mode 100644 index 0000000..4d26b6c --- /dev/null +++ b/src/test/kotlin/net/pterodactylus/sone/web/pages/MetricsPageTest.kt @@ -0,0 +1,85 @@ +/** + * Sone - MetricsPageTest.kt - Copyright © 2019–2020 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 . + */ + +package net.pterodactylus.sone.web.pages + +import com.codahale.metrics.* +import net.pterodactylus.sone.test.* +import net.pterodactylus.sone.web.* +import net.pterodactylus.sone.web.page.* +import org.hamcrest.MatcherAssert.* +import org.hamcrest.Matchers.* +import kotlin.test.* + +class MetricsPageTest : WebPageTest() { + + private val metricRegistry = MetricRegistry() + override val page by lazy { MetricsPage(webInterface, loaders, templateRenderer, metricRegistry) } + + @Test + fun `page returns correct path`() { + assertThat(page.path, equalTo("metrics.html")) + } + + @Test + fun `page does not require login`() { + assertThat(page.requiresLogin(), equalTo(false)) + } + + @Test + fun `page returns correct title`() { + addTranslation("Page.Metrics.Title", "metrics page title") + assertThat(page.getPageTitle(soneRequest), equalTo("metrics page title")) + } + + @Test + fun `page can be created by dependency injection`() { + assertThat(baseInjector.getInstance(), notNullValue()) + } + + @Test + fun `page is annotated with the correct menuname`() { + assertThat(page.menuName, equalTo("Metrics")) + } + + @Test + fun `page is annotated with correct template path`() { + assertThat(page.templatePath, equalTo("/templates/metrics.html")) + } + + @Test + @Suppress("UNCHECKED_CAST") + fun `metrics page stores histograms in template context`() { + createHistogram("sone.random.duration2") + createHistogram("sone.random.duration1") + page.handleRequest(soneRequest, templateContext) + val histograms = templateContext["histograms"] as Map + assertThat(histograms.entries.map { it.key to it.value }, containsInAnyOrder( + "sone.random.duration1" to metricRegistry.histogram("sone.random.duration1"), + "sone.random.duration2" to metricRegistry.histogram("sone.random.duration2") + )) + } + + private fun createHistogram(name: String) = metricRegistry.histogram(name).run { + update(10) + update(9) + update(1) + update(1) + update(8) + } + +} diff --git a/src/test/kotlin/net/pterodactylus/sone/web/pages/OptionsPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/pages/OptionsPageTest.kt index 71e1b7b..d2929d9 100644 --- a/src/test/kotlin/net/pterodactylus/sone/web/pages/OptionsPageTest.kt +++ b/src/test/kotlin/net/pterodactylus/sone/web/pages/OptionsPageTest.kt @@ -25,11 +25,8 @@ class OptionsPageTest : WebPageTest(::OptionsPage) { core.preferences.newImagesPerPage = 4 core.preferences.newFcpInterfaceActive = true core.preferences.newRequireFullAccess = true - core.preferences.newNegativeTrust = 7 - core.preferences.newPositiveTrust = 8 core.preferences.newPostCutOffLength = 51 core.preferences.newPostsPerPage = 10 - core.preferences.newTrustComment = "11" } @Before @@ -77,11 +74,8 @@ class OptionsPageTest : WebPageTest(::OptionsPage) { assertThat(templateContext["images-per-page"], equalTo(4)) assertThat(templateContext["fcp-interface-active"], equalTo(true)) assertThat(templateContext["require-full-access"], equalTo(true)) - assertThat(templateContext["negative-trust"], equalTo(7)) - assertThat(templateContext["positive-trust"], equalTo(8)) assertThat(templateContext["post-cut-off-length"], equalTo(51)) assertThat(templateContext["posts-per-page"], equalTo(10)) - assertThat(templateContext["trust-comment"], equalTo("11")) } } @@ -284,56 +278,6 @@ class OptionsPageTest : WebPageTest(::OptionsPage) { } @Test - fun `negative trust can not be set to -101`() { - verifyThatWrongValueForPreferenceIsDetected("negative-trust", "-101") - } - - @Test - fun `negative trust can be set to -100`() { - verifyThatPreferencesCanBeSet("negative-trust", "-100", -100) { core.preferences.negativeTrust } - } - - @Test - fun `negative trust can be set to 100`() { - verifyThatPreferencesCanBeSet("negative-trust", "100", 100) { core.preferences.negativeTrust } - } - - @Test - fun `negative trust can not be set to 101`() { - verifyThatWrongValueForPreferenceIsDetected("negative-trust", "101") - } - - @Test - fun `negative trust is set to default on invalid value`() { - verifyThatPreferencesCanBeSet("negative-trust", "invalid", -25) { core.preferences.negativeTrust } - } - - @Test - fun `positive trust can not be set to -1`() { - verifyThatWrongValueForPreferenceIsDetected("positive-trust", "-1") - } - - @Test - fun `positive trust can be set to 0`() { - verifyThatPreferencesCanBeSet("positive-trust", "0", 0) { core.preferences.positiveTrust } - } - - @Test - fun `positive trust can be set to 100`() { - verifyThatPreferencesCanBeSet("positive-trust", "100", 100) { core.preferences.positiveTrust } - } - - @Test - fun `positive trust can not be set to 101`() { - verifyThatWrongValueForPreferenceIsDetected("positive-trust", "101") - } - - @Test - fun `positive trust is set to default on invalid value`() { - verifyThatPreferencesCanBeSet("positive-trust", "invalid", 75) { core.preferences.positiveTrust } - } - - @Test fun `post cut off length can not be set to -49`() { verifyThatWrongValueForPreferenceIsDetected("post-cut-off-length", "-49") } @@ -364,16 +308,6 @@ class OptionsPageTest : WebPageTest(::OptionsPage) { } @Test - fun `trust comment can be set`() { - verifyThatPreferencesCanBeSet("trust-comment", "trust", "trust") { core.preferences.trustComment } - } - - @Test - fun `trust comment is set to default when set to empty value`() { - verifyThatPreferencesCanBeSet("trust-comment", "", "Set from Sone Web Interface") { core.preferences.trustComment } - } - - @Test fun `page can be created by dependency injection`() { assertThat(baseInjector.getInstance(), notNullValue()) } diff --git a/src/test/kotlin/net/pterodactylus/sone/web/pages/SoneTemplatePageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/pages/SoneTemplatePageTest.kt index 8e29df1..cffd478 100644 --- a/src/test/kotlin/net/pterodactylus/sone/web/pages/SoneTemplatePageTest.kt +++ b/src/test/kotlin/net/pterodactylus/sone/web/pages/SoneTemplatePageTest.kt @@ -31,7 +31,7 @@ class SoneTemplatePageTest : WebPageTest({ webInterface, loaders, templateRender @Test fun `page title is retrieved from l10n if page title key is given`() { SoneTemplatePage(webInterface, loaders, templateRenderer, pageTitleKey = "page.title", requiresLogin = false).let { page -> - whenever(l10n.getString("page.title")).thenReturn("Page Title") + addTranslation("page.title", "Page Title") assertThat(page.getPageTitle(soneRequest), equalTo("Page Title")) } } diff --git a/src/test/kotlin/net/pterodactylus/sone/web/pages/TrustPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/pages/TrustPageTest.kt deleted file mode 100644 index 990c1b7..0000000 --- a/src/test/kotlin/net/pterodactylus/sone/web/pages/TrustPageTest.kt +++ /dev/null @@ -1,78 +0,0 @@ -package net.pterodactylus.sone.web.pages - -import net.pterodactylus.sone.data.* -import net.pterodactylus.sone.test.getInstance -import net.pterodactylus.sone.test.mock -import net.pterodactylus.sone.web.* -import net.pterodactylus.util.web.Method.* -import org.hamcrest.MatcherAssert.* -import org.hamcrest.Matchers.* -import org.junit.* -import org.mockito.ArgumentMatchers.any -import org.mockito.ArgumentMatchers.eq -import org.mockito.Mockito.never -import org.mockito.Mockito.verify - -/** - * Unit test for [TrustPage]. - */ -class TrustPageTest : WebPageTest(::TrustPage) { - - @Test - fun `page returns correct path`() { - assertThat(page.path, equalTo("trust.html")) - } - - @Test - fun `page requires login`() { - assertThat(page.requiresLogin(), equalTo(true)) - } - - @Test - fun `page returns correct title`() { - addTranslation("Page.Trust.Title", "title trust page") - assertThat(page.getPageTitle(soneRequest), equalTo("title trust page")) - } - - @Test - fun `get method does not redirect`() { - verifyNoRedirect { } - } - - @Test - fun `post request without sone redirects to return page`() { - setMethod(POST) - addHttpRequestPart("returnPage", "return.html") - verifyRedirect("return.html") { - verify(core, never()).trustSone(eq(currentSone), any()) - } - } - - @Test - fun `post request with missing sone redirects to return page`() { - setMethod(POST) - addHttpRequestPart("returnPage", "return.html") - addHttpRequestPart("sone", "sone-id") - verifyRedirect("return.html") { - verify(core, never()).trustSone(eq(currentSone), any()) - } - } - - @Test - fun `post request with existing sone trusts the identity and redirects to return page`() { - setMethod(POST) - addHttpRequestPart("returnPage", "return.html") - addHttpRequestPart("sone", "sone-id") - val sone = mock() - addSone("sone-id", sone) - verifyRedirect("return.html") { - verify(core).trustSone(eq(currentSone), eq(sone)) - } - } - - @Test - fun `page can be created by dependency injection`() { - assertThat(baseInjector.getInstance(), notNullValue()) - } - -} diff --git a/src/test/kotlin/net/pterodactylus/sone/web/pages/UntrustPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/pages/UntrustPageTest.kt deleted file mode 100644 index eb0ebdd..0000000 --- a/src/test/kotlin/net/pterodactylus/sone/web/pages/UntrustPageTest.kt +++ /dev/null @@ -1,80 +0,0 @@ -package net.pterodactylus.sone.web.pages - -import net.pterodactylus.sone.data.* -import net.pterodactylus.sone.test.getInstance -import net.pterodactylus.sone.test.mock -import net.pterodactylus.sone.web.* -import net.pterodactylus.util.web.Method.* -import org.hamcrest.MatcherAssert.* -import org.hamcrest.Matchers.* -import org.junit.* -import org.mockito.ArgumentMatchers.any -import org.mockito.ArgumentMatchers.eq -import org.mockito.Mockito.never -import org.mockito.Mockito.verify - -/** - * Unit test for [UntrustPage]. - */ -class UntrustPageTest : WebPageTest(::UntrustPage) { - - @Test - fun `page returns correct path`() { - assertThat(page.path, equalTo("untrust.html")) - } - - @Test - fun `page requires login`() { - assertThat(page.requiresLogin(), equalTo(true)) - } - - @Test - fun `page returns correct title`() { - addTranslation("Page.Untrust.Title", "untrust page title") - assertThat(page.getPageTitle(soneRequest), equalTo("untrust page title")) - } - - @Test - fun `get request does not redirect`() { - verifyNoRedirect { - verify(core, never()).untrustSone(eq(currentSone), any()) - } - } - - @Test - fun `post request without sone parameter does not untrust but redirects`() { - setMethod(POST) - addHttpRequestPart("returnPage", "return.html") - verifyRedirect("return.html") { - verify(core, never()).untrustSone(eq(currentSone), any()) - } - } - - @Test - fun `post request with invalid sone parameter does not untrust but redirects`() { - setMethod(POST) - addHttpRequestPart("returnPage", "return.html") - addHttpRequestPart("sone", "no-sone") - verifyRedirect("return.html") { - verify(core, never()).untrustSone(eq(currentSone), any()) - } - } - - @Test - fun `post request with valid sone parameter untrusts and redirects`() { - setMethod(POST) - addHttpRequestPart("returnPage", "return.html") - addHttpRequestPart("sone", "sone-id") - val sone = mock() - addSone("sone-id", sone) - verifyRedirect("return.html") { - verify(core).untrustSone(currentSone, sone) - } - } - - @Test - fun `page can be created by dependency injection`() { - assertThat(baseInjector.getInstance(), notNullValue()) - } - -} diff --git a/src/test/kotlin/net/pterodactylus/sone/web/pages/WebPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/pages/WebPageTest.kt index f51bece..07ae727 100644 --- a/src/test/kotlin/net/pterodactylus/sone/web/pages/WebPageTest.kt +++ b/src/test/kotlin/net/pterodactylus/sone/web/pages/WebPageTest.kt @@ -6,6 +6,7 @@ import freenet.support.* import freenet.support.api.* import net.pterodactylus.sone.core.* import net.pterodactylus.sone.data.* +import net.pterodactylus.sone.freenet.* import net.pterodactylus.sone.freenet.wot.* import net.pterodactylus.sone.main.* import net.pterodactylus.sone.test.deepMock @@ -26,6 +27,7 @@ import org.mockito.ArgumentMatchers.eq import java.io.* import java.net.* import java.nio.charset.* +import java.util.* import kotlin.text.Charsets.UTF_8 /** @@ -40,16 +42,14 @@ open class WebPageTest(pageSupplier: (WebInterface, Loaders, TemplateRenderer) - val core = webInterface.core val eventBus = mock() val preferences = Preferences(eventBus) - val l10n = webInterface.l10n!! val sessionManager = mock() - val page by lazy { pageSupplier(webInterface, loaders, templateRenderer) } + open val page by lazy { pageSupplier(webInterface, loaders, templateRenderer) } val httpRequest = mock() val freenetRequest = mock() init { - whenever(freenetRequest.l10n).thenReturn(l10n) whenever(freenetRequest.sessionManager).thenReturn(sessionManager) whenever(freenetRequest.uri).thenReturn(mock()) } @@ -77,12 +77,16 @@ open class WebPageTest(pageSupplier: (WebInterface, Loaders, TemplateRenderer) - private val notifications = mutableMapOf() private val translations = mutableMapOf() + private val translation = object : Translation { + override val currentLocale = Locale.ENGLISH + override fun translate(key: String) = translations[key] ?: key + } + init { setupCore() setupWebInterface() setupHttpRequest() setupFreenetRequest() - setupTranslations() } private fun setupCore() { @@ -108,6 +112,7 @@ open class WebPageTest(pageSupplier: (WebInterface, Loaders, TemplateRenderer) - whenever(webInterface.getCurrentSoneWithoutCreatingSession(eq(toadletContext))).thenReturn(currentSone) whenever(webInterface.getNotifications(currentSone)).then { notifications.values } whenever(webInterface.getNotification(anyString())).then { notifications[it[0]].asOptional() } + whenever(webInterface.translation).thenReturn(translation) } private fun setupHttpRequest() { @@ -147,10 +152,6 @@ open class WebPageTest(pageSupplier: (WebInterface, Loaders, TemplateRenderer) - whenever(freenetRequest.toadletContext).thenReturn(toadletContext) } - private fun setupTranslations() { - whenever(l10n.getString(anyString())).then { translations[it[0]] ?: it[0] } - } - fun setMethod(method: Method) { whenever(httpRequest.method).thenReturn(method.name) whenever(freenetRequest.method).thenReturn(method) diff --git a/src/test/resources/net/pterodactylus/sone/core/sone-parser-with-invalid-recipient.xml b/src/test/resources/net/pterodactylus/sone/core/sone-parser-with-invalid-recipient.xml index e627651..ec7a190 100644 --- a/src/test/resources/net/pterodactylus/sone/core/sone-parser-with-invalid-recipient.xml +++ b/src/test/resources/net/pterodactylus/sone/core/sone-parser-with-invalid-recipient.xml @@ -5,7 +5,7 @@ - post-id + 3de12680-afef-11e9-a124-e713cf8912fe text 123456789012345678901234567890123456789012 diff --git a/src/test/resources/net/pterodactylus/sone/core/sone-parser-with-recipient.xml b/src/test/resources/net/pterodactylus/sone/core/sone-parser-with-recipient.xml index 14dabe1..a6e07ed 100644 --- a/src/test/resources/net/pterodactylus/sone/core/sone-parser-with-recipient.xml +++ b/src/test/resources/net/pterodactylus/sone/core/sone-parser-with-recipient.xml @@ -5,7 +5,7 @@ - post-id + 3de12680-afef-11e9-a124-e713cf8912fe text 1234567890123456789012345678901234567890123 diff --git a/src/test/resources/net/pterodactylus/sone/core/sone-parser-with-valid-post-reply-time.xml b/src/test/resources/net/pterodactylus/sone/core/sone-parser-with-valid-post-reply-time.xml index 4d77cb2..c206ef7 100644 --- a/src/test/resources/net/pterodactylus/sone/core/sone-parser-with-valid-post-reply-time.xml +++ b/src/test/resources/net/pterodactylus/sone/core/sone-parser-with-valid-post-reply-time.xml @@ -5,8 +5,8 @@ - reply-id - post-id + 5ccba7f4-aff0-11e9-b176-a7b9db60ce98 + 3de12680-afef-11e9-a124-e713cf8912fe reply-text diff --git a/src/test/resources/net/pterodactylus/sone/core/sone-parser-with-valid-post-time.xml b/src/test/resources/net/pterodactylus/sone/core/sone-parser-with-valid-post-time.xml index fdfb493..0bc7046 100644 --- a/src/test/resources/net/pterodactylus/sone/core/sone-parser-with-valid-post-time.xml +++ b/src/test/resources/net/pterodactylus/sone/core/sone-parser-with-valid-post-time.xml @@ -5,7 +5,7 @@ - post-id + 3de12680-afef-11e9-a124-e713cf8912fe text