🔀 Merge branch 'release/v82' master v82
authorDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Tue, 28 Apr 2020 08:44:24 +0000 (10:44 +0200)
committerDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Tue, 28 Apr 2020 08:44:24 +0000 (10:44 +0200)
150 files changed:
build.gradle
src/main/java/net/pterodactylus/sone/core/Core.java
src/main/java/net/pterodactylus/sone/core/FreenetInterface.java
src/main/java/net/pterodactylus/sone/core/Options.java
src/main/java/net/pterodactylus/sone/core/PreferenceChangedEvent.kt [deleted file]
src/main/java/net/pterodactylus/sone/core/PreferencesLoader.kt [deleted file]
src/main/java/net/pterodactylus/sone/core/SoneDownloaderImpl.java
src/main/java/net/pterodactylus/sone/core/SoneInserter.java
src/main/java/net/pterodactylus/sone/core/SoneModificationDetector.java
src/main/java/net/pterodactylus/sone/core/SoneUri.java [deleted file]
src/main/java/net/pterodactylus/sone/data/Album.java
src/main/java/net/pterodactylus/sone/data/Post.java
src/main/java/net/pterodactylus/sone/data/PostReply.java
src/main/java/net/pterodactylus/sone/data/Profile.java
src/main/java/net/pterodactylus/sone/data/Reply.java
src/main/java/net/pterodactylus/sone/data/Sone.java
src/main/java/net/pterodactylus/sone/data/impl/IdOnlySone.java
src/main/java/net/pterodactylus/sone/data/impl/SoneImpl.java
src/main/java/net/pterodactylus/sone/database/memory/MemoryDatabase.kt [deleted file]
src/main/java/net/pterodactylus/sone/database/memory/MemoryPostReply.java
src/main/java/net/pterodactylus/sone/fcp/CreatePostCommand.java
src/main/java/net/pterodactylus/sone/fcp/GetPostFeedCommand.java
src/main/java/net/pterodactylus/sone/fcp/GetSonesCommand.java
src/main/java/net/pterodactylus/sone/freenet/wot/Identity.java
src/main/java/net/pterodactylus/sone/freenet/wot/IdentityChangeDetector.kt [deleted file]
src/main/java/net/pterodactylus/sone/main/DefaultLoaders.java
src/main/java/net/pterodactylus/sone/main/SonePlugin.java
src/main/java/net/pterodactylus/sone/main/SonePlugin.kt [deleted file]
src/main/java/net/pterodactylus/sone/template/CollectionAccessor.java
src/main/java/net/pterodactylus/sone/template/SoneAccessor.java
src/main/java/net/pterodactylus/sone/text/SoneTextParserContext.java
src/main/java/net/pterodactylus/sone/utils/DefaultOption.java [deleted file]
src/main/java/net/pterodactylus/sone/utils/IntegerRangePredicate.java [deleted file]
src/main/java/net/pterodactylus/sone/utils/NumberParsers.java
src/main/java/net/pterodactylus/sone/web/AllPages.kt [deleted file]
src/main/java/net/pterodactylus/sone/web/WebInterface.java
src/main/java/net/pterodactylus/sone/web/page/PageToadlet.java [deleted file]
src/main/kotlin/net/pterodactylus/sone/core/PreferenceChangedEvent.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/core/Preferences.kt
src/main/kotlin/net/pterodactylus/sone/core/PreferencesLoader.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/core/SoneUriCreator.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/core/UpdatedSoneProcessor.kt
src/main/kotlin/net/pterodactylus/sone/core/event/StrictFilteringEvents.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/data/Album.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/data/Albums.kt [deleted file]
src/main/kotlin/net/pterodactylus/sone/data/Post.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/data/Reply.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/data/Sone.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/database/PostReplyStore.kt
src/main/kotlin/net/pterodactylus/sone/database/memory/MemoryDatabase.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/fcp/AbstractSoneCommand.kt
src/main/kotlin/net/pterodactylus/sone/freenet/wot/DefaultIdentity.kt
src/main/kotlin/net/pterodactylus/sone/freenet/wot/IdentityChangeDetector.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/freenet/wot/IdentityLoader.kt
src/main/kotlin/net/pterodactylus/sone/freenet/wot/IdentityManagerImpl.kt
src/main/kotlin/net/pterodactylus/sone/freenet/wot/PluginWebOfTrustConnector.kt
src/main/kotlin/net/pterodactylus/sone/freenet/wot/WebOfTrustConnector.kt
src/main/kotlin/net/pterodactylus/sone/main/SoneModule.kt
src/main/kotlin/net/pterodactylus/sone/main/SonePlugin.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/template/PostAccessor.kt
src/main/kotlin/net/pterodactylus/sone/text/SoneTextParser.kt
src/main/kotlin/net/pterodactylus/sone/utils/AutoCloseableBucket.kt [deleted file]
src/main/kotlin/net/pterodactylus/sone/utils/DefaultOption.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/AllPages.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/FreenetSessionProvider.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/SessionProvider.kt
src/main/kotlin/net/pterodactylus/sone/web/ajax/GetNotificationsAjaxPage.kt
src/main/kotlin/net/pterodactylus/sone/web/ajax/GetStatusAjaxPage.kt
src/main/kotlin/net/pterodactylus/sone/web/ajax/JsonPage.kt
src/main/kotlin/net/pterodactylus/sone/web/page/FreenetRequest.kt
src/main/kotlin/net/pterodactylus/sone/web/page/PageToadlet.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/page/PageToadletFactory.kt
src/main/kotlin/net/pterodactylus/sone/web/page/SoneRequest.kt
src/main/kotlin/net/pterodactylus/sone/web/pages/CreateSonePage.kt
src/main/kotlin/net/pterodactylus/sone/web/pages/ImageBrowserPage.kt
src/main/kotlin/net/pterodactylus/sone/web/pages/KnownSonesPage.kt
src/main/kotlin/net/pterodactylus/sone/web/pages/LoginPage.kt
src/main/kotlin/net/pterodactylus/sone/web/pages/OptionsPage.kt
src/main/kotlin/net/pterodactylus/sone/web/pages/SearchPage.kt
src/main/kotlin/net/pterodactylus/sone/web/pages/SoneTemplatePage.kt
src/main/resources/i18n/sone.de.properties
src/main/resources/i18n/sone.en.properties
src/main/resources/i18n/sone.es.properties
src/main/resources/i18n/sone.fr.properties
src/main/resources/i18n/sone.it.properties [new file with mode: 0644]
src/main/resources/i18n/sone.ja.properties
src/main/resources/i18n/sone.no.properties
src/main/resources/i18n/sone.pl.properties
src/main/resources/i18n/sone.ru.properties
src/main/resources/templates/options.html
src/test/java/net/pterodactylus/sone/core/SoneUriTest.java [deleted file]
src/test/java/net/pterodactylus/sone/core/WebOfTrustUpdaterTest.java
src/test/java/net/pterodactylus/sone/freenet/wot/IdentityManagerTest.kt [deleted file]
src/test/java/net/pterodactylus/sone/main/DebugLoadersTest.java
src/test/java/net/pterodactylus/sone/main/DefaultLoadersTest.java
src/test/java/net/pterodactylus/sone/test/Matchers.java
src/test/java/net/pterodactylus/sone/text/TextFilterTest.java
src/test/java/net/pterodactylus/sone/utils/DefaultOptionTest.java [deleted file]
src/test/java/net/pterodactylus/sone/utils/IntegerRangePredicateTest.java [deleted file]
src/test/kotlin/net/pterodactylus/sone/core/ConfigurationSoneParserTest.kt
src/test/kotlin/net/pterodactylus/sone/core/CoreTest.kt
src/test/kotlin/net/pterodactylus/sone/core/FreenetInterfaceTest.kt
src/test/kotlin/net/pterodactylus/sone/core/ImageInserterTest.kt
src/test/kotlin/net/pterodactylus/sone/core/PreferencesLoaderTest.kt
src/test/kotlin/net/pterodactylus/sone/core/PreferencesTest.kt
src/test/kotlin/net/pterodactylus/sone/core/SoneInserterTest.kt
src/test/kotlin/net/pterodactylus/sone/core/SoneParserTest.kt
src/test/kotlin/net/pterodactylus/sone/core/SoneUriCreatorTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/core/UpdatedSoneProcessorTest.kt
src/test/kotlin/net/pterodactylus/sone/data/AlbumTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/data/AlbumsTest.kt [deleted file]
src/test/kotlin/net/pterodactylus/sone/data/PostTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/data/ReplyTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/data/SoneTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/database/memory/MemoryDatabaseTest.kt
src/test/kotlin/net/pterodactylus/sone/fcp/CreatePostCommandTest.kt
src/test/kotlin/net/pterodactylus/sone/freenet/wot/IdentityLoaderTest.kt
src/test/kotlin/net/pterodactylus/sone/freenet/wot/IdentityManagerTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/main/SoneModuleTest.kt
src/test/kotlin/net/pterodactylus/sone/template/ImageAccessorTest.kt
src/test/kotlin/net/pterodactylus/sone/template/ProfileAccessorTest.kt
src/test/kotlin/net/pterodactylus/sone/template/SoneAccessorTest.kt
src/test/kotlin/net/pterodactylus/sone/template/UnknownDateFilterTest.kt
src/test/kotlin/net/pterodactylus/sone/test/Logging.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/test/Matchers.kt
src/test/kotlin/net/pterodactylus/sone/test/Mocks.kt
src/test/kotlin/net/pterodactylus/sone/utils/AutoCloseableBucketTest.kt [deleted file]
src/test/kotlin/net/pterodactylus/sone/utils/DefaultOptionTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/utils/OptionalsTest.kt
src/test/kotlin/net/pterodactylus/sone/web/FreenetSessionProviderTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/WebInterfaceModuleTest.kt
src/test/kotlin/net/pterodactylus/sone/web/ajax/CreatePostAjaxPageTest.kt
src/test/kotlin/net/pterodactylus/sone/web/ajax/EditAlbumAjaxPageTest.kt
src/test/kotlin/net/pterodactylus/sone/web/ajax/EditImageAjaxPageTest.kt
src/test/kotlin/net/pterodactylus/sone/web/ajax/TestObjects.kt
src/test/kotlin/net/pterodactylus/sone/web/page/FreenetRequestTest.kt
src/test/kotlin/net/pterodactylus/sone/web/page/PageToadletFactoryTest.kt
src/test/kotlin/net/pterodactylus/sone/web/page/PageToadletTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/page/SoneRequestTest.kt
src/test/kotlin/net/pterodactylus/sone/web/pages/CreatePostPageTest.kt
src/test/kotlin/net/pterodactylus/sone/web/pages/DeleteAlbumPageTest.kt
src/test/kotlin/net/pterodactylus/sone/web/pages/DeleteImagePageTest.kt
src/test/kotlin/net/pterodactylus/sone/web/pages/EditAlbumPageTest.kt
src/test/kotlin/net/pterodactylus/sone/web/pages/EditImagePageTest.kt
src/test/kotlin/net/pterodactylus/sone/web/pages/EditProfilePageTest.kt
src/test/kotlin/net/pterodactylus/sone/web/pages/ImageBrowserPageTest.kt
src/test/kotlin/net/pterodactylus/sone/web/pages/KnownSonesPageTest.kt
src/test/kotlin/net/pterodactylus/sone/web/pages/OptionsPageTest.kt
src/test/kotlin/net/pterodactylus/sone/web/pages/UploadImagePageTest.kt
src/test/kotlin/net/pterodactylus/sone/web/pages/WebPageTest.kt

index 3730ead..db5af2b 100644 (file)
@@ -1,12 +1,13 @@
+import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
 
 plugins {
-    id 'org.jetbrains.kotlin.jvm' version '1.3.61'
-    id 'org.jetbrains.kotlin.plugin.noarg' version '1.3.61'
+    id 'org.jetbrains.kotlin.jvm' version '1.3.70'
+    id 'org.jetbrains.kotlin.plugin.noarg' version '1.3.70'
     id 'info.solidsoft.pitest' version '1.4.5'
 }
 
 group = 'net.pterodactylus'
-version = '81'
+version = '82'
 
 repositories {
      mavenCentral()
@@ -22,6 +23,12 @@ tasks.withType(JavaCompile) {
        options.encoding = 'UTF-8'
 }
 
+tasks.withType(KotlinCompile) {
+    kotlinOptions {
+        jvmTarget = "1.8"
+    }
+}
+
 configurations {
     provided {
         dependencies.all { dep ->
@@ -30,7 +37,7 @@ configurations {
     }
     compile.extendsFrom provided
 }
+
 dependencies {
     provided group: 'org.freenetproject', name: 'fred', version: '0.7.5.1475'
     provided group: 'org.freenetproject', name: 'freenet-ext', version: '29'
@@ -69,6 +76,7 @@ task notParallelTest(type: Test) {
     useJUnit {
         includeCategories 'net.pterodactylus.sone.test.NotParallel'
     }
+    dependsOn parallelTest
 }
 
 test {
@@ -77,7 +85,7 @@ test {
 }
 
 task fatJar(type: Jar) {
-    archiveName = project.name.toLowerCase() + '-jar-with-dependencies.jar'
+    archiveFileName = project.name.toLowerCase() + '-jar-with-dependencies.jar'
     from { (configurations.runtime - configurations.provided).collect { it.isDirectory() ? it : zipTree(it) } }
     manifest {
         attributes('Plugin-Main-Class': 'net.pterodactylus.sone.main.SonePlugin')
index 18588a1..da1b2f8 100644 (file)
@@ -24,7 +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 static net.pterodactylus.sone.data.AlbumKt.getAllImages;
 
 import java.util.ArrayList;
 import java.util.Collection;
@@ -62,6 +62,7 @@ import net.pterodactylus.sone.data.Profile.Field;
 import net.pterodactylus.sone.data.Reply;
 import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.data.Sone.SoneStatus;
+import net.pterodactylus.sone.data.SoneKt;
 import net.pterodactylus.sone.data.SoneOptions.LoadExternalContent;
 import net.pterodactylus.sone.data.TemporaryImage;
 import net.pterodactylus.sone.database.AlbumBuilder;
@@ -90,7 +91,6 @@ import net.pterodactylus.util.thread.NamedThreadFactory;
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Stopwatch;
-import com.google.common.collect.FluentIterable;
 import com.google.common.collect.HashMultimap;
 import com.google.common.collect.Multimap;
 import com.google.common.collect.Multimaps;
@@ -180,24 +180,10 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
        private final MetricRegistry metricRegistry;
        private final Histogram configurationSaveTimeHistogram;
 
-       /**
-        * Creates a new core.
-        *
-        * @param configuration
-        *            The configuration of the core
-        * @param freenetInterface
-        *            The freenet interface
-        * @param identityManager
-        *            The identity manager
-        * @param webOfTrustUpdater
-        *            The WebOfTrust updater
-        * @param eventBus
-        *            The event bus
-        * @param database
-        *            The database
-        */
+       private final SoneUriCreator soneUriCreator;
+
        @Inject
-       public Core(Configuration configuration, FreenetInterface freenetInterface, IdentityManager identityManager, SoneDownloader soneDownloader, ImageInserter imageInserter, UpdateChecker updateChecker, WebOfTrustUpdater webOfTrustUpdater, EventBus eventBus, Database database, MetricRegistry metricRegistry) {
+       public Core(Configuration configuration, FreenetInterface freenetInterface, IdentityManager identityManager, SoneDownloader soneDownloader, ImageInserter imageInserter, UpdateChecker updateChecker, WebOfTrustUpdater webOfTrustUpdater, EventBus eventBus, Database database, MetricRegistry metricRegistry, SoneUriCreator soneUriCreator) {
                super("Sone Core");
                this.configuration = configuration;
                this.freenetInterface = freenetInterface;
@@ -209,6 +195,7 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
                this.eventBus = eventBus;
                this.database = database;
                this.metricRegistry = metricRegistry;
+               this.soneUriCreator = soneUriCreator;
                preferences = new Preferences(eventBus);
                this.configurationSaveTimeHistogram = metricRegistry.histogram("configuration.save.duration", () -> new Histogram(new ExponentiallyDecayingReservoir(3000, 0)));
        }
@@ -626,7 +613,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, metricRegistry, ownIdentity.getId());
+               SoneInserter soneInserter = new SoneInserter(this, eventBus, freenetInterface, metricRegistry, soneUriCreator, ownIdentity.getId());
                soneInserter.insertionDelayChanged(new InsertionDelayChangedEvent(preferences.getInsertionDelay()));
                eventBus.register(soneInserter);
                synchronized (soneInserters) {
@@ -810,9 +797,9 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
                }
                for (PostReply postReply : soneComparison.getNewPostReplies()) {
                        if (postReply.getSone().equals(newSone)) {
-                               postReply.setKnown(true);
+                               database.setPostReplyKnown(postReply);
                        } else if (postReply.getTime() < database.getFollowingTime(newSone.getId())) {
-                               postReply.setKnown(true);
+                               database.setPostReplyKnown(postReply);
                        } else if (!postReply.isKnown()) {
                                events.add(new NewPostReplyFoundEvent(postReply));
                        }
@@ -989,7 +976,7 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
                        post.setKnown(true);
                }
                for (PostReply reply : replies) {
-                       reply.setKnown(true);
+                       database.setPostReplyKnown(reply);
                }
 
                logger.info(String.format("Sone loaded successfully: %s", sone));
@@ -1131,7 +1118,7 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
         */
        public void markReplyKnown(PostReply reply) {
                boolean previouslyKnown = reply.isKnown();
-               reply.setKnown(true);
+               database.setPostReplyKnown(reply);
                eventBus.post(new MarkPostReplyKnownEvent(reply));
                if (!previouslyKnown) {
                        touchConfiguration();
@@ -1405,7 +1392,7 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
                        configuration.getStringValue(sonePrefix + "/Likes/Reply/" + replyLikeCounter + "/ID").setValue(null);
 
                        /* save albums. first, collect in a flat structure, top-level first. */
-                       List<Album> albums = FluentIterable.from(sone.getRootAlbum().getAlbums()).transformAndConcat(Album.FLATTENER).toList();
+                       List<Album> albums = SoneKt.getAllAlbums(sone);
 
                        int albumCounter = 0;
                        for (Album album : albums) {
index a01a2bb..35ac6a1 100644 (file)
@@ -91,6 +91,8 @@ public class FreenetInterface {
        /** The node to interact with. */
        private final Node node;
 
+       private final SoneUriCreator soneUriCreator;
+
        /** The high-level client to use for requests. */
        private final HighLevelSimpleClient client;
        private final RequestClient requestClient = new RequestClientBuilder().realTime().build();
@@ -104,18 +106,11 @@ public class FreenetInterface {
        private final RequestClient imageInserts = new RequestClientBuilder().realTime().build();
        private final RequestClient imageLoader = new RequestClientBuilder().realTime().build();
 
-       /**
-        * Creates a new Freenet interface.
-        *
-        * @param eventBus
-        *            The event bus
-        * @param node
-        *            The node to interact with
-        */
        @Inject
-       public FreenetInterface(EventBus eventBus, Node node) {
+       public FreenetInterface(EventBus eventBus, Node node, SoneUriCreator soneUriCreator) {
                this.eventBus = eventBus;
                this.node = node;
+               this.soneUriCreator = soneUriCreator;
                this.client = node.clientCore.makeClient(RequestStarter.INTERACTIVE_PRIORITY_CLASS, false, true);
        }
 
@@ -291,9 +286,9 @@ public class FreenetInterface {
                }
                try {
                        logger.log(Level.FINEST, String.format("Unsubscribing from USK for %s…", sone));
-                       node.clientCore.uskManager.unsubscribe(USK.create(sone.getRequestUri()), uskCallback);
+                       node.clientCore.uskManager.unsubscribe(USK.create(soneUriCreator.getRequestUri(sone)), uskCallback);
                } catch (MalformedURLException mue1) {
-                       logger.log(Level.FINE, String.format("Could not unsubscribe USK “%s”!", sone.getRequestUri()), mue1);
+                       logger.log(Level.FINE, String.format("Could not unsubscribe USK “%s”!", soneUriCreator.getRequestUri(sone)), mue1);
                }
        }
 
index 88e33a0..398374a 100644 (file)
@@ -23,8 +23,6 @@ import java.util.Map;
 
 import net.pterodactylus.sone.utils.Option;
 
-import com.google.common.base.Predicate;
-
 /**
  * Stores various options that influence Sone’s behaviour.
  */
diff --git a/src/main/java/net/pterodactylus/sone/core/PreferenceChangedEvent.kt b/src/main/java/net/pterodactylus/sone/core/PreferenceChangedEvent.kt
deleted file mode 100644 (file)
index 2ebb62d..0000000
+++ /dev/null
@@ -1,3 +0,0 @@
-package net.pterodactylus.sone.core
-
-data class PreferenceChangedEvent(val preferenceName: String, val newValue: Any)
diff --git a/src/main/java/net/pterodactylus/sone/core/PreferencesLoader.kt b/src/main/java/net/pterodactylus/sone/core/PreferencesLoader.kt
deleted file mode 100644 (file)
index 32c35cb..0000000
+++ /dev/null
@@ -1,58 +0,0 @@
-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] }
-       }
-
-}
index 0538c8b..43a87fb 100644 (file)
@@ -17,7 +17,6 @@
 
 package net.pterodactylus.sone.core;
 
-import static freenet.support.io.Closer.close;
 import static java.lang.String.format;
 import static java.lang.System.currentTimeMillis;
 import static java.util.concurrent.TimeUnit.DAYS;
@@ -185,11 +184,8 @@ public class SoneDownloaderImpl extends AbstractService implements SoneDownloade
        private Sone parseSone(Sone originalSone, FetchResult fetchResult, FreenetURI requestUri) {
                logger.finest(() -> format("Parsing FetchResult (%d bytes, %s) for %s…", fetchResult.size(), fetchResult.getMimeType(), originalSone));
                Bucket soneBucket = fetchResult.asBucket();
-               InputStream soneInputStream = null;
-               try {
-                       soneInputStream = soneBucket.getInputStream();
-                       Sone parsedSone = soneParser.parseSone(originalSone,
-                                       soneInputStream);
+               try (InputStream soneInputStream = soneBucket.getInputStream()) {
+                       Sone parsedSone = soneParser.parseSone(originalSone, soneInputStream);
                        if (parsedSone != null) {
                                logger.finer(() -> format("Sone %s was successfully parsed.", parsedSone));
                                parsedSone.setLatestEdition(requestUri.getEdition());
@@ -198,8 +194,7 @@ public class SoneDownloaderImpl extends AbstractService implements SoneDownloade
                } catch (Exception e1) {
                        logger.log(Level.WARNING, e1, () -> format("Could not parse Sone from %s!", requestUri));
                } finally {
-                       close(soneInputStream);
-                       close(soneBucket);
+                       soneBucket.free();
                }
                return null;
        }
index 7ccb374..fcbe1b5 100644 (file)
@@ -21,18 +21,16 @@ 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;
+import static java.util.stream.Collectors.toList;
+import static net.pterodactylus.sone.data.PostKt.newestPostFirst;
+import static net.pterodactylus.sone.data.ReplyKt.newestReplyFirst;
 
-import java.io.Closeable;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.StringWriter;
+import java.io.*;
 import java.nio.charset.Charset;
 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;
@@ -44,13 +42,11 @@ import net.pterodactylus.sone.core.event.InsertionDelayChangedEvent;
 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.Post;
-import net.pterodactylus.sone.data.Reply;
+import net.pterodactylus.sone.data.AlbumKt;
 import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.data.Sone.SoneStatus;
+import net.pterodactylus.sone.data.SoneKt;
 import net.pterodactylus.sone.main.SonePlugin;
-import net.pterodactylus.util.io.Closer;
 import net.pterodactylus.util.service.AbstractService;
 import net.pterodactylus.util.template.HtmlFilter;
 import net.pterodactylus.util.template.ReflectionAccessor;
@@ -62,7 +58,6 @@ import net.pterodactylus.util.template.TemplateParser;
 import net.pterodactylus.util.template.XmlFilter;
 
 import com.google.common.annotations.VisibleForTesting;
-import com.google.common.collect.FluentIterable;
 import com.google.common.collect.Ordering;
 import com.google.common.eventbus.EventBus;
 import com.google.common.eventbus.Subscribe;
@@ -106,6 +101,7 @@ public class SoneInserter extends AbstractService {
        private final FreenetInterface freenetInterface;
 
        private final SoneModificationDetector soneModificationDetector;
+       private final SoneUriCreator soneUriCreator;
        private final long delay;
        private final String soneId;
        private final Histogram soneInsertDurationHistogram;
@@ -123,8 +119,8 @@ public class SoneInserter extends AbstractService {
         * @param soneId
         *            The ID of the Sone to insert
         */
-       public SoneInserter(final Core core, EventBus eventBus, FreenetInterface freenetInterface, MetricRegistry metricRegistry, final String soneId) {
-               this(core, eventBus, freenetInterface, metricRegistry, soneId, new SoneModificationDetector(new LockableFingerprintProvider() {
+       public SoneInserter(final Core core, EventBus eventBus, FreenetInterface freenetInterface, MetricRegistry metricRegistry, SoneUriCreator soneUriCreator, final String soneId) {
+               this(core, eventBus, freenetInterface, metricRegistry, soneUriCreator, soneId, new SoneModificationDetector(new LockableFingerprintProvider() {
                        @Override
                        public boolean isLocked() {
                                Sone sone = core.getSone(soneId);
@@ -146,13 +142,14 @@ public class SoneInserter extends AbstractService {
        }
 
        @VisibleForTesting
-       SoneInserter(Core core, EventBus eventBus, FreenetInterface freenetInterface, MetricRegistry metricRegistry, String soneId, SoneModificationDetector soneModificationDetector, long delay) {
+       SoneInserter(Core core, EventBus eventBus, FreenetInterface freenetInterface, MetricRegistry metricRegistry, SoneUriCreator soneUriCreator, 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.soneUriCreator = soneUriCreator;
                this.soneId = soneId;
                this.soneModificationDetector = soneModificationDetector;
                this.delay = delay;
@@ -237,7 +234,7 @@ public class SoneInserter extends AbstractService {
                                                long insertTime = currentTimeMillis();
                                                eventBus.post(new SoneInsertingEvent(sone));
                                                Stopwatch stopwatch = Stopwatch.createStarted();
-                                               FreenetURI finalUri = freenetInterface.insertDirectory(sone.getInsertUri(), insertInformation.generateManifestEntries(), "index.html");
+                                               FreenetURI finalUri = freenetInterface.insertDirectory(soneUriCreator.getInsertUri(sone), insertInformation.generateManifestEntries(), "index.html");
                                                stopwatch.stop();
                                                soneInsertDurationHistogram.update(stopwatch.elapsed(MICROSECONDS));
                                                eventBus.post(new SoneInsertedEvent(sone, stopwatch.elapsed(MILLISECONDS), insertInformation.getFingerprint()));
@@ -310,13 +307,12 @@ public class SoneInserter extends AbstractService {
                        soneProperties.put("id", sone.getId());
                        soneProperties.put("name", sone.getName());
                        soneProperties.put("time", currentTimeMillis());
-                       soneProperties.put("requestUri", sone.getRequestUri());
                        soneProperties.put("profile", sone.getProfile());
-                       soneProperties.put("posts", Ordering.from(Post.NEWEST_FIRST).sortedCopy(sone.getPosts()));
-                       soneProperties.put("replies", Ordering.from(Reply.TIME_COMPARATOR).reverse().sortedCopy(sone.getReplies()));
+                       soneProperties.put("posts", Ordering.from(newestPostFirst()).sortedCopy(sone.getPosts()));
+                       soneProperties.put("replies", Ordering.from(newestReplyFirst()).sortedCopy(sone.getReplies()));
                        soneProperties.put("likedPostIds", new HashSet<>(sone.getLikedPostIds()));
                        soneProperties.put("likedReplyIds", new HashSet<>(sone.getLikedReplyIds()));
-                       soneProperties.put("albums", FluentIterable.from(sone.getRootAlbum().getAlbums()).transformAndConcat(Album.FLATTENER).filter(NOT_EMPTY).toList());
+                       soneProperties.put("albums", SoneKt.getAllAlbums(sone).stream().filter(AlbumKt.notEmpty()::invoke).collect(toList()));
                        manifestCreator = new ManifestCreator(core, soneProperties);
                }
 
@@ -377,19 +373,13 @@ public class SoneInserter extends AbstractService {
                }
 
                public ManifestElement createManifestElement(String name, String contentType, String templateName) {
-                       InputStreamReader templateInputStreamReader = null;
-                       InputStream templateInputStream = null;
                        Template template;
-                       try {
-                               templateInputStream = getClass().getResourceAsStream(templateName);
-                               templateInputStreamReader = new InputStreamReader(templateInputStream, utf8Charset);
+                       try (InputStream templateInputStream = getClass().getResourceAsStream(templateName);
+                                       InputStreamReader templateInputStreamReader = new InputStreamReader(templateInputStream, utf8Charset)) {
                                template = TemplateParser.parse(templateInputStreamReader);
-                       } catch (TemplateException te1) {
-                               logger.log(Level.SEVERE, String.format("Could not parse template “%s”!", templateName), te1);
+                       } catch (IOException | TemplateException e1) {
+                               logger.log(Level.SEVERE, String.format("Could not parse template “%s”!", templateName), e1);
                                return null;
-                       } finally {
-                               Closer.close(templateInputStreamReader);
-                               Closer.close(templateInputStream);
                        }
 
                        TemplateContext templateContext = templateContextFactory.createTemplateContext();
@@ -397,17 +387,14 @@ public class SoneInserter extends AbstractService {
                        templateContext.set("currentSone", soneProperties);
                        templateContext.set("currentEdition", core.getUpdateChecker().getLatestEdition());
                        templateContext.set("version", SonePlugin.getPluginVersion());
-                       StringWriter writer = new StringWriter();
-                       try {
+                       try (StringWriter writer = new StringWriter()) {
                                template.render(templateContext, writer);
                                RandomAccessBucket bucket = new ArrayBucket(writer.toString().getBytes(Charsets.UTF_8));
                                buckets.add(bucket);
                                return new ManifestElement(name, bucket, contentType, bucket.size());
-                       } catch (TemplateException te1) {
-                               logger.log(Level.SEVERE, String.format("Could not render template “%s”!", templateName), te1);
+                       } catch (IOException | TemplateException e1) {
+                               logger.log(Level.SEVERE, String.format("Could not render template “%s”!", templateName), e1);
                                return null;
-                       } finally {
-                               Closer.close(writer);
                        }
                }
 
index e5e20d9..793d332 100644 (file)
@@ -1,7 +1,5 @@
 package net.pterodactylus.sone.core;
 
-import static com.google.common.base.Optional.absent;
-import static com.google.common.base.Optional.of;
 import static com.google.common.base.Ticker.systemTicker;
 import static java.util.concurrent.TimeUnit.NANOSECONDS;
 
@@ -11,7 +9,6 @@ import net.pterodactylus.sone.data.Sone;
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Objects;
-import com.google.common.base.Optional;
 import com.google.common.base.Ticker;
 
 /**
diff --git a/src/main/java/net/pterodactylus/sone/core/SoneUri.java b/src/main/java/net/pterodactylus/sone/core/SoneUri.java
deleted file mode 100644 (file)
index a929950..0000000
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * 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
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.sone.core;
-
-import static java.util.logging.Logger.getLogger;
-
-import java.net.MalformedURLException;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-
-import freenet.keys.FreenetURI;
-
-/**
- * Helper class that creates {@link FreenetURI}s for Sone to insert to and
- * request from.
- */
-public class SoneUri {
-
-       /** The logger. */
-       private static final Logger logger = getLogger(SoneUri.class.getName());
-
-       /**
-        * Generate a Sone URI from the given URI.
-        *
-        * @param uri
-        *            The URI to derive the Sone URI from
-        * @return The derived URI
-        */
-       public static FreenetURI create(String uri) {
-               try {
-                       return new FreenetURI(uri).setDocName("Sone").setMetaString(new String[0]);
-               } catch (MalformedURLException mue1) {
-                       /* this should never happen. */
-                       logger.log(Level.WARNING, String.format("Could not create Sone URI from URI: %s", uri), mue1);
-                       return null;
-               }
-       }
-
-}
index 576c73a..bf8f3ec 100644 (file)
 
 package net.pterodactylus.sone.data;
 
-import static java.util.Arrays.asList;
-import static java.util.Collections.emptyList;
-
-import java.util.ArrayList;
-import java.util.Collections;
 import java.util.List;
-import javax.annotation.Nonnull;
-
-import com.google.common.base.Function;
-import com.google.common.base.Predicate;
-import com.google.common.collect.FluentIterable;
-import com.google.common.collect.ImmutableList;
 
 /**
  * Container for images that can also contain nested {@link Album}s.
  */
 public interface Album extends Identified, Fingerprintable {
 
-       /** Function that flattens the given album and all albums beneath it. */
-       Function<Album, List<Album>> FLATTENER = new Function<Album, List<Album>>() {
-
-               @Override
-               @Nonnull
-               public List<Album> apply(Album album) {
-                       if (album == null) {
-                               return emptyList();
-                       }
-                       List<Album> albums = new ArrayList<>();
-                       albums.add(album);
-                       for (Album subAlbum : album.getAlbums()) {
-                               albums.addAll(FluentIterable.from(ImmutableList.of(subAlbum)).transformAndConcat(FLATTENER).toList());
-                       }
-                       return albums;
-               }
-       };
-
-       /** Function that transforms an album into the images it contains. */
-       Function<Album, List<Image>> IMAGES = new Function<Album, List<Image>>() {
-
-               @Override
-               @Nonnull
-               public List<Image> apply(Album album) {
-                       return (album != null) ? album.getImages() : Collections.<Image>emptyList();
-               }
-       };
-
-       /**
-        * Filter that removes all albums that do not have any images in any album
-        * below it.
-        */
-       Predicate<Album> NOT_EMPTY = new Predicate<Album>() {
-
-               @Override
-               public boolean apply(Album album) {
-                       /* so, we flatten all albums below the given one and check whether at least one album… */
-                       return FluentIterable.from(asList(album)).transformAndConcat(FLATTENER).anyMatch(new Predicate<Album>() {
-
-                               @Override
-                               public boolean apply(Album album) {
-                                       /* …contains any inserted images. */
-                                       return !album.getImages().isEmpty() && FluentIterable.from(album.getImages()).allMatch(new Predicate<Image>() {
-
-                                               @Override
-                                               public boolean apply(Image input) {
-                                                       return input.isInserted();
-                                               }
-                                       });
-                               }
-                       });
-               }
-       };
-
        /**
         * Returns the ID of this album.
         *
index a4a794e..d4d34e6 100644 (file)
@@ -19,10 +19,7 @@ package net.pterodactylus.sone.data;
 
 import static com.google.common.base.Optional.absent;
 
-import java.util.Comparator;
-
 import com.google.common.base.Optional;
-import com.google.common.base.Predicate;
 
 /**
  * A post is a short message that a user writes in his Sone to let other users
@@ -30,26 +27,6 @@ import com.google.common.base.Predicate;
  */
 public interface Post extends Identified {
 
-       /** Comparator for posts, sorts descending by time. */
-       public static final Comparator<Post> NEWEST_FIRST = new Comparator<Post>() {
-
-               @Override
-               public int compare(Post leftPost, Post rightPost) {
-                       return (int) Math.max(Integer.MIN_VALUE, Math.min(Integer.MAX_VALUE, rightPost.getTime() - leftPost.getTime()));
-               }
-
-       };
-
-       /** Filter for posts with timestamps from the future. */
-       public static final Predicate<Post> FUTURE_POSTS_FILTER = new Predicate<Post>() {
-
-               @Override
-               public boolean apply(Post post) {
-                       return (post != null) && (post.getTime() <= System.currentTimeMillis());
-               }
-
-       };
-
        //
        // ACCESSORS
        //
index f5ffea3..6db3876 100644 (file)
@@ -18,7 +18,6 @@
 package net.pterodactylus.sone.data;
 
 import com.google.common.base.Optional;
-import com.google.common.base.Predicate;
 
 /**
  * A reply is like a {@link Post} but can never be posted on its own, it always
@@ -27,18 +26,6 @@ import com.google.common.base.Predicate;
 public interface PostReply extends Reply<PostReply> {
 
        /**
-        * Filter that selects {@link PostReply}s that have a
-        * {@link Optional#isPresent() present} {@link #getPost() post}.
-        */
-       public static final Predicate<PostReply> HAS_POST_FILTER = new Predicate<PostReply>() {
-
-               @Override
-               public boolean apply(PostReply postReply) {
-                       return (postReply != null) && postReply.getPost().isPresent();
-               }
-       };
-
-       /**
         * Returns the ID of the post this reply refers to.
         *
         * @return The ID of the post this reply refers to
index b87ef4f..34246cb 100644 (file)
@@ -19,7 +19,6 @@ package net.pterodactylus.sone.data;
 
 import static com.google.common.base.Preconditions.checkArgument;
 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.ArrayList;
index 120575e..0cf2a52 100644 (file)
 
 package net.pterodactylus.sone.data;
 
-import java.util.Comparator;
-
-import com.google.common.base.Predicate;
-
 /**
  * Defines methods common for all replies.
  *
@@ -29,32 +25,6 @@ import com.google.common.base.Predicate;
  */
 public interface Reply<T extends Reply<T>> extends Identified {
 
-       /** Comparator that sorts replies ascending by time. */
-       public static final Comparator<? super Reply<?>> TIME_COMPARATOR = new Comparator<Reply<?>>() {
-
-               /**
-                * {@inheritDoc}
-                */
-               @Override
-               public int compare(Reply<?> leftReply, Reply<?> rightReply) {
-                       return (int) Math.max(Integer.MIN_VALUE, Math.min(Integer.MAX_VALUE, leftReply.getTime() - rightReply.getTime()));
-               }
-
-       };
-
-       /** Filter for replies with timestamps from the future. */
-       public static final Predicate<Reply<?>> FUTURE_REPLY_FILTER = new Predicate<Reply<?>>() {
-
-               /**
-                * {@inheritDoc}
-                */
-               @Override
-               public boolean apply(Reply<?> reply) {
-                       return (reply != null) && (reply.getTime() <= System.currentTimeMillis());
-               }
-
-       };
-
        /**
         * Returns the ID of the reply.
         *
@@ -90,13 +60,4 @@ public interface Reply<T extends Reply<T>> extends Identified {
         */
        public boolean isKnown();
 
-       /**
-        * Sets whether this reply is known.
-        *
-        * @param known
-        *            {@code true} if this reply is known, {@code false} otherwise
-        * @return This reply
-        */
-       public T setKnown(boolean known);
-
 }
index b43d025..2b0a2eb 100644 (file)
 
 package net.pterodactylus.sone.data;
 
-import static com.google.common.collect.FluentIterable.from;
-import static java.util.Arrays.asList;
-import static net.pterodactylus.sone.data.Album.FLATTENER;
-import static net.pterodactylus.sone.data.Album.IMAGES;
-
 import java.util.Collection;
-import java.util.Collections;
-import java.util.Comparator;
 import java.util.List;
 import java.util.Set;
 
@@ -32,15 +25,9 @@ import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 
 import net.pterodactylus.sone.freenet.wot.Identity;
-import net.pterodactylus.sone.freenet.wot.OwnIdentity;
-import net.pterodactylus.sone.template.SoneAccessor;
 
 import freenet.keys.FreenetURI;
 
-import com.google.common.base.Function;
-import com.google.common.base.Predicate;
-import com.google.common.primitives.Ints;
-
 /**
  * A Sone defines everything about a user: her profile, her status updates, her
  * replies, her likes and dislikes, etc.
@@ -65,101 +52,6 @@ public interface Sone extends Identified, Fingerprintable, Comparable<Sone> {
                downloading,
        }
 
-       /** comparator that sorts Sones by their nice name. */
-       public static final Comparator<Sone> NICE_NAME_COMPARATOR = new Comparator<Sone>() {
-
-               @Override
-               public int compare(Sone leftSone, Sone rightSone) {
-                       int diff = SoneAccessor.getNiceName(leftSone).compareToIgnoreCase(SoneAccessor.getNiceName(rightSone));
-                       if (diff != 0) {
-                               return diff;
-                       }
-                       return leftSone.getId().compareToIgnoreCase(rightSone.getId());
-               }
-
-       };
-
-       /** Comparator that sorts Sones by last activity (least recent active first). */
-       public static final Comparator<Sone> LAST_ACTIVITY_COMPARATOR = new Comparator<Sone>() {
-
-               @Override
-               public int compare(Sone firstSone, Sone secondSone) {
-                       return (int) Math.min(Integer.MAX_VALUE, Math.max(Integer.MIN_VALUE, secondSone.getTime() - firstSone.getTime()));
-               }
-       };
-
-       /** Comparator that sorts Sones by numbers of posts (descending). */
-       public static final Comparator<Sone> POST_COUNT_COMPARATOR = new Comparator<Sone>() {
-
-               /**
-                * {@inheritDoc}
-                */
-               @Override
-               public int compare(Sone leftSone, Sone rightSone) {
-                       return (leftSone.getPosts().size() != rightSone.getPosts().size()) ? (rightSone.getPosts().size() - leftSone.getPosts().size()) : (rightSone.getReplies().size() - leftSone.getReplies().size());
-               }
-       };
-
-       /** Comparator that sorts Sones by number of images (descending). */
-       public static final Comparator<Sone> IMAGE_COUNT_COMPARATOR = new Comparator<Sone>() {
-
-               /**
-                * {@inheritDoc}
-                */
-               @Override
-               public int compare(Sone leftSone, Sone rightSone) {
-                       int rightSoneImageCount = from(asList(rightSone.getRootAlbum())).transformAndConcat(FLATTENER).transformAndConcat(IMAGES).size();
-                       int leftSoneImageCount = from(asList(leftSone.getRootAlbum())).transformAndConcat(FLATTENER).transformAndConcat(IMAGES).size();
-                       /* sort descending. */
-                       return Ints.compare(rightSoneImageCount, leftSoneImageCount);
-               }
-       };
-
-       /** Filter to remove Sones that have not been downloaded. */
-       public static final Predicate<Sone> EMPTY_SONE_FILTER = new Predicate<Sone>() {
-
-               @Override
-               public boolean apply(Sone sone) {
-                       return (sone != null) && (sone.getTime() != 0);
-               }
-       };
-
-       /** Filter that matches all {@link Sone#isLocal() local Sones}. */
-       public static final Predicate<Sone> LOCAL_SONE_FILTER = new Predicate<Sone>() {
-
-               @Override
-               public boolean apply(Sone sone) {
-                       return (sone != null) && (sone.getIdentity() instanceof OwnIdentity);
-               }
-
-       };
-
-       /** Filter that matches Sones that have at least one album. */
-       public static final Predicate<Sone> HAS_ALBUM_FILTER = new Predicate<Sone>() {
-
-               @Override
-               public boolean apply(Sone sone) {
-                       return (sone != null) && !sone.getRootAlbum().getAlbums().isEmpty();
-               }
-       };
-
-       public static final Function<Sone, List<Album>> toAllAlbums = new Function<Sone, List<Album>>() {
-               @Override
-               public List<Album> apply(@Nullable Sone sone) {
-                       return (sone == null) ? Collections.<Album>emptyList() : FLATTENER.apply(
-                                       sone.getRootAlbum());
-               }
-       };
-
-       public static final Function<Sone, List<Image>> toAllImages = new Function<Sone, List<Image>>() {
-               @Override
-               public List<Image> apply(@Nullable Sone sone) {
-                       return (sone == null) ? Collections.<Image>emptyList() :
-                                       from(FLATTENER.apply(sone.getRootAlbum()))
-                                                       .transformAndConcat(IMAGES).toList();
-               }
-       };
-
        /**
         * Returns the identity of this Sone.
         *
@@ -192,14 +84,6 @@ public interface Sone extends Identified, Fingerprintable, Comparable<Sone> {
        FreenetURI getRequestUri();
 
        /**
-        * Returns the insert URI of this Sone.
-        *
-        * @return The insert URI of this Sone
-        */
-       @Nullable
-       FreenetURI getInsertUri();
-
-       /**
         * Returns the latest edition of this Sone.
         *
         * @return The latest edition of this Sone
index e06e5a7..ddd96b9 100644 (file)
@@ -53,11 +53,6 @@ public class IdOnlySone implements Sone {
        }
 
        @Override
-       public FreenetURI getInsertUri() {
-               return null;
-       }
-
-       @Override
        public long getLatestEdition() {
                return 0;
        }
index 1069550..f2f2ea6 100644 (file)
@@ -21,6 +21,9 @@ import static com.google.common.base.Preconditions.checkNotNull;
 import static java.lang.String.format;
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static java.util.logging.Logger.getLogger;
+import static net.pterodactylus.sone.data.PostKt.newestPostFirst;
+import static net.pterodactylus.sone.data.ReplyKt.newestReplyFirst;
+import static net.pterodactylus.sone.data.SoneKt.*;
 
 import java.net.MalformedURLException;
 import java.util.ArrayList;
@@ -36,17 +39,16 @@ import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 
 import net.pterodactylus.sone.data.Album;
+import net.pterodactylus.sone.data.AlbumKt;
 import net.pterodactylus.sone.data.Client;
 import net.pterodactylus.sone.data.Post;
 import net.pterodactylus.sone.data.PostReply;
 import net.pterodactylus.sone.data.Profile;
-import net.pterodactylus.sone.data.Reply;
 import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.data.SoneOptions;
 import net.pterodactylus.sone.data.SoneOptions.DefaultSoneOptions;
 import net.pterodactylus.sone.database.Database;
 import net.pterodactylus.sone.freenet.wot.Identity;
-import net.pterodactylus.sone.freenet.wot.OwnIdentity;
 
 import freenet.keys.FreenetURI;
 
@@ -191,26 +193,6 @@ public class SoneImpl implements Sone {
        }
 
        /**
-        * Returns the insert URI of this Sone.
-        *
-        * @return The insert URI of this Sone
-        */
-       @Nullable
-       public FreenetURI getInsertUri() {
-               if (!isLocal()) {
-                       return null;
-               }
-               try {
-                       return new FreenetURI(((OwnIdentity) getIdentity()).getInsertUri())
-                                       .setDocName("Sone")
-                                       .setMetaString(new String[0])
-                                       .setSuggestedEdition(latestEdition);
-               } catch (MalformedURLException e) {
-                       throw new IllegalStateException(format("Own identity %s's insert URI is incorrect.", getIdentity()), e);
-               }
-       }
-
-       /**
         * Returns the latest edition of this Sone.
         *
         * @return The latest edition of this Sone
@@ -384,7 +366,7 @@ public class SoneImpl implements Sone {
                synchronized (this) {
                        sortedPosts = new ArrayList<>(posts);
                }
-               Collections.sort(sortedPosts, Post.NEWEST_FIRST);
+               sortedPosts.sort(newestPostFirst());
                return sortedPosts;
        }
 
@@ -644,7 +626,7 @@ public class SoneImpl implements Sone {
                hash.putString(")", UTF_8);
 
                List<PostReply> replies = new ArrayList<>(getReplies());
-               Collections.sort(replies, Reply.TIME_COMPARATOR);
+               replies.sort(newestReplyFirst().reversed());
                hash.putString("Replies(", UTF_8);
                for (PostReply reply : replies) {
                        hash.putString("Reply(", UTF_8).putString(reply.getId(), UTF_8).putString(")", UTF_8);
@@ -669,7 +651,7 @@ public class SoneImpl implements Sone {
 
                hash.putString("Albums(", UTF_8);
                for (Album album : rootAlbum.getAlbums()) {
-                       if (!Album.NOT_EMPTY.apply(album)) {
+                       if (!AlbumKt.notEmpty().invoke(album)) {
                                continue;
                        }
                        hash.putString(album.getFingerprint(), UTF_8);
@@ -686,7 +668,7 @@ public class SoneImpl implements Sone {
        /** {@inheritDoc} */
        @Override
        public int compareTo(Sone sone) {
-               return NICE_NAME_COMPARATOR.compare(this, sone);
+               return niceNameComparator().compare(this, sone);
        }
 
        //
diff --git a/src/main/java/net/pterodactylus/sone/database/memory/MemoryDatabase.kt b/src/main/java/net/pterodactylus/sone/database/memory/MemoryDatabase.kt
deleted file mode 100644 (file)
index 8722873..0000000
+++ /dev/null
@@ -1,354 +0,0 @@
-/*
- * 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
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.sone.database.memory
-
-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.*
-import com.google.inject.Inject
-import com.google.inject.Singleton
-import net.pterodactylus.sone.data.Album
-import net.pterodactylus.sone.data.Image
-import net.pterodactylus.sone.data.Post
-import net.pterodactylus.sone.data.PostReply
-import net.pterodactylus.sone.data.Reply.TIME_COMPARATOR
-import net.pterodactylus.sone.data.Sone
-import net.pterodactylus.sone.data.Sone.toAllAlbums
-import net.pterodactylus.sone.data.Sone.toAllImages
-import net.pterodactylus.sone.data.impl.AlbumBuilderImpl
-import net.pterodactylus.sone.data.impl.ImageBuilderImpl
-import net.pterodactylus.sone.database.AlbumBuilder
-import net.pterodactylus.sone.database.Database
-import net.pterodactylus.sone.database.DatabaseException
-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.*
-import net.pterodactylus.util.config.Configuration
-import net.pterodactylus.util.config.ConfigurationException
-import java.util.concurrent.locks.ReentrantReadWriteLock
-import kotlin.concurrent.withLock
-
-/**
- * Memory-based [PostDatabase] implementation.
- */
-@Singleton
-class MemoryDatabase @Inject constructor(private val configuration: Configuration) : AbstractService(), Database {
-
-       private val lock = ReentrantReadWriteLock()
-       private val readLock by lazy { lock.readLock()!! }
-       private val writeLock by lazy { lock.writeLock()!! }
-       private val configurationLoader = ConfigurationLoader(configuration)
-       private val allSones = mutableMapOf<String, Sone>()
-       private val allPosts = mutableMapOf<String, Post>()
-       private val sonePosts: Multimap<String, Post> = HashMultimap.create<String, Post>()
-       private val knownPosts = mutableSetOf<String>()
-       private val allPostReplies = mutableMapOf<String, PostReply>()
-       private val sonePostReplies: Multimap<String, PostReply> = TreeMultimap.create<String, PostReply>(Comparator { leftString, rightString -> leftString.compareTo(rightString) }, TIME_COMPARATOR)
-       private val knownPostReplies = mutableSetOf<String>()
-       private val allAlbums = mutableMapOf<String, Album>()
-       private val soneAlbums: Multimap<String, Album> = HashMultimap.create<String, Album>()
-       private val allImages = mutableMapOf<String, Image>()
-       private val soneImages: Multimap<String, Image> = HashMultimap.create<String, Image>()
-       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
-
-       override val sones get() = readLock.withLock { allSones.values.toSet() }
-
-       override val localSones get() = readLock.withLock { allSones.values.filter(Sone::isLocal) }
-
-       override val remoteSones get() = readLock.withLock { allSones.values.filterNot(Sone::isLocal) }
-
-       override val bookmarkedPosts get() = memoryBookmarkDatabase.bookmarkedPosts
-
-       override fun save() {
-               if (saveRateLimiter.tryAcquire()) {
-                       saveKnownPosts()
-                       saveKnownPostReplies()
-               }
-       }
-
-       override fun doStart() {
-               memoryBookmarkDatabase.start()
-               loadKnownPosts()
-               loadKnownPostReplies()
-               notifyStarted()
-       }
-
-       override fun doStop() {
-               try {
-                       memoryBookmarkDatabase.stop()
-                       save()
-                       notifyStopped()
-               } catch (de1: DatabaseException) {
-                       notifyFailed(de1)
-               }
-       }
-
-       override fun newSoneBuilder() = MemorySoneBuilder(this)
-
-       override fun storeSone(sone: Sone) {
-               writeLock.withLock {
-                       removeSone(sone)
-
-                       allSones[sone.id] = sone
-                       sonePosts.putAll(sone.id, sone.posts)
-                       for (post in sone.posts) {
-                               allPosts[post.id] = post
-                       }
-                       sonePostReplies.putAll(sone.id, sone.replies)
-                       for (postReply in sone.replies) {
-                               allPostReplies[postReply.id] = postReply
-                       }
-                       soneAlbums.putAll(sone.id, toAllAlbums.apply(sone)!!)
-                       for (album in toAllAlbums.apply(sone)!!) {
-                               allAlbums[album.id] = album
-                       }
-                       soneImages.putAll(sone.id, toAllImages.apply(sone)!!)
-                       for (image in toAllImages.apply(sone)!!) {
-                               allImages[image.id] = image
-                       }
-               }
-       }
-
-       override fun removeSone(sone: Sone) {
-               writeLock.withLock {
-                       allSones.remove(sone.id)
-                       val removedPosts = sonePosts.removeAll(sone.id)
-                       for (removedPost in removedPosts) {
-                               allPosts.remove(removedPost.id)
-                       }
-                       val removedPostReplies = sonePostReplies.removeAll(sone.id)
-                       for (removedPostReply in removedPostReplies) {
-                               allPostReplies.remove(removedPostReply.id)
-                       }
-                       val removedAlbums = soneAlbums.removeAll(sone.id)
-                       for (removedAlbum in removedAlbums) {
-                               allAlbums.remove(removedAlbum.id)
-                       }
-                       val removedImages = soneImages.removeAll(sone.id)
-                       for (removedImage in removedImages) {
-                               allImages.remove(removedImage.id)
-                       }
-               }
-       }
-
-       override fun getSone(soneId: String) = readLock.withLock { allSones[soneId] }
-
-       override fun getFriends(localSone: Sone): Collection<String> =
-                       if (!localSone.isLocal) {
-                               emptySet()
-                       } else {
-                               memoryFriendDatabase.getFriends(localSone.id)
-                       }
-
-       override fun isFriend(localSone: Sone, friendSoneId: String) =
-                       if (!localSone.isLocal) {
-                               false
-                       } else {
-                               memoryFriendDatabase.isFriend(localSone.id, friendSoneId)
-                       }
-
-       override fun addFriend(localSone: Sone, friendSoneId: String) {
-               if (!localSone.isLocal) {
-                       return
-               }
-               memoryFriendDatabase.addFriend(localSone.id, friendSoneId)
-       }
-
-       override fun removeFriend(localSone: Sone, friendSoneId: String) {
-               if (!localSone.isLocal) {
-                       return
-               }
-               memoryFriendDatabase.removeFriend(localSone.id, friendSoneId)
-       }
-
-       override fun getFollowingTime(friendSoneId: String) =
-                       memoryFriendDatabase.getFollowingTime(friendSoneId)
-
-       override fun getPost(postId: String) =
-                       readLock.withLock { allPosts[postId] }
-
-       override fun getPosts(soneId: String): Collection<Post> =
-                       sonePosts[soneId].toSet()
-
-       override fun getDirectedPosts(recipientId: String) =
-                       readLock.withLock {
-                               allPosts.values.filter {
-                                       it.recipientId.orNull() == recipientId
-                               }
-                       }
-
-       override fun newPostBuilder(): PostBuilder = MemoryPostBuilder(this, this)
-
-       override fun storePost(post: Post) {
-               checkNotNull(post, "post must not be null")
-               writeLock.withLock {
-                       allPosts[post.id] = post
-                       sonePosts[post.sone.id].add(post)
-               }
-       }
-
-       override fun removePost(post: Post) {
-               checkNotNull(post, "post must not be null")
-               writeLock.withLock {
-                       allPosts.remove(post.id)
-                       sonePosts[post.sone.id].remove(post)
-                       post.sone.removePost(post)
-               }
-       }
-
-       override fun getPostReply(id: String) = readLock.withLock { allPostReplies[id] }
-
-       override fun getReplies(postId: String) =
-                       readLock.withLock {
-                               allPostReplies.values
-                                               .filter { it.postId == postId }
-                                               .sortedWith(TIME_COMPARATOR)
-                       }
-
-       override fun newPostReplyBuilder(): PostReplyBuilder =
-                       MemoryPostReplyBuilder(this, this)
-
-       override fun storePostReply(postReply: PostReply) =
-                       writeLock.withLock {
-                               allPostReplies[postReply.id] = postReply
-                       }
-
-       override fun removePostReply(postReply: PostReply) =
-                       writeLock.withLock {
-                               allPostReplies.remove(postReply.id)
-                       }.unit
-
-       override fun getAlbum(albumId: String) = readLock.withLock { allAlbums[albumId] }
-
-       override fun newAlbumBuilder(): AlbumBuilder = AlbumBuilderImpl()
-
-       override fun storeAlbum(album: Album) =
-                       writeLock.withLock {
-                               allAlbums[album.id] = album
-                               soneAlbums.put(album.sone.id, album)
-                       }.unit
-
-       override fun removeAlbum(album: Album) =
-                       writeLock.withLock {
-                               allAlbums.remove(album.id)
-                               soneAlbums.remove(album.sone.id, album)
-                       }.unit
-
-       override fun getImage(imageId: String) = readLock.withLock { allImages[imageId] }
-
-       override fun newImageBuilder(): ImageBuilder = ImageBuilderImpl()
-
-       override fun storeImage(image: Image): Unit =
-                       writeLock.withLock {
-                               allImages[image.id] = image
-                               soneImages.put(image.sone.id, image)
-                       }
-
-       override fun removeImage(image: Image): Unit =
-                       writeLock.withLock {
-                               allImages.remove(image.id)
-                               soneImages.remove(image.sone.id, image)
-                       }
-
-       override fun bookmarkPost(post: Post) =
-                       memoryBookmarkDatabase.bookmarkPost(post)
-
-       override fun unbookmarkPost(post: Post) =
-                       memoryBookmarkDatabase.unbookmarkPost(post)
-
-       override fun isPostBookmarked(post: Post) =
-                       memoryBookmarkDatabase.isPostBookmarked(post)
-
-       protected fun isPostKnown(post: Post) = readLock.withLock { post.id in knownPosts }
-
-       fun setPostKnown(post: Post, known: Boolean): Unit =
-                       writeLock.withLock {
-                               if (known)
-                                       knownPosts.add(post.id)
-                               else
-                                       knownPosts.remove(post.id)
-                               saveKnownPosts()
-                       }
-
-       protected fun isPostReplyKnown(postReply: PostReply) = readLock.withLock { postReply.id in knownPostReplies }
-
-       fun setPostReplyKnown(postReply: PostReply, known: Boolean): Unit =
-                       writeLock.withLock {
-                               if (known)
-                                       knownPostReplies.add(postReply.id)
-                               else
-                                       knownPostReplies.remove(postReply.id)
-                               saveKnownPostReplies()
-                       }
-
-       private fun loadKnownPosts() =
-                       configurationLoader.loadKnownPosts()
-                                       .let {
-                                               writeLock.withLock {
-                                                       knownPosts.clear()
-                                                       knownPosts.addAll(it)
-                                               }
-                                       }
-
-       private fun saveKnownPosts() =
-                       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
-                                       }
-                               } catch (ce1: ConfigurationException) {
-                                       throw DatabaseException("Could not save database.", ce1)
-                               }
-                       }
-
-       private fun loadKnownPostReplies(): Unit =
-                       configurationLoader.loadKnownPostReplies().let { knownPostReplies ->
-                               writeLock.withLock {
-                                       this.knownPostReplies.clear()
-                                       this.knownPostReplies.addAll(knownPostReplies)
-                               }
-                       }
-
-       private fun saveKnownPostReplies() =
-                       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
-                                       }
-                               } catch (ce1: ConfigurationException) {
-                                       throw DatabaseException("Could not save database.", ce1)
-                               }
-                       }
-
-}
index e4a8f30..5764622 100644 (file)
@@ -124,15 +124,6 @@ class MemoryPostReply implements PostReply {
                return database.isPostReplyKnown(this);
        }
 
-       /**
-        * {@inheritDocs}
-        */
-       @Override
-       public PostReply setKnown(boolean known) {
-               database.setPostReplyKnown(this, known);
-               return this;
-       }
-
        //
        // POSTREPLY METHODS
        //
@@ -177,4 +168,17 @@ class MemoryPostReply implements PostReply {
                return memoryPostReply.id.equals(id);
        }
 
+       @Override
+       public String toString() {
+               return "MemoryPostReply{" +
+                               "database=" + database +
+                               ", soneProvider=" + soneProvider +
+                               ", id='" + id + '\'' +
+                               ", soneId='" + soneId + '\'' +
+                               ", time=" + time +
+                               ", text='" + text + '\'' +
+                               ", postId='" + postId + '\'' +
+                               '}';
+       }
+
 }
index 39a6a1e..3cb1d6b 100644 (file)
@@ -17,8 +17,6 @@
 
 package net.pterodactylus.sone.fcp;
 
-import com.google.common.base.Optional;
-
 import net.pterodactylus.sone.core.Core;
 import net.pterodactylus.sone.data.Post;
 import net.pterodactylus.sone.data.Sone;
index 821c198..05c2349 100644 (file)
@@ -32,6 +32,9 @@ import com.google.common.collect.Collections2;
 
 import freenet.support.SimpleFieldSet;
 
+import static net.pterodactylus.sone.data.PostKt.newestPostFirst;
+import static net.pterodactylus.sone.data.PostKt.noFuturePost;
+
 /**
  * Implementation of an FCP interface for other clients or plugins to
  * communicate with Sone.
@@ -67,10 +70,10 @@ public class GetPostFeedCommand extends AbstractSoneCommand {
                        allPosts.addAll(friendSone.getPosts());
                }
                allPosts.addAll(getCore().getDirectedPosts(sone.getId()));
-               allPosts = Collections2.filter(allPosts, Post.FUTURE_POSTS_FILTER);
+               allPosts = Collections2.filter(allPosts, noFuturePost()::invoke);
 
                List<Post> sortedPosts = new ArrayList<>(allPosts);
-               Collections.sort(sortedPosts, Post.NEWEST_FIRST);
+               sortedPosts.sort(newestPostFirst());
 
                if (sortedPosts.size() < startPost) {
                        return new Response("PostFeed", encodePosts(Collections.<Post> emptyList(), "Posts.", false));
index 83167cd..eedf120 100644 (file)
@@ -17,6 +17,7 @@
 
 package net.pterodactylus.sone.fcp;
 
+import static net.pterodactylus.sone.data.SoneKt.*;
 import static net.pterodactylus.sone.fcp.AbstractSoneCommandKt.encodeSones;
 
 import java.util.ArrayList;
@@ -24,7 +25,8 @@ import java.util.Collections;
 import java.util.List;
 
 import net.pterodactylus.sone.core.Core;
-import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.data.*;
+
 import freenet.support.SimpleFieldSet;
 
 /**
@@ -53,7 +55,7 @@ public class GetSonesCommand extends AbstractSoneCommand {
                if (sones.size() < startSone) {
                        return new Response("Sones", encodeSones(Collections.<Sone> emptyList(), "Sones."));
                }
-               Collections.sort(sones, Sone.NICE_NAME_COMPARATOR);
+               sones.sort(niceNameComparator());
                return new Response("Sones", encodeSones(sones.subList(startSone, (maxSones == -1) ? sones.size() : Math.min(startSone + maxSones, sones.size())), "Sones."));
        }
 
index b8d4e6d..e6f4f62 100644 (file)
 
 package net.pterodactylus.sone.freenet.wot;
 
-import java.util.Collection;
-import java.util.Collections;
 import java.util.Map;
 import java.util.Set;
 
-import com.google.common.base.Function;
-
 /**
  * Interface for web of trust identities, defining all functions that can be
  * performed on an identity. An identity is only a container for identity data
@@ -135,6 +131,8 @@ public interface Identity {
         */
        public Identity removeProperty(String name);
 
+       Map<OwnIdentity, Trust> getTrust();
+
        /**
         * Retrieves the trust that this identity receives from the given own
         * identity. If this identity is not in the own identity’s trust tree, a
diff --git a/src/main/java/net/pterodactylus/sone/freenet/wot/IdentityChangeDetector.kt b/src/main/java/net/pterodactylus/sone/freenet/wot/IdentityChangeDetector.kt
deleted file mode 100644 (file)
index ffcafb3..0000000
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- * 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 <http://www.gnu.org/licenses/>.
- */
-
-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<Identity>) {
-
-       private val oldIdentities: Map<String, Identity> = oldIdentities.associateBy { it.id }
-       var onNewIdentity: IdentityProcessor? = null
-       var onRemovedIdentity: IdentityProcessor? = null
-       var onChangedIdentity: IdentityProcessor? = null
-       var onUnchangedIdentity: IdentityProcessor? = null
-
-       fun detectChanges(newIdentities: Collection<Identity>) {
-               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<Identity>) =
-               this?.let { identities.forEach(this::invoke) }
index 2a2bd8c..5072970 100644 (file)
@@ -1,13 +1,10 @@
 package net.pterodactylus.sone.main;
 
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.Reader;
-import java.io.UnsupportedEncodingException;
+import java.io.*;
+
 import javax.annotation.Nonnull;
 
 import net.pterodactylus.sone.web.WebInterface;
-import net.pterodactylus.util.io.Closer;
 import net.pterodactylus.util.template.ClassPathTemplateProvider;
 import net.pterodactylus.util.template.Template;
 import net.pterodactylus.util.template.TemplateProvider;
@@ -25,17 +22,11 @@ public class DefaultLoaders implements Loaders {
        @Nonnull
        @Override
        public Template loadTemplate(@Nonnull String path) {
-               InputStream templateInputStream = null;
-               Reader reader = null;
-               try {
-                       templateInputStream = getClass().getResourceAsStream(path);
-                       reader = new InputStreamReader(templateInputStream, "UTF-8");
+               try (InputStream templateInputStream = getClass().getResourceAsStream(path);
+                               Reader reader = new InputStreamReader(templateInputStream, "UTF-8");) {
                        return parse(reader);
-               } catch (UnsupportedEncodingException uee1) {
+               } catch (IOException ioe1) {
                        throw new RuntimeException("UTF-8 not supported.");
-               } finally {
-                       Closer.close(reader);
-                       Closer.close(templateInputStream);
                }
        }
 
index 25c6dac..82ab065 100644 (file)
@@ -42,7 +42,6 @@ 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.name.*;
 import kotlin.jvm.functions.*;
 
@@ -97,7 +96,7 @@ public class SonePlugin implements FredPlugin, FredPluginFCP, FredPluginL10n, Fr
        /** The current year at time of release. */
        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 = 80;
+       private static final int LATEST_EDITION = 81;
 
        /** The logger. */
        private static final Logger logger = getLogger(SonePlugin.class.getName());
diff --git a/src/main/java/net/pterodactylus/sone/main/SonePlugin.kt b/src/main/java/net/pterodactylus/sone/main/SonePlugin.kt
deleted file mode 100644 (file)
index 5e0b2c1..0000000
+++ /dev/null
@@ -1,7 +0,0 @@
-package net.pterodactylus.sone.main
-
-data class PluginVersion(val version: String)
-
-data class PluginYear(val year: Int)
-
-data class PluginHomepage(val homepage: String)
index f170701..f960b08 100644 (file)
 
 package net.pterodactylus.sone.template;
 
+import static net.pterodactylus.sone.data.SoneKt.*;
+
 import java.util.ArrayList;
 import java.util.Collection;
-import java.util.Collections;
 import java.util.List;
 
-import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.data.*;
 import net.pterodactylus.util.template.Accessor;
 import net.pterodactylus.util.template.ReflectionAccessor;
 import net.pterodactylus.util.template.TemplateContext;
@@ -52,7 +53,7 @@ public class CollectionAccessor extends ReflectionAccessor {
                                }
                                sones.add((Sone) sone);
                        }
-                       Collections.sort(sones, Sone.NICE_NAME_COMPARATOR);
+                       sones.sort(niceNameComparator());
                        StringBuilder soneNames = new StringBuilder();
                        for (Sone sone : sones) {
                                if (soneNames.length() > 0) {
index 4bc6a5f..b072dc9 100644 (file)
 
 package net.pterodactylus.sone.template;
 
-import static com.google.common.collect.FluentIterable.from;
-import static java.util.Arrays.asList;
 import static java.util.logging.Logger.getLogger;
-import static net.pterodactylus.sone.data.Album.FLATTENER;
-import static net.pterodactylus.sone.data.Album.IMAGES;
 
 import java.util.logging.Level;
 import java.util.logging.Logger;
@@ -30,6 +26,7 @@ import net.pterodactylus.sone.core.Core;
 import net.pterodactylus.sone.data.Profile;
 import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.data.Sone.SoneStatus;
+import net.pterodactylus.sone.data.SoneKt;
 import net.pterodactylus.sone.freenet.wot.OwnIdentity;
 import net.pterodactylus.sone.freenet.wot.Trust;
 import net.pterodactylus.sone.text.TimeTextConverter;
@@ -116,7 +113,7 @@ public class SoneAccessor extends ReflectionAccessor {
                        }
                        return trust;
                } else if (member.equals("allImages")) {
-                       return from(asList(sone.getRootAlbum())).transformAndConcat(FLATTENER).transformAndConcat(IMAGES);
+                       return SoneKt.getAllImages(sone);
                } else if (member.equals("albums")) {
                        return sone.getRootAlbum().getAlbums();
                }
index d3a1557..598765a 100644 (file)
@@ -18,7 +18,6 @@
 package net.pterodactylus.sone.text;
 
 import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.sone.web.page.FreenetRequest;
 
 /**
  * {@link ParserContext} implementation for the {@link SoneTextParser}. It
diff --git a/src/main/java/net/pterodactylus/sone/utils/DefaultOption.java b/src/main/java/net/pterodactylus/sone/utils/DefaultOption.java
deleted file mode 100644 (file)
index d9acaeb..0000000
+++ /dev/null
@@ -1,83 +0,0 @@
-package net.pterodactylus.sone.utils;
-
-import com.google.common.base.Predicate;
-
-/**
- * Basic implementation of an {@link Option}.
- *
- * @param <T>
- *            The type of the option
- */
-public class DefaultOption<T> implements Option<T> {
-
-       /** The default value. */
-       private final T defaultValue;
-
-       /** The current value. */
-       private volatile T value;
-
-       /** The validator. */
-       private Predicate<T> validator;
-
-       /**
-        * Creates a new default option.
-        *
-        * @param defaultValue
-        *            The default value of the option
-        */
-       public DefaultOption(T defaultValue) {
-               this(defaultValue, null);
-       }
-
-       /**
-        * Creates a new default option.
-        *
-        * @param defaultValue
-        *            The default value of the option
-        * @param validator
-        *            The validator for value validation (may be {@code null})
-        */
-       public DefaultOption(T defaultValue, Predicate<T> validator) {
-               this.defaultValue = defaultValue;
-               this.validator = validator;
-       }
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       public T get() {
-               return (value != null) ? value : defaultValue;
-       }
-
-       /**
-        * Returns the real value of the option. This will also return an unset
-        * value (usually {@code null})!
-        *
-        * @return The real value of the option
-        */
-       @Override
-       public T getReal() {
-               return value;
-       }
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       public boolean validate(T value) {
-               return (validator == null) || (value == null) || validator.apply(value);
-       }
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       public void set(T value) {
-               if ((value != null) && (validator != null) && (!validator.apply(value))) {
-                       throw new IllegalArgumentException("New Value (" + value + ") could not be validated.");
-               }
-               this.value = value;
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/utils/IntegerRangePredicate.java b/src/main/java/net/pterodactylus/sone/utils/IntegerRangePredicate.java
deleted file mode 100644 (file)
index 3a631cf..0000000
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- * 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
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.sone.utils;
-
-import com.google.common.base.Predicate;
-
-/**
- * {@link Predicate} that verifies that an {@link Integer} value is not
- * {@code null} and is between a lower and an upper bound. Both bounds are
- * inclusive.
- */
-public class IntegerRangePredicate implements Predicate<Integer> {
-
-       /** The lower bound. */
-       private final int lowerBound;
-
-       /** The upper bound. */
-       private final int upperBound;
-
-       /**
-        * Creates a new integer range predicate.
-        *
-        * @param lowerBound
-        *            The lower bound
-        * @param upperBound
-        *            The upper bound
-        */
-       public IntegerRangePredicate(int lowerBound, int upperBound) {
-               this.lowerBound = lowerBound;
-               this.upperBound = upperBound;
-       }
-
-       //
-       // PREDICATE METHODS
-       //
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       public boolean apply(Integer value) {
-               return (value != null) && (value >= lowerBound) && (value <= upperBound);
-       }
-
-       public static IntegerRangePredicate range(int lowerBound, int upperBound) {
-               return new IntegerRangePredicate(lowerBound, upperBound);
-       }
-
-}
index 471fc26..d9091c0 100644 (file)
@@ -1,6 +1,5 @@
 package net.pterodactylus.sone.utils;
 
-import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 
 import com.google.common.primitives.Ints;
diff --git a/src/main/java/net/pterodactylus/sone/web/AllPages.kt b/src/main/java/net/pterodactylus/sone/web/AllPages.kt
deleted file mode 100644 (file)
index c538f0e..0000000
+++ /dev/null
@@ -1,52 +0,0 @@
-package net.pterodactylus.sone.web
-
-import net.pterodactylus.sone.web.pages.*
-import javax.inject.Inject
-
-/**
- * Container for all web pages. This uses field injection because there are way too many pages
- * to sensibly use constructor injection.
- */
-class AllPages {
-
-       @Inject lateinit var aboutPage: AboutPage
-       @Inject lateinit var bookmarkPage: BookmarkPage
-       @Inject lateinit var bookmarksPage: BookmarksPage
-       @Inject lateinit var createAlbumPage: CreateAlbumPage
-       @Inject lateinit var createPostPage: CreatePostPage
-       @Inject lateinit var createReplyPage: CreateReplyPage
-       @Inject lateinit var createSonePage: CreateSonePage
-       @Inject lateinit var deleteAlbumPage: DeleteAlbumPage
-       @Inject lateinit var deleteImagePage: DeleteImagePage
-       @Inject lateinit var deletePostPage: DeletePostPage
-       @Inject lateinit var deleteProfileFieldPage: DeleteProfileFieldPage
-       @Inject lateinit var deleteReplyPage: DeleteReplyPage
-       @Inject lateinit var deleteSonePage: DeleteSonePage
-       @Inject lateinit var dismissNotificationPage: DismissNotificationPage
-       @Inject lateinit var editAlbumPage: EditAlbumPage
-       @Inject lateinit var editImagePage: EditImagePage
-       @Inject lateinit var editProfileFieldPage: EditProfileFieldPage
-       @Inject lateinit var editProfilePage: EditProfilePage
-       @Inject lateinit var followSonePage: FollowSonePage
-       @Inject lateinit var getImagePage: GetImagePage
-       @Inject lateinit var imageBrowserPage: ImageBrowserPage
-       @Inject lateinit var indexPage: IndexPage
-       @Inject lateinit var knownSonesPage: KnownSonesPage
-       @Inject lateinit var likePage: LikePage
-       @Inject lateinit var lockSonePage: LockSonePage
-       @Inject lateinit var loginPage: LoginPage
-       @Inject lateinit var logoutPage: LogoutPage
-       @Inject lateinit var markAsKnownPage: MarkAsKnownPage
-       @Inject lateinit var newPage: NewPage
-       @Inject lateinit var optionsPage: OptionsPage
-       @Inject lateinit var rescuePage: RescuePage
-       @Inject lateinit var searchPage: SearchPage
-       @Inject lateinit var unbookmarkPage: UnbookmarkPage
-       @Inject lateinit var unfollowSonePage: UnfollowSonePage
-       @Inject lateinit var unlikePage: UnlikePage
-       @Inject lateinit var unlockSonePage: UnlockSonePage
-       @Inject lateinit var uploadImagePage: UploadImagePage
-       @Inject lateinit var viewPostPage: ViewPostPage
-       @Inject lateinit var viewSonePage: ViewSonePage
-
-}
index 5448054..40e5708 100644 (file)
@@ -23,7 +23,6 @@ import static java.util.logging.Logger.getLogger;
 import java.util.Collection;
 import java.util.Set;
 import java.util.TimeZone;
-import java.util.UUID;
 import java.util.logging.Logger;
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
@@ -88,15 +87,12 @@ import net.pterodactylus.util.template.TemplateContextFactory;
 import net.pterodactylus.util.web.RedirectPage;
 import net.pterodactylus.util.web.TemplatePage;
 
-import freenet.clients.http.SessionManager;
-import freenet.clients.http.SessionManager.Session;
-import freenet.clients.http.ToadletContext;
-
 import com.codahale.metrics.*;
 import com.google.common.base.Optional;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.eventbus.Subscribe;
 import com.google.inject.Inject;
+import freenet.clients.http.ToadletContext;
 
 /**
  * Bundles functionality that a web interface of a Freenet plugin needs, e.g.
@@ -140,6 +136,7 @@ public class WebInterface implements SessionProvider {
        private final PageToadletRegistry pageToadletRegistry;
        private final MetricRegistry metricRegistry;
        private final Translation translation;
+       private final SessionProvider sessionProvider;
 
        /** The “new post” notification. */
        private final ListNotification<Post> newPostNotification;
@@ -162,7 +159,8 @@ public class WebInterface implements SessionProvider {
                        RenderFilter renderFilter,
                        LinkedElementRenderFilter linkedElementRenderFilter,
                        PageToadletRegistry pageToadletRegistry, MetricRegistry metricRegistry, Translation translation, L10nFilter l10nFilter,
-                       NotificationManager notificationManager, @Named("newRemotePost") ListNotification<Post> newPostNotification,
+                       NotificationManager notificationManager, SessionProvider sessionProvider,
+                       @Named("newRemotePost") ListNotification<Post> newPostNotification,
                        @Named("newRemotePostReply") ListNotification<PostReply> newReplyNotification,
                        @Named("localPost") ListNotification<Post> localPostNotification,
                        @Named("localReply") ListNotification<PostReply> localReplyNotification) {
@@ -182,6 +180,7 @@ public class WebInterface implements SessionProvider {
                this.l10nFilter = l10nFilter;
                this.translation = translation;
                this.notificationManager = notificationManager;
+               this.sessionProvider = sessionProvider;
                this.newPostNotification = newPostNotification;
                this.newReplyNotification = newReplyNotification;
                this.localPostNotification = localPostNotification;
@@ -216,75 +215,15 @@ public class WebInterface implements SessionProvider {
                return templateContextFactory;
        }
 
-       private Session getCurrentSessionWithoutCreation(ToadletContext toadletContenxt) {
-               return getSessionManager().useSession(toadletContenxt);
-       }
-
-       private Session getOrCreateCurrentSession(ToadletContext toadletContenxt) {
-               Session session = getCurrentSessionWithoutCreation(toadletContenxt);
-               if (session == null) {
-                       session = getSessionManager().createSession(UUID.randomUUID().toString(), toadletContenxt);
-               }
-               return session;
-       }
-
-       public Sone getCurrentSoneCreatingSession(ToadletContext toadletContext) {
-               Collection<Sone> localSones = getCore().getLocalSones();
-               if (localSones.size() == 1) {
-                       return localSones.iterator().next();
-               }
-               return getCurrentSone(getOrCreateCurrentSession(toadletContext));
-       }
-
-       public Sone getCurrentSoneWithoutCreatingSession(ToadletContext toadletContext) {
-               Collection<Sone> localSones = getCore().getLocalSones();
-               if (localSones.size() == 1) {
-                       return localSones.iterator().next();
-               }
-               return getCurrentSone(getCurrentSessionWithoutCreation(toadletContext));
-       }
-
-       /**
-        * Returns the currently logged in Sone.
-        *
-        * @param session
-        *            The session
-        * @return The currently logged in Sone, or {@code null} if no Sone is
-        *         currently logged in
-        */
-       private Sone getCurrentSone(Session session) {
-               if (session == null) {
-                       return null;
-               }
-               String soneId = (String) session.getAttribute("Sone.CurrentSone");
-               if (soneId == null) {
-                       return null;
-               }
-               return getCore().getLocalSone(soneId);
-       }
-
-       @Override
        @Nullable
-       public Sone getCurrentSone(@Nonnull ToadletContext toadletContext, boolean createSession) {
-               return createSession ? getCurrentSoneCreatingSession(toadletContext) : getCurrentSoneWithoutCreatingSession(toadletContext);
+       @Override
+       public Sone getCurrentSone(@Nonnull ToadletContext toadletContext) {
+               return sessionProvider.getCurrentSone(toadletContext);
        }
 
-       /**
-        * Sets the currently logged in Sone.
-        *
-        * @param toadletContext
-        *            The toadlet context
-        * @param sone
-        *            The Sone to set as currently logged in
-        */
        @Override
        public void setCurrentSone(@Nonnull ToadletContext toadletContext, @Nullable Sone sone) {
-               Session session = getOrCreateCurrentSession(toadletContext);
-               if (sone == null) {
-                       session.removeAttribute("Sone.CurrentSone");
-               } else {
-                       session.setAttribute("Sone.CurrentSone", sone.getId());
-               }
+               sessionProvider.setCurrentSone(toadletContext, sone);
        }
 
        /**
@@ -311,15 +250,6 @@ public class WebInterface implements SessionProvider {
        }
 
        /**
-        * Returns the session manager of the node.
-        *
-        * @return The node’s session manager
-        */
-       public SessionManager getSessionManager() {
-               return sonePlugin.pluginRespirator().getSessionManager("Sone");
-       }
-
-       /**
         * Returns the node’s form password.
         *
         * @return The form password
diff --git a/src/main/java/net/pterodactylus/sone/web/page/PageToadlet.java b/src/main/java/net/pterodactylus/sone/web/page/PageToadlet.java
deleted file mode 100644 (file)
index 45d6fff..0000000
+++ /dev/null
@@ -1,187 +0,0 @@
-/*
- * 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
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.sone.web.page;
-
-import java.io.IOException;
-import java.io.OutputStream;
-import java.net.URI;
-
-import net.pterodactylus.sone.utils.AutoCloseableBucket;
-import net.pterodactylus.util.web.Header;
-import net.pterodactylus.util.web.Method;
-import net.pterodactylus.util.web.Page;
-import net.pterodactylus.util.web.Response;
-
-import freenet.client.HighLevelSimpleClient;
-import freenet.clients.http.LinkEnabledCallback;
-import freenet.clients.http.LinkFilterExceptedToadlet;
-import freenet.clients.http.SessionManager;
-import freenet.clients.http.Toadlet;
-import freenet.clients.http.ToadletContext;
-import freenet.clients.http.ToadletContextClosedException;
-import freenet.support.MultiValueTable;
-import freenet.support.api.HTTPRequest;
-
-/**
- * {@link Toadlet} implementation that is wrapped around a {@link Page}.
- */
-public class PageToadlet extends Toadlet implements LinkEnabledCallback, LinkFilterExceptedToadlet {
-
-       private final SessionManager sessionManager;
-
-       /** The name of the menu item. */
-       private final String menuName;
-
-       /** The page that handles processing. */
-       private final Page<FreenetRequest> page;
-
-       /** The path prefix for the page. */
-       private final String pathPrefix;
-
-       /**
-        * Creates a new toadlet that hands off processing to a {@link Page}.
-        *
-        * @param highLevelSimpleClient
-        *            The high-level simple client
-        * @param menuName
-        *            The name of the menu item
-        * @param page
-        *            The page to handle processing
-        * @param pathPrefix
-        *            Prefix that is prepended to all {@link Page#getPath()} return
-        *            values
-        */
-       protected PageToadlet(HighLevelSimpleClient highLevelSimpleClient, SessionManager sessionManager, String menuName, Page<FreenetRequest> page, String pathPrefix) {
-               super(highLevelSimpleClient);
-               this.sessionManager = sessionManager;
-               this.menuName = menuName;
-               this.page = page;
-               this.pathPrefix = pathPrefix;
-       }
-
-       /**
-        * Returns the name to display in the menu.
-        *
-        * @return The name in the menu
-        */
-       public String getMenuName() {
-               return menuName;
-       }
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       public String path() {
-               return pathPrefix + page.getPath();
-       }
-
-       /**
-        * Handles a HTTP GET request.
-        *
-        * @param uri
-        *            The URI of the request
-        * @param httpRequest
-        *            The HTTP request
-        * @param toadletContext
-        *            The toadlet context
-        * @throws IOException
-        *             if an I/O error occurs
-        * @throws ToadletContextClosedException
-        *             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, sessionManager));
-       }
-
-       /**
-        * Handles a HTTP POST request.
-        *
-        * @param uri
-        *            The URI of the request
-        * @param httpRequest
-        *            The HTTP request
-        * @param toadletContext
-        *            The toadlet context
-        * @throws IOException
-        *             if an I/O error occurs
-        * @throws ToadletContextClosedException
-        *             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, sessionManager));
-       }
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       public String toString() {
-               return getClass().getName() + "[path=" + path() + ",page=" + page + "]";
-       }
-
-       /**
-        * Handles a HTTP request.
-        *
-        * @param pageRequest
-        *            The request to handle
-        * @throws IOException
-        *             if an I/O error occurs
-        * @throws ToadletContextClosedException
-        *             if the toadlet context is closed
-        */
-       private void handleRequest(FreenetRequest pageRequest) throws IOException, ToadletContextClosedException {
-               try (AutoCloseableBucket pageBucket = new AutoCloseableBucket(pageRequest.getToadletContext().getBucketFactory().makeBucket(-1));
-                    OutputStream pageBucketOutputStream = pageBucket.getBucket().getOutputStream()) {
-                       Response pageResponse = page.handleRequest(pageRequest, new Response(pageBucketOutputStream));
-                       MultiValueTable<String, String> headers = new MultiValueTable<>();
-                       if (pageResponse.getHeaders() != null) {
-                               for (Header header : pageResponse.getHeaders()) {
-                                       for (String value : header) {
-                                               headers.put(header.getName(), value);
-                                       }
-                               }
-                       }
-                       writeReply(pageRequest.getToadletContext(), pageResponse.getStatusCode(), pageResponse.getContentType(), pageResponse.getStatusText(), headers, pageBucket.getBucket());
-               }
-       }
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       public boolean isEnabled(ToadletContext toadletContext) {
-               if (page instanceof LinkEnabledCallback) {
-                       return ((LinkEnabledCallback) page).isEnabled(toadletContext);
-               }
-               return true;
-       }
-
-       //
-       // LINKFILTEREXCEPTEDTOADLET METHODS
-       //
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       public boolean isLinkExcepted(URI link) {
-               return (page instanceof FreenetPage) && ((FreenetPage) page).isLinkExcepted(link);
-       }
-
-}
diff --git a/src/main/kotlin/net/pterodactylus/sone/core/PreferenceChangedEvent.kt b/src/main/kotlin/net/pterodactylus/sone/core/PreferenceChangedEvent.kt
new file mode 100644 (file)
index 0000000..2ebb62d
--- /dev/null
@@ -0,0 +1,3 @@
+package net.pterodactylus.sone.core
+
+data class PreferenceChangedEvent(val preferenceName: String, val newValue: Any)
index 05b3279..fe1e3c3 100644 (file)
 
 package net.pterodactylus.sone.core
 
-import com.google.common.base.Predicates.*
-import com.google.common.eventbus.*
-import net.pterodactylus.sone.core.event.*
-import net.pterodactylus.sone.fcp.FcpInterface.*
-import net.pterodactylus.sone.fcp.FcpInterface.FullAccessRequired.*
-import net.pterodactylus.sone.fcp.event.*
-import net.pterodactylus.sone.utils.*
-import net.pterodactylus.sone.utils.IntegerRangePredicate.*
-import net.pterodactylus.util.config.*
-import java.lang.Integer.*
+import com.google.common.eventbus.EventBus
+import net.pterodactylus.sone.core.event.InsertionDelayChangedEvent
+import net.pterodactylus.sone.core.event.StrictFilteringActivatedEvent
+import net.pterodactylus.sone.core.event.StrictFilteringDeactivatedEvent
+import net.pterodactylus.sone.fcp.FcpInterface.FullAccessRequired
+import net.pterodactylus.sone.fcp.FcpInterface.FullAccessRequired.ALWAYS
+import net.pterodactylus.sone.fcp.event.FcpInterfaceActivatedEvent
+import net.pterodactylus.sone.fcp.event.FcpInterfaceDeactivatedEvent
+import net.pterodactylus.sone.fcp.event.FullAccessRequiredChanged
+import net.pterodactylus.sone.utils.DefaultOption
+import net.pterodactylus.util.config.Configuration
+import net.pterodactylus.util.config.ConfigurationException
+import java.lang.Integer.MAX_VALUE
 
 /**
  * Convenience interface for external classes that want to access the core’s
@@ -34,7 +37,7 @@ import java.lang.Integer.*
  */
 class Preferences(private val eventBus: EventBus) {
 
-       private val _insertionDelay = DefaultOption(60, range(0, MAX_VALUE))
+       private val _insertionDelay = DefaultOption(60) { it in 0..MAX_VALUE }
        val insertionDelay: Int get() = _insertionDelay.get()
        var newInsertionDelay: Int?
                get() = unsupported
@@ -44,7 +47,7 @@ class Preferences(private val eventBus: EventBus) {
                        eventBus.post(PreferenceChangedEvent("InsertionDelay", insertionDelay))
                }
 
-       private val _postsPerPage = DefaultOption(10, range(1, MAX_VALUE))
+       private val _postsPerPage = DefaultOption(10) { it in 1..MAX_VALUE }
        val postsPerPage: Int get() = _postsPerPage.get()
        var newPostsPerPage: Int?
                get() = unsupported
@@ -53,19 +56,19 @@ class Preferences(private val eventBus: EventBus) {
                        eventBus.post(PreferenceChangedEvent("PostsPerPage", postsPerPage))
                }
 
-       private val _imagesPerPage = DefaultOption(9, range(1, MAX_VALUE))
+       private val _imagesPerPage = DefaultOption(9) { it in 1..MAX_VALUE }
        val imagesPerPage: Int get() = _imagesPerPage.get()
        var newImagesPerPage: Int?
                get() = unsupported
-               set (value: Int?) = _imagesPerPage.set(value)
+               set(value: Int?) = _imagesPerPage.set(value)
 
-       private val _charactersPerPost = DefaultOption(400, or(range(50, MAX_VALUE), equalTo(-1)))
+       private val _charactersPerPost = DefaultOption(400) { it == -1 || it in 50..MAX_VALUE }
        val charactersPerPost: Int get() = _charactersPerPost.get()
        var newCharactersPerPost: Int?
                get() = unsupported
                set(value) = _charactersPerPost.set(value)
 
-       private val _postCutOffLength = DefaultOption(200, range(50, MAX_VALUE))
+       private val _postCutOffLength = DefaultOption(200) { it in 50..MAX_VALUE }
        val postCutOffLength: Int get() = _postCutOffLength.get()
        var newPostCutOffLength: Int?
                get() = unsupported
@@ -98,6 +101,17 @@ class Preferences(private val eventBus: EventBus) {
                        eventBus.post(FullAccessRequiredChanged(fcpFullAccessRequired))
                }
 
+       private val _strictFiltering = DefaultOption(false)
+       val strictFiltering: Boolean get() = _strictFiltering.get()
+       var newStrictFiltering: Boolean? = false
+               set(value) {
+                       _strictFiltering.set(value)
+                       when (strictFiltering) {
+                               true -> eventBus.post(StrictFilteringActivatedEvent())
+                               else -> eventBus.post(StrictFilteringDeactivatedEvent())
+                       }
+               }
+
        @Throws(ConfigurationException::class)
        fun saveTo(configuration: Configuration) {
                configuration.getIntValue("Option/ConfigurationVersion").value = 0
@@ -109,6 +123,7 @@ class Preferences(private val eventBus: EventBus) {
                configuration.getBooleanValue("Option/RequireFullAccess").value = _requireFullAccess.real
                configuration.getBooleanValue("Option/ActivateFcpInterface").value = _fcpInterfaceActive.real
                configuration.getIntValue("Option/FcpFullAccessRequired").value = toInt(_fcpFullAccessRequired.real)
+               configuration.getBooleanValue("Option/StrictFiltering").value = _strictFiltering.real
        }
 
        private fun toInt(fullAccessRequired: FullAccessRequired?): Int? {
diff --git a/src/main/kotlin/net/pterodactylus/sone/core/PreferencesLoader.kt b/src/main/kotlin/net/pterodactylus/sone/core/PreferencesLoader.kt
new file mode 100644 (file)
index 0000000..62b60ae
--- /dev/null
@@ -0,0 +1,63 @@
+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)
+               loadStrictFiltering(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] }
+       }
+
+       private fun loadStrictFiltering(configuration: Configuration) {
+               preferences.newStrictFiltering = configuration.getBooleanValue("Option/StrictFiltering").getValue(null)
+       }
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/core/SoneUriCreator.kt b/src/main/kotlin/net/pterodactylus/sone/core/SoneUriCreator.kt
new file mode 100644 (file)
index 0000000..183303a
--- /dev/null
@@ -0,0 +1,26 @@
+package net.pterodactylus.sone.core
+
+import freenet.keys.FreenetURI
+import net.pterodactylus.sone.data.Sone
+import net.pterodactylus.sone.freenet.wot.OwnIdentity
+
+/**
+ * Injectable helper class that can create request and insert URIs for [Sones][Sone].
+ */
+open class SoneUriCreator {
+
+       fun getRequestUri(sone: Sone): FreenetURI = sone.identity.requestUri
+                       .let(::FreenetURI)
+                       .sonify(sone.latestEdition)
+
+       open fun getInsertUri(sone: Sone): FreenetURI? = (sone.identity as? OwnIdentity)?.insertUri
+                       ?.let(::FreenetURI)
+                       ?.sonify(sone.latestEdition)
+
+}
+
+private fun FreenetURI.sonify(edition: Long): FreenetURI =
+               setKeyType("USK")
+                               .setDocName("Sone")
+                               .setMetaString(emptyArray())
+                               .setSuggestedEdition(edition)
index 28bac6d..54af30a 100644 (file)
@@ -42,7 +42,7 @@ abstract class BasicUpdateSoneProcessor(private val database: Database, private
                                        .map { PostRemovedEvent(it) }
                                        .forEach(eventBus::post)
                        newPostReplies
-                                       .onEach { postReply -> if (postReply.time <= sone.followingTime) postReply.isKnown = true }
+                                       .onEach { postReply -> if (postReply.time <= sone.followingTime) database.setPostReplyKnown(postReply) }
                                        .mapNotNull { postReply -> postReply.isKnown.ifFalse { NewPostReplyFoundEvent(postReply) } }
                                        .forEach(eventBus::post)
                        removedPostReplies
diff --git a/src/main/kotlin/net/pterodactylus/sone/core/event/StrictFilteringEvents.kt b/src/main/kotlin/net/pterodactylus/sone/core/event/StrictFilteringEvents.kt
new file mode 100644 (file)
index 0000000..ea12459
--- /dev/null
@@ -0,0 +1,13 @@
+package net.pterodactylus.sone.core.event
+
+/**
+ * Event that signals that the “[strict filtering][net.pterodactylus.sone.core.Preferences.strictFiltering]”
+ * preference was activated.
+ */
+class StrictFilteringActivatedEvent
+
+/**
+ * Event that signals that the “[strict filtering][net.pterodactylus.sone.core.Preferences.strictFiltering]”
+ * preference was deactivated.
+ */
+class StrictFilteringDeactivatedEvent
diff --git a/src/main/kotlin/net/pterodactylus/sone/data/Album.kt b/src/main/kotlin/net/pterodactylus/sone/data/Album.kt
new file mode 100644 (file)
index 0000000..991c0ec
--- /dev/null
@@ -0,0 +1,38 @@
+/**
+ * Sone - Album.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 <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.data
+
+/** Returns all images contained in this album and all its albums. */
+val Album.allImages: Collection<Image>
+       get() =
+               images + albums.flatMap(Album::allImages)
+
+/**
+ *  Returns this album and all albums contained in this album (recursively).
+ * A child album is always listed after its parent.
+ */
+val Album.allAlbums: List<Album>
+       get() =
+               listOf(this) + albums.flatMap(Album::allAlbums)
+
+@get:JvmName("notEmpty")
+val notEmpty: (Album) -> Boolean = { album ->
+       album.allImages.let { images ->
+               images.isNotEmpty() && images.any(Image::isInserted)
+       }
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/data/Albums.kt b/src/main/kotlin/net/pterodactylus/sone/data/Albums.kt
deleted file mode 100644 (file)
index 0c79a84..0000000
+++ /dev/null
@@ -1,23 +0,0 @@
-/**
- * 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 <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.sone.data
-
-/** Returns all images contained in this album and all its albums. */
-val Album.allImages: Collection<Image>
-       get() =
-               images + albums.flatMap { it.allImages }
diff --git a/src/main/kotlin/net/pterodactylus/sone/data/Post.kt b/src/main/kotlin/net/pterodactylus/sone/data/Post.kt
new file mode 100644 (file)
index 0000000..d87bd3c
--- /dev/null
@@ -0,0 +1,16 @@
+package net.pterodactylus.sone.data
+
+import java.util.Comparator.comparing
+
+/**
+ * Predicate that returns whether a post is _not_ from the future,
+ * i.e. whether it should be visible now.
+ */
+@get:JvmName("noFuturePost")
+val noFuturePost: (Post) -> Boolean = { it.time <= System.currentTimeMillis() }
+
+/**
+ * Comparator that orders posts by their time, newest posts first.
+ */
+@get:JvmName("newestPostFirst")
+val newestPostFirst: Comparator<Post> = comparing(Post::getTime).reversed()
diff --git a/src/main/kotlin/net/pterodactylus/sone/data/Reply.kt b/src/main/kotlin/net/pterodactylus/sone/data/Reply.kt
new file mode 100644 (file)
index 0000000..cfc940a
--- /dev/null
@@ -0,0 +1,34 @@
+/**
+ * Sone - Reply.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 <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.data
+
+import java.util.Comparator.comparing
+
+/**
+ * Comparator that orders replies by their time, newest replies first.
+ */
+@get:JvmName("newestReplyFirst")
+val newestReplyFirst: Comparator<Reply<*>> =
+               comparing(Reply<*>::getTime).reversed()
+
+/**
+ * Predicate that returns whether a reply is _not_ from the future,
+ * i.e. whether it should be visible now.
+ */
+val noFutureReply: (Reply<*>) -> Boolean =
+               { it.getTime() <= System.currentTimeMillis() }
diff --git a/src/main/kotlin/net/pterodactylus/sone/data/Sone.kt b/src/main/kotlin/net/pterodactylus/sone/data/Sone.kt
new file mode 100644 (file)
index 0000000..34403a1
--- /dev/null
@@ -0,0 +1,61 @@
+/**
+ * Sone - Sone.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 <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.data
+
+import net.pterodactylus.sone.template.*
+import java.util.Comparator.*
+
+private val caseInsensitiveCompare = { left: String, right: String -> left.compareTo(right, true) }
+
+/**
+ * Comparator that sorts Sones by their [nice name][SoneAccessor.getNiceName]
+ * and, failing that, by [ID][Sone.id].
+ */
+@get:JvmName("niceNameComparator") // TODO: remove once Sone is 100% Kotlin
+val niceNameComparator: Comparator<Sone> =
+               comparing(SoneAccessor::getNiceName, caseInsensitiveCompare).thenComparing(Sone::id)
+
+/**
+ * Comparator that sorts Sones by their [last activity][Sone.getTime], least
+ * recently active Sones first.
+ */
+@get:JvmName("lastActivityComparator") // TODO: remove once Sone is 100% Kotlin
+val lastActivityComparator: Comparator<Sone> =
+               comparing(Sone::getTime).reversed()
+
+/**
+ * Comparator that sorts Sones by their [post count][Sone.getPosts] (most posts
+ * first) and, failing that, by their [reply count][Sone.getReplies] (most
+ * replies first).
+ */
+@get:JvmName("postCountComparator") // TODO: remove once Sone is 100% Kotlin
+val postCountComparator: Comparator<Sone> =
+               comparing<Sone, Int> { it.posts.size }
+                               .thenComparing<Int> { it.replies.size }
+                               .reversed()
+
+val imageCountComparator: Comparator<Sone> =
+               comparing<Sone, Int> { it.rootAlbum.allImages.size }.reversed()
+
+val Sone.allAlbums: List<Album>
+       get() =
+               rootAlbum.albums.flatMap(Album::allAlbums)
+
+val Sone.allImages: Collection<Image>
+       get() =
+               rootAlbum.allImages
index 671ab53..859cbb0 100644 (file)
@@ -26,5 +26,6 @@ interface PostReplyStore {
 
        fun storePostReply(postReply: PostReply)
        fun removePostReply(postReply: PostReply)
+       fun setPostReplyKnown(postReply: PostReply)
 
 }
diff --git a/src/main/kotlin/net/pterodactylus/sone/database/memory/MemoryDatabase.kt b/src/main/kotlin/net/pterodactylus/sone/database/memory/MemoryDatabase.kt
new file mode 100644 (file)
index 0000000..3b5a6a9
--- /dev/null
@@ -0,0 +1,353 @@
+/*
+ * 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
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.database.memory
+
+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.RateLimiter
+import com.google.inject.Inject
+import com.google.inject.Singleton
+import net.pterodactylus.sone.data.Album
+import net.pterodactylus.sone.data.Image
+import net.pterodactylus.sone.data.Post
+import net.pterodactylus.sone.data.PostReply
+import net.pterodactylus.sone.data.Sone
+import net.pterodactylus.sone.data.allAlbums
+import net.pterodactylus.sone.data.allImages
+import net.pterodactylus.sone.data.impl.AlbumBuilderImpl
+import net.pterodactylus.sone.data.impl.ImageBuilderImpl
+import net.pterodactylus.sone.data.newestReplyFirst
+import net.pterodactylus.sone.database.AlbumBuilder
+import net.pterodactylus.sone.database.Database
+import net.pterodactylus.sone.database.DatabaseException
+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.ifTrue
+import net.pterodactylus.sone.utils.unit
+import net.pterodactylus.util.config.Configuration
+import net.pterodactylus.util.config.ConfigurationException
+import java.util.concurrent.locks.ReentrantReadWriteLock
+import kotlin.concurrent.withLock
+
+/**
+ * Memory-based [PostDatabase] implementation.
+ */
+@Singleton
+class MemoryDatabase @Inject constructor(private val configuration: Configuration) : AbstractService(), Database {
+
+       private val lock = ReentrantReadWriteLock()
+       private val readLock by lazy { lock.readLock()!! }
+       private val writeLock by lazy { lock.writeLock()!! }
+       private val configurationLoader = ConfigurationLoader(configuration)
+       private val allSones = mutableMapOf<String, Sone>()
+       private val allPosts = mutableMapOf<String, Post>()
+       private val sonePosts: Multimap<String, Post> = HashMultimap.create<String, Post>()
+       private val knownPosts = mutableSetOf<String>()
+       private val allPostReplies = mutableMapOf<String, PostReply>()
+       private val sonePostReplies: Multimap<String, PostReply> = TreeMultimap.create<String, PostReply>(Comparator { leftString, rightString -> leftString.compareTo(rightString) }, newestReplyFirst)
+       private val knownPostReplies = mutableSetOf<String>()
+       private val allAlbums = mutableMapOf<String, Album>()
+       private val soneAlbums: Multimap<String, Album> = HashMultimap.create<String, Album>()
+       private val allImages = mutableMapOf<String, Image>()
+       private val soneImages: Multimap<String, Image> = HashMultimap.create<String, Image>()
+       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
+
+       override val sones get() = readLock.withLock { allSones.values.toSet() }
+
+       override val localSones get() = readLock.withLock { allSones.values.filter(Sone::isLocal) }
+
+       override val remoteSones get() = readLock.withLock { allSones.values.filterNot(Sone::isLocal) }
+
+       override val bookmarkedPosts get() = memoryBookmarkDatabase.bookmarkedPosts
+
+       override fun save() {
+               if (saveRateLimiter.tryAcquire()) {
+                       saveKnownPosts()
+                       saveKnownPostReplies()
+               }
+       }
+
+       override fun doStart() {
+               memoryBookmarkDatabase.start()
+               loadKnownPosts()
+               loadKnownPostReplies()
+               notifyStarted()
+       }
+
+       override fun doStop() {
+               try {
+                       memoryBookmarkDatabase.stop()
+                       save()
+                       notifyStopped()
+               } catch (de1: DatabaseException) {
+                       notifyFailed(de1)
+               }
+       }
+
+       override fun newSoneBuilder() = MemorySoneBuilder(this)
+
+       override fun storeSone(sone: Sone) {
+               writeLock.withLock {
+                       removeSone(sone)
+
+                       allSones[sone.id] = sone
+                       sonePosts.putAll(sone.id, sone.posts)
+                       for (post in sone.posts) {
+                               allPosts[post.id] = post
+                       }
+                       sonePostReplies.putAll(sone.id, sone.replies)
+                       for (postReply in sone.replies) {
+                               allPostReplies[postReply.id] = postReply
+                       }
+                       sone.allAlbums.let { albums ->
+                               soneAlbums.putAll(sone.id, albums)
+                               albums.forEach { album -> allAlbums[album.id] = album }
+                       }
+                       sone.rootAlbum.allImages.let { images ->
+                               soneImages.putAll(sone.id, images)
+                               images.forEach { image -> allImages[image.id] = image }
+                       }
+               }
+       }
+
+       override fun removeSone(sone: Sone) {
+               writeLock.withLock {
+                       allSones.remove(sone.id)
+                       val removedPosts = sonePosts.removeAll(sone.id)
+                       for (removedPost in removedPosts) {
+                               allPosts.remove(removedPost.id)
+                       }
+                       val removedPostReplies = sonePostReplies.removeAll(sone.id)
+                       for (removedPostReply in removedPostReplies) {
+                               allPostReplies.remove(removedPostReply.id)
+                       }
+                       val removedAlbums = soneAlbums.removeAll(sone.id)
+                       for (removedAlbum in removedAlbums) {
+                               allAlbums.remove(removedAlbum.id)
+                       }
+                       val removedImages = soneImages.removeAll(sone.id)
+                       for (removedImage in removedImages) {
+                               allImages.remove(removedImage.id)
+                       }
+               }
+       }
+
+       override fun getSone(soneId: String) = readLock.withLock { allSones[soneId] }
+
+       override fun getFriends(localSone: Sone): Collection<String> =
+                       if (!localSone.isLocal) {
+                               emptySet()
+                       } else {
+                               memoryFriendDatabase.getFriends(localSone.id)
+                       }
+
+       override fun isFriend(localSone: Sone, friendSoneId: String) =
+                       if (!localSone.isLocal) {
+                               false
+                       } else {
+                               memoryFriendDatabase.isFriend(localSone.id, friendSoneId)
+                       }
+
+       override fun addFriend(localSone: Sone, friendSoneId: String) {
+               if (!localSone.isLocal) {
+                       return
+               }
+               memoryFriendDatabase.addFriend(localSone.id, friendSoneId)
+       }
+
+       override fun removeFriend(localSone: Sone, friendSoneId: String) {
+               if (!localSone.isLocal) {
+                       return
+               }
+               memoryFriendDatabase.removeFriend(localSone.id, friendSoneId)
+       }
+
+       override fun getFollowingTime(friendSoneId: String) =
+                       memoryFriendDatabase.getFollowingTime(friendSoneId)
+
+       override fun getPost(postId: String) =
+                       readLock.withLock { allPosts[postId] }
+
+       override fun getPosts(soneId: String): Collection<Post> =
+                       sonePosts[soneId].toSet()
+
+       override fun getDirectedPosts(recipientId: String) =
+                       readLock.withLock {
+                               allPosts.values.filter {
+                                       it.recipientId.orNull() == recipientId
+                               }
+                       }
+
+       override fun newPostBuilder(): PostBuilder = MemoryPostBuilder(this, this)
+
+       override fun storePost(post: Post) {
+               checkNotNull(post, "post must not be null")
+               writeLock.withLock {
+                       allPosts[post.id] = post
+                       sonePosts[post.sone.id].add(post)
+               }
+       }
+
+       override fun removePost(post: Post) {
+               checkNotNull(post, "post must not be null")
+               writeLock.withLock {
+                       allPosts.remove(post.id)
+                       sonePosts[post.sone.id].remove(post)
+                       post.sone.removePost(post)
+               }
+       }
+
+       override fun getPostReply(id: String) = readLock.withLock { allPostReplies[id] }
+
+       override fun getReplies(postId: String) =
+                       readLock.withLock {
+                               allPostReplies.values
+                                               .filter { it.postId == postId }
+                                               .sortedWith(newestReplyFirst.reversed())
+                       }
+
+       override fun newPostReplyBuilder(): PostReplyBuilder =
+                       MemoryPostReplyBuilder(this, this)
+
+       override fun storePostReply(postReply: PostReply) =
+                       writeLock.withLock {
+                               allPostReplies[postReply.id] = postReply
+                       }
+
+       override fun removePostReply(postReply: PostReply) =
+                       writeLock.withLock {
+                               allPostReplies.remove(postReply.id)
+                       }.unit
+
+       override fun getAlbum(albumId: String) = readLock.withLock { allAlbums[albumId] }
+
+       override fun newAlbumBuilder(): AlbumBuilder = AlbumBuilderImpl()
+
+       override fun storeAlbum(album: Album) =
+                       writeLock.withLock {
+                               allAlbums[album.id] = album
+                               soneAlbums.put(album.sone.id, album)
+                       }.unit
+
+       override fun removeAlbum(album: Album) =
+                       writeLock.withLock {
+                               allAlbums.remove(album.id)
+                               soneAlbums.remove(album.sone.id, album)
+                       }.unit
+
+       override fun getImage(imageId: String) = readLock.withLock { allImages[imageId] }
+
+       override fun newImageBuilder(): ImageBuilder = ImageBuilderImpl()
+
+       override fun storeImage(image: Image): Unit =
+                       writeLock.withLock {
+                               allImages[image.id] = image
+                               soneImages.put(image.sone.id, image)
+                       }
+
+       override fun removeImage(image: Image): Unit =
+                       writeLock.withLock {
+                               allImages.remove(image.id)
+                               soneImages.remove(image.sone.id, image)
+                       }
+
+       override fun bookmarkPost(post: Post) =
+                       memoryBookmarkDatabase.bookmarkPost(post)
+
+       override fun unbookmarkPost(post: Post) =
+                       memoryBookmarkDatabase.unbookmarkPost(post)
+
+       override fun isPostBookmarked(post: Post) =
+                       memoryBookmarkDatabase.isPostBookmarked(post)
+
+       protected fun isPostKnown(post: Post) = readLock.withLock { post.id in knownPosts }
+
+       fun setPostKnown(post: Post, known: Boolean): Unit =
+                       writeLock.withLock {
+                               if (known)
+                                       knownPosts.add(post.id)
+                               else
+                                       knownPosts.remove(post.id)
+                               saveKnownPosts()
+                       }
+
+       protected fun isPostReplyKnown(postReply: PostReply) = readLock.withLock { postReply.id in knownPostReplies }
+
+       override fun setPostReplyKnown(postReply: PostReply): Unit =
+                       writeLock.withLock {
+                               knownPostReplies.add(postReply.id)
+                               saveKnownPostReplies()
+                       }
+
+       private fun loadKnownPosts() =
+                       configurationLoader.loadKnownPosts()
+                                       .let {
+                                               writeLock.withLock {
+                                                       knownPosts.clear()
+                                                       knownPosts.addAll(it)
+                                               }
+                                       }
+
+       private fun saveKnownPosts() =
+                       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
+                                       }
+                               } catch (ce1: ConfigurationException) {
+                                       throw DatabaseException("Could not save database.", ce1)
+                               }
+                       }
+
+       private fun loadKnownPostReplies(): Unit =
+                       configurationLoader.loadKnownPostReplies().let { knownPostReplies ->
+                               writeLock.withLock {
+                                       this.knownPostReplies.clear()
+                                       this.knownPostReplies.addAll(knownPostReplies)
+                               }
+                       }
+
+       private fun saveKnownPostReplies() =
+                       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
+                                       }
+                               } catch (ce1: ConfigurationException) {
+                                       throw DatabaseException("Could not save database.", ce1)
+                               }
+                       }
+
+}
index 08e0593..f884e34 100644 (file)
@@ -44,12 +44,12 @@ abstract class AbstractSoneCommand
                val requiresWriteAccess: Boolean = false) : AbstractCommand() {
 
        @Throws(FcpException::class)
-       protected fun getSone(simpleFieldSet: SimpleFieldSet, parameterName: String, localOnly: Boolean): Sone =
-                       getSone(simpleFieldSet, parameterName, localOnly, true).get()
+       protected fun SimpleFieldSet.getSone(parameterName: String, localOnly: Boolean): Sone =
+                       getSone(parameterName, localOnly, true).get()
 
        @Throws(FcpException::class)
-       protected fun getSone(simpleFieldSet: SimpleFieldSet, parameterName: String, localOnly: Boolean, mandatory: Boolean): Optional<Sone> {
-               val soneId = simpleFieldSet.get(parameterName)
+       protected fun SimpleFieldSet.getSone(parameterName: String, localOnly: Boolean, mandatory: Boolean): Optional<Sone> {
+               val soneId = get(parameterName)
                                .throwOnNullIf(mandatory) { FcpException("Could not load Sone ID from “$parameterName”.") }
                                ?: return Optional.absent()
                val sone = core.getSone(soneId)
@@ -60,9 +60,9 @@ abstract class AbstractSoneCommand
        }
 
        @Throws(FcpException::class)
-       protected fun getPost(simpleFieldSet: SimpleFieldSet, parameterName: String): Post {
+       protected fun SimpleFieldSet.getPost(parameterName: String): Post {
                try {
-                       val postId = simpleFieldSet.getString(parameterName)
+                       val postId = getString(parameterName)
                        return core.getPost(postId)
                                        ?: throw FcpException("Could not load post from “$postId”.")
                } catch (fspe1: FSParseException) {
@@ -71,9 +71,9 @@ abstract class AbstractSoneCommand
        }
 
        @Throws(FcpException::class)
-       protected fun getReply(simpleFieldSet: SimpleFieldSet, parameterName: String): PostReply {
+       protected fun SimpleFieldSet.getReply(parameterName: String): PostReply {
                try {
-                       val replyId = simpleFieldSet.getString(parameterName)
+                       val replyId = getString(parameterName)
                        return core.getPostReply(replyId)
                                        ?: throw FcpException("Could not load reply from “$replyId”.")
                } catch (fspe1: FSParseException) {
index 6097916..88527b3 100644 (file)
@@ -17,7 +17,9 @@
 
 package net.pterodactylus.sone.freenet.wot
 
-import java.util.Collections.*
+import java.util.Collections.synchronizedMap
+import java.util.Collections.synchronizedSet
+import kotlin.collections.set
 
 /**
  * A Web of Trust identity.
@@ -77,6 +79,10 @@ open class DefaultIdentity(private val id: String, private val nickname: String?
                }
        }
 
+       override fun getTrust(): Map<OwnIdentity, Trust> = synchronized(trustCache) {
+               trustCache.toMap()
+       }
+
        override fun getTrust(ownIdentity: OwnIdentity) = synchronized(trustCache) {
                trustCache[ownIdentity]
        }
diff --git a/src/main/kotlin/net/pterodactylus/sone/freenet/wot/IdentityChangeDetector.kt b/src/main/kotlin/net/pterodactylus/sone/freenet/wot/IdentityChangeDetector.kt
new file mode 100644 (file)
index 0000000..ffcafb3
--- /dev/null
@@ -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 <http://www.gnu.org/licenses/>.
+ */
+
+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<Identity>) {
+
+       private val oldIdentities: Map<String, Identity> = oldIdentities.associateBy { it.id }
+       var onNewIdentity: IdentityProcessor? = null
+       var onRemovedIdentity: IdentityProcessor? = null
+       var onChangedIdentity: IdentityProcessor? = null
+       var onUnchangedIdentity: IdentityProcessor? = null
+
+       fun detectChanges(newIdentities: Collection<Identity>) {
+               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<Identity>) =
+               this?.let { identities.forEach(this::invoke) }
index 474ab57..f6e1d59 100644 (file)
@@ -31,11 +31,16 @@ class IdentityLoader @Inject constructor(private val webOfTrustConnector: WebOfT
        private val logger: Logger = Logger.getLogger(IdentityLoader::class.java.name)
 
        @Throws(WebOfTrustException::class)
-       fun loadIdentities() =
+       fun loadTrustedIdentities() =
                        time({ stopwatch, identities -> "Loaded ${identities.size} own identities in ${stopwatch.elapsed(MILLISECONDS) / 1000.0}s." }) {
                                webOfTrustConnector.loadAllOwnIdentities()
                        }.let(this::loadTrustedIdentitiesForOwnIdentities)
 
+       fun loadAllIdentities() =
+                       time({ stopwatch, identities -> "Loaded ${identities.size} own identities in ${stopwatch.elapsed(MILLISECONDS) / 1000.0}s." }) {
+                               webOfTrustConnector.loadAllOwnIdentities()
+                       }.let(this::loadAllIdentitiesForOwnIdentities)
+
        @Throws(PluginException::class)
        private fun loadTrustedIdentitiesForOwnIdentities(ownIdentities: Collection<OwnIdentity>) =
                        ownIdentities
@@ -53,6 +58,22 @@ class IdentityLoader @Inject constructor(private val webOfTrustConnector: WebOfT
                                                }
                                        }
 
+       private fun loadAllIdentitiesForOwnIdentities(ownIdentities: Collection<OwnIdentity>) =
+                       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.loadAllIdentities(ownIdentity, context?.context)
+                                                       }
+                                               }
+                                       }
+
        private fun OwnIdentity.doesNotHaveCorrectContext() =
                        context?.let { it.context !in contexts } ?: false
 
index 67e70e0..829affc 100644 (file)
 
 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.*
+import com.google.common.eventbus.EventBus
+import com.google.common.eventbus.Subscribe
+import com.google.inject.Inject
+import com.google.inject.Singleton
+import net.pterodactylus.sone.core.event.StrictFilteringActivatedEvent
+import net.pterodactylus.sone.core.event.StrictFilteringDeactivatedEvent
+import net.pterodactylus.util.service.AbstractService
+import java.util.concurrent.TimeUnit.SECONDS
+import java.util.concurrent.atomic.AtomicBoolean
+import java.util.logging.Level
+import java.util.logging.Logger
+import java.util.logging.Logger.getLogger
 
 /**
  * The identity manager takes care of loading and storing identities, their
@@ -42,6 +48,7 @@ class IdentityManagerImpl @Inject constructor(
 ) : AbstractService("Sone Identity Manager", false), IdentityManager {
 
        private val currentOwnIdentities = mutableSetOf<OwnIdentity>()
+       private val strictFiltering = AtomicBoolean(false)
 
        override val isConnected: Boolean
                get() = notThrowing { webOfTrustConnector.ping() }
@@ -56,7 +63,7 @@ class IdentityManagerImpl @Inject constructor(
 
                while (!shouldStop()) {
                        try {
-                               val currentIdentities = identityLoader.loadIdentities()
+                               val currentIdentities = identityLoader.loadAllIdentities().applyStrictFiltering()
 
                                val identityChangeEventSender = IdentityChangeEventSender(eventBus, oldIdentities)
                                identityChangeEventSender.detectChanges(currentIdentities)
@@ -78,6 +85,38 @@ class IdentityManagerImpl @Inject constructor(
                }
        }
 
+       private fun Map<OwnIdentity, Set<Identity>>.applyStrictFiltering() =
+                       if (strictFiltering.get()) {
+                               val identitiesWithTrust = values.flatten()
+                                               .groupBy { it.id }
+                                               .mapValues { (_, identities) ->
+                                                       identities.reduce { accIdentity, identity ->
+                                                               identity.trust.forEach { (ownIdentity: OwnIdentity?, trust: Trust?) ->
+                                                                       accIdentity.setTrust(ownIdentity, trust)
+                                                               }
+                                                               accIdentity
+                                                       }
+                                               }
+
+                               mapValues { (_, trustedIdentities) ->
+                                       trustedIdentities.filter { trustedIdentity ->
+                                               identitiesWithTrust[trustedIdentity.id]!!.trust.all { it.value.hasZeroOrPositiveTrust() }
+                                       }
+                               }
+                       } else {
+                               this
+                       }
+
+       @Subscribe
+       fun strictFilteringActivated(event: StrictFilteringActivatedEvent) {
+               strictFiltering.set(true)
+       }
+
+       @Subscribe
+       fun strictFilteringDeactivated(event: StrictFilteringDeactivatedEvent) {
+               strictFiltering.set(false)
+       }
+
 }
 
 private val logger: Logger = getLogger(IdentityManagerImpl::class.java.name)
@@ -89,3 +128,10 @@ private fun notThrowing(action: () -> Unit): Boolean =
                } catch (e: Exception) {
                        false
                }
+
+private fun Trust.hasZeroOrPositiveTrust() =
+               if (explicit == null) {
+                       implicit == null || implicit >= 0
+               } else {
+                       explicit >= 0
+               }
index 2fad1d1..d829734 100644 (file)
 
 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 com.google.inject.Inject
+import freenet.support.SimpleFieldSet
+import kotlinx.coroutines.runBlocking
+import net.pterodactylus.sone.freenet.SimpleFieldSetBuilder
+import net.pterodactylus.sone.freenet.plugin.PluginConnector
+import net.pterodactylus.sone.freenet.plugin.PluginException
+import net.pterodactylus.sone.freenet.plugin.PluginReply
+import java.lang.String.format
+import java.util.logging.Level
 import java.util.logging.Logger
-import java.util.logging.Logger.*
+import java.util.logging.Logger.getLogger
 
 /**
  * Connector for the Web of Trust plugin.
@@ -46,6 +48,14 @@ class PluginWebOfTrustConnector @Inject constructor(private val pluginConnector:
                                        .fields
                                        .parseIdentities { parseTrustedIdentity(it, ownIdentity) }
 
+       override fun loadAllIdentities(ownIdentity: OwnIdentity, context: String?): Set<Identity> =
+                       performRequest(SimpleFieldSetBuilder().put("Message", "GetIdentitiesByScore").put("Truster", ownIdentity.id).put("Selection", "+").put("Context", context ?: "").put("WantTrustValues", "true").get())
+                                       .fields
+                                       .parseIdentities { parseTrustedIdentity(it, ownIdentity) } +
+                                       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())
index e31dce6..a407e2a 100644 (file)
@@ -29,6 +29,16 @@ interface WebOfTrustConnector {
        fun loadTrustedIdentities(ownIdentity: OwnIdentity, context: String? = null): Set<Identity>
 
        /**
+        * Loads all identities known to the given own identity that have 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
+        */
+       fun loadAllIdentities(ownIdentity: OwnIdentity, context: String? = null): Set<Identity>
+
+       /**
         * Adds the given context to the given identity.
         *
         * @param ownIdentity The identity to add the context to
index 749de0d..a95cf78 100644 (file)
@@ -7,10 +7,13 @@ import com.google.inject.*
 import com.google.inject.matcher.*
 import com.google.inject.name.Names.*
 import com.google.inject.spi.*
+import net.pterodactylus.sone.core.SoneUriCreator
 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.sone.web.FreenetSessionProvider
+import net.pterodactylus.sone.web.SessionProvider
 import net.pterodactylus.util.config.*
 import net.pterodactylus.util.config.ConfigurationException
 import net.pterodactylus.util.logging.*
@@ -61,6 +64,8 @@ open class SoneModule(private val sonePlugin: SonePlugin, private val eventBus:
                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)
+               bind(SoneUriCreator::class.java).`in`(Singleton::class.java)
+               bind(SessionProvider::class.java).to(FreenetSessionProvider::class.java).`in`(Singleton::class.java)
 
                bindListener(Matchers.any(), object : TypeListener {
                        override fun <I> hear(typeLiteral: TypeLiteral<I>, typeEncounter: TypeEncounter<I>) {
diff --git a/src/main/kotlin/net/pterodactylus/sone/main/SonePlugin.kt b/src/main/kotlin/net/pterodactylus/sone/main/SonePlugin.kt
new file mode 100644 (file)
index 0000000..5e0b2c1
--- /dev/null
@@ -0,0 +1,7 @@
+package net.pterodactylus.sone.main
+
+data class PluginVersion(val version: String)
+
+data class PluginYear(val year: Int)
+
+data class PluginHomepage(val homepage: String)
index ab7f6ba..42239aa 100644 (file)
@@ -51,5 +51,5 @@ class PostAccessor(private val core: Core) : ReflectionAccessor() {
 
 }
 
-private fun Core.getReplies(post: Post) = getReplies(post.id).filter { Reply.FUTURE_REPLY_FILTER.apply(it) }
+private fun Core.getReplies(post: Post) = getReplies(post.id).filter(noFutureReply)
 private val TemplateContext?.currentSone: Sone? get() = this?.get("currentSone") as? Sone
index b711ca8..24b04fa 100644 (file)
@@ -1,7 +1,6 @@
 package net.pterodactylus.sone.text
 
 import freenet.keys.*
-import freenet.support.*
 import net.pterodactylus.sone.data.*
 import net.pterodactylus.sone.data.impl.*
 import net.pterodactylus.sone.database.*
diff --git a/src/main/kotlin/net/pterodactylus/sone/utils/AutoCloseableBucket.kt b/src/main/kotlin/net/pterodactylus/sone/utils/AutoCloseableBucket.kt
deleted file mode 100644 (file)
index 58c181f..0000000
+++ /dev/null
@@ -1,11 +0,0 @@
-package net.pterodactylus.sone.utils
-
-import freenet.support.api.Bucket
-
-class AutoCloseableBucket(val bucket: Bucket) : AutoCloseable {
-
-       override fun close() {
-               bucket.free()
-       }
-
-}
diff --git a/src/main/kotlin/net/pterodactylus/sone/utils/DefaultOption.kt b/src/main/kotlin/net/pterodactylus/sone/utils/DefaultOption.kt
new file mode 100644 (file)
index 0000000..fd4215f
--- /dev/null
@@ -0,0 +1,28 @@
+package net.pterodactylus.sone.utils
+
+/**
+ * Basic implementation of an [Option].
+ *
+ * @param <T> The type of the option
+ */
+class DefaultOption<T> @JvmOverloads constructor(
+               private val defaultValue: T,
+               private val validator: ((T) -> Boolean)? = null
+) : Option<T> {
+
+       @Volatile
+       private var value: T? = null
+
+       override fun get() = value ?: defaultValue
+
+       override fun getReal(): T? = value
+
+       override fun validate(value: T?): Boolean =
+                       value == null || validator?.invoke(value) ?: true
+
+       override fun set(value: T?) {
+               require(validate(value)) { "New Value ($value) could not be validated." }
+               this.value = value
+       }
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/AllPages.kt b/src/main/kotlin/net/pterodactylus/sone/web/AllPages.kt
new file mode 100644 (file)
index 0000000..c538f0e
--- /dev/null
@@ -0,0 +1,52 @@
+package net.pterodactylus.sone.web
+
+import net.pterodactylus.sone.web.pages.*
+import javax.inject.Inject
+
+/**
+ * Container for all web pages. This uses field injection because there are way too many pages
+ * to sensibly use constructor injection.
+ */
+class AllPages {
+
+       @Inject lateinit var aboutPage: AboutPage
+       @Inject lateinit var bookmarkPage: BookmarkPage
+       @Inject lateinit var bookmarksPage: BookmarksPage
+       @Inject lateinit var createAlbumPage: CreateAlbumPage
+       @Inject lateinit var createPostPage: CreatePostPage
+       @Inject lateinit var createReplyPage: CreateReplyPage
+       @Inject lateinit var createSonePage: CreateSonePage
+       @Inject lateinit var deleteAlbumPage: DeleteAlbumPage
+       @Inject lateinit var deleteImagePage: DeleteImagePage
+       @Inject lateinit var deletePostPage: DeletePostPage
+       @Inject lateinit var deleteProfileFieldPage: DeleteProfileFieldPage
+       @Inject lateinit var deleteReplyPage: DeleteReplyPage
+       @Inject lateinit var deleteSonePage: DeleteSonePage
+       @Inject lateinit var dismissNotificationPage: DismissNotificationPage
+       @Inject lateinit var editAlbumPage: EditAlbumPage
+       @Inject lateinit var editImagePage: EditImagePage
+       @Inject lateinit var editProfileFieldPage: EditProfileFieldPage
+       @Inject lateinit var editProfilePage: EditProfilePage
+       @Inject lateinit var followSonePage: FollowSonePage
+       @Inject lateinit var getImagePage: GetImagePage
+       @Inject lateinit var imageBrowserPage: ImageBrowserPage
+       @Inject lateinit var indexPage: IndexPage
+       @Inject lateinit var knownSonesPage: KnownSonesPage
+       @Inject lateinit var likePage: LikePage
+       @Inject lateinit var lockSonePage: LockSonePage
+       @Inject lateinit var loginPage: LoginPage
+       @Inject lateinit var logoutPage: LogoutPage
+       @Inject lateinit var markAsKnownPage: MarkAsKnownPage
+       @Inject lateinit var newPage: NewPage
+       @Inject lateinit var optionsPage: OptionsPage
+       @Inject lateinit var rescuePage: RescuePage
+       @Inject lateinit var searchPage: SearchPage
+       @Inject lateinit var unbookmarkPage: UnbookmarkPage
+       @Inject lateinit var unfollowSonePage: UnfollowSonePage
+       @Inject lateinit var unlikePage: UnlikePage
+       @Inject lateinit var unlockSonePage: UnlockSonePage
+       @Inject lateinit var uploadImagePage: UploadImagePage
+       @Inject lateinit var viewPostPage: ViewPostPage
+       @Inject lateinit var viewSonePage: ViewSonePage
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/FreenetSessionProvider.kt b/src/main/kotlin/net/pterodactylus/sone/web/FreenetSessionProvider.kt
new file mode 100644 (file)
index 0000000..8928f12
--- /dev/null
@@ -0,0 +1,53 @@
+/**
+ * Sone - FreenetSessionProvider.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 <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web
+
+import freenet.clients.http.SessionManager
+import freenet.clients.http.ToadletContext
+import net.pterodactylus.sone.data.Sone
+import net.pterodactylus.sone.database.SoneProvider
+import java.util.UUID
+import javax.inject.Inject
+
+/**
+ * [SoneProvider] implementation based on Freenet’s [SessionManager].
+ */
+class FreenetSessionProvider @Inject constructor(private val soneProvider: SoneProvider, private val sessionManager: SessionManager) : SessionProvider {
+
+       override fun getCurrentSone(toadletContext: ToadletContext): Sone? =
+                       soneProvider.localSones.singleOrNull()
+                                       ?: sessionManager.useSession(toadletContext)
+                                                       ?.let { it.getAttribute("Sone.CurrentSone") as? String }
+                                                       ?.let(soneProvider.soneLoader)
+                                                       ?.takeIf { it.isLocal }
+
+       override fun setCurrentSone(toadletContext: ToadletContext, sone: Sone?) {
+               if (sone == null) {
+                       sessionManager.useSession(toadletContext)
+                                       ?.removeAttribute("Sone.CurrentSone")
+               } else {
+                       sessionManager.getOrCreateSession(toadletContext)
+                                       ?.setAttribute("Sone.CurrentSone", sone.id)
+               }
+       }
+
+       private fun SessionManager.getOrCreateSession(toadletContext: ToadletContext) =
+                       useSession(toadletContext)
+                                       ?: createSession(UUID.randomUUID().toString(), toadletContext)
+
+}
index 463ddaa..93cd6af 100644 (file)
@@ -8,7 +8,7 @@ import net.pterodactylus.sone.data.Sone
  */
 interface SessionProvider {
 
-       fun getCurrentSone(toadletContext: ToadletContext, createSession: Boolean = true): Sone?
+       fun getCurrentSone(toadletContext: ToadletContext): Sone?
        fun setCurrentSone(toadletContext: ToadletContext, sone: Sone?)
 
 }
index 66c8ed2..c60fe56 100644 (file)
@@ -21,7 +21,7 @@ class GetNotificationsAjaxPage @Inject constructor(webInterface: WebInterface) :
        override val requiresLogin = false
 
        override fun createJsonObject(request: FreenetRequest) =
-                       getCurrentSone(request.toadletContext, false).let { currentSone ->
+                       getCurrentSone(request.toadletContext).let { currentSone ->
                                webInterface.getNotifications(currentSone)
                                                .sortedBy(Notification::getCreatedTime)
                                                .let { notifications ->
index 75f3c3c..5d17262 100644 (file)
@@ -35,7 +35,7 @@ class GetStatusAjaxPage(webInterface: WebInterface, private val elementLoader: E
        }
 
        override fun createJsonObject(request: FreenetRequest) =
-                       getCurrentSone(request.toadletContext, false).let { currentSone ->
+                       getCurrentSone(request.toadletContext).let { currentSone ->
                                createSuccessJsonObject().apply {
                                        this["loggedIn"] = currentSone != null
                                        this["options"] = currentSone?.options?.toJsonOptions() ?: jsonObject {}
index 99c0828..356aca6 100644 (file)
@@ -31,8 +31,8 @@ abstract class JsonPage(protected val webInterface: WebInterface) : Page<Freenet
        protected fun createErrorJsonObject(error: String) =
                        JsonErrorReturnObject(error)
 
-       protected fun getCurrentSone(toadletContext: ToadletContext, createSession: Boolean = true) =
-                       sessionProvider.getCurrentSone(toadletContext, createSession)
+       protected fun getCurrentSone(toadletContext: ToadletContext) =
+                       sessionProvider.getCurrentSone(toadletContext)
 
        override fun handleRequest(request: FreenetRequest, response: Response): Response {
                if (core.preferences.requireFullAccess && !request.toadletContext.isAllowedFullAccess) {
@@ -41,7 +41,7 @@ abstract class JsonPage(protected val webInterface: WebInterface) : Page<Freenet
                if (needsFormPassword && request.parameters["formPassword"] != webInterface.formPassword) {
                        return response.setStatusCode(403).setStatusText("Forbidden").setContentType("application/json").write(createErrorJsonObject("auth-required").asJsonString())
                }
-               if (requiresLogin && (sessionProvider.getCurrentSone(request.toadletContext, false) == null)) {
+               if (requiresLogin && (sessionProvider.getCurrentSone(request.toadletContext) == null)) {
                        return response.setStatusCode(403).setStatusText("Forbidden").setContentType("application/json").write(createErrorJsonObject("auth-required").asJsonString())
                }
                return try {
index 38e0100..5729b25 100644 (file)
 package net.pterodactylus.sone.web.page
 
 import freenet.clients.http.*
-import freenet.clients.http.SessionManager.*
 import freenet.support.api.*
-import net.pterodactylus.sone.freenet.*
 import net.pterodactylus.util.web.*
 import java.net.*
-import java.util.UUID.*
 
 open class FreenetRequest(uri: URI, method: Method,
                val httpRequest: HTTPRequest,
-               val toadletContext: ToadletContext,
-               val sessionManager: SessionManager
-) : Request(uri, method) {
-
-       val session: Session
-               get() =
-                       sessionManager.useSession(toadletContext)
-                                       ?: sessionManager.createSession(randomUUID().toString(), toadletContext)
-
-       val existingSession: Session? get() = sessionManager.useSession(toadletContext)
-
-}
+               val toadletContext: ToadletContext
+) : Request(uri, method)
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/page/PageToadlet.kt b/src/main/kotlin/net/pterodactylus/sone/web/page/PageToadlet.kt
new file mode 100644 (file)
index 0000000..db7ede0
--- /dev/null
@@ -0,0 +1,79 @@
+/*
+ * Sone - PageToadlet.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 <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.page
+
+import freenet.client.HighLevelSimpleClient
+import freenet.clients.http.LinkEnabledCallback
+import freenet.clients.http.LinkFilterExceptedToadlet
+import freenet.clients.http.Toadlet
+import freenet.clients.http.ToadletContext
+import freenet.support.MultiValueTable
+import freenet.support.api.HTTPRequest
+import net.pterodactylus.sone.utils.use
+import net.pterodactylus.util.web.Method
+import net.pterodactylus.util.web.Page
+import net.pterodactylus.util.web.Response
+import java.net.URI
+
+/**
+ * [Toadlet] implementation that is wrapped around a [Page].
+ */
+class PageToadlet(
+               highLevelSimpleClient: HighLevelSimpleClient,
+               val menuName: String?,
+               private val page: Page<FreenetRequest>,
+               private val pathPrefix: String
+) : Toadlet(highLevelSimpleClient), LinkEnabledCallback, LinkFilterExceptedToadlet {
+
+       override fun path() = pathPrefix + page.path
+
+       override fun handleMethodGET(uri: URI, httpRequest: HTTPRequest, toadletContext: ToadletContext) =
+                       handleRequest(FreenetRequest(uri, Method.GET, httpRequest, toadletContext))
+
+       fun handleMethodPOST(uri: URI?, httpRequest: HTTPRequest?, toadletContext: ToadletContext?) =
+                       handleRequest(FreenetRequest(uri!!, Method.POST, httpRequest!!, toadletContext!!))
+
+       private fun handleRequest(pageRequest: FreenetRequest) {
+               pageRequest.toadletContext.bucketFactory.makeBucket(-1).use { pageBucket ->
+                       pageBucket.outputStream.use { pageBucketOutputStream ->
+                               val pageResponse = page.handleRequest(pageRequest, Response(pageBucketOutputStream))
+                               // according to the javadoc, headers is allowed to return null but that’s stupid and it doesn’t do that.
+                               val headers = pageResponse.headers.fold(MultiValueTable<String, String>()) { headers, header ->
+                                       headers.apply {
+                                               header.forEach { put(header.name, it) }
+                                       }
+                               }
+                               with(pageResponse) {
+                                       writeReply(pageRequest.toadletContext, statusCode, contentType, statusText, headers, pageBucket)
+                               }
+                       }
+               }
+       }
+
+       override fun isEnabled(toadletContext: ToadletContext) =
+                       if (page is LinkEnabledCallback) {
+                               page.isEnabled(toadletContext)
+                       } else
+                               true
+
+       override fun isLinkExcepted(link: URI) =
+                       page is FreenetPage && page.isLinkExcepted(link)
+
+       override fun toString() = "${javaClass.name}[path=${path()},page=$page]"
+
+}
index 4c671ad..61d8718 100644 (file)
 package net.pterodactylus.sone.web.page
 
 import freenet.client.*
-import freenet.clients.http.*
 import net.pterodactylus.util.web.*
 import javax.inject.*
 
 class PageToadletFactory @Inject constructor(
                private val highLevelSimpleClient: HighLevelSimpleClient,
-               private val sessionManager: SessionManager,
                @Named("toadletPathPrefix") private val pathPrefix: String
 ) {
 
        @JvmOverloads
        fun createPageToadlet(page: Page<FreenetRequest>, menuName: String? = null) =
-                       PageToadlet(highLevelSimpleClient, sessionManager, menuName ?: page.menuName, page, pathPrefix)
+                       PageToadlet(highLevelSimpleClient, menuName ?: page.menuName, page, pathPrefix)
 
 }
index 703f953..fcfc63d 100644 (file)
@@ -7,10 +7,10 @@ import net.pterodactylus.sone.web.*
 import net.pterodactylus.util.web.*
 import java.net.*
 
-class SoneRequest(uri: URI, method: Method, httpRequest: HTTPRequest, toadletContext: ToadletContext, sessionManager: SessionManager,
+class SoneRequest(uri: URI, method: Method, httpRequest: HTTPRequest, toadletContext: ToadletContext,
                                  val core: Core,
                                  val webInterface: WebInterface
-) : FreenetRequest(uri, method, httpRequest, toadletContext, sessionManager)
+) : FreenetRequest(uri, method, httpRequest, toadletContext)
 
 fun FreenetRequest.toSoneRequest(core: Core, webInterface: WebInterface) =
-               SoneRequest(uri, method, httpRequest, toadletContext, sessionManager, core, webInterface)
+               SoneRequest(uri, method, httpRequest, toadletContext, core, webInterface)
index 0b690da..959788b 100644 (file)
@@ -21,7 +21,7 @@ class CreateSonePage @Inject constructor(webInterface: WebInterface, loaders: Lo
        private val logger = Logger.getLogger(CreateSonePage::class.java.name)
 
        override fun handleRequest(soneRequest: SoneRequest, templateContext: TemplateContext) {
-               templateContext["sones"] = soneRequest.core.localSones.sortedWith(Sone.NICE_NAME_COMPARATOR)
+               templateContext["sones"] = soneRequest.core.localSones.sortedWith(niceNameComparator)
                templateContext["identitiesWithoutSone"] = soneRequest.core.identityManager.allOwnIdentities.filterNot { "Sone" in it.contexts }.sortedBy { "${it.nickname}@${it.id}".toLowerCase() }
                if (soneRequest.isPOST) {
                        val identity = soneRequest.httpRequest.getPartAsStringFailsafe("identity", 43)
index 20219d7..a0ca0d7 100644 (file)
@@ -29,9 +29,7 @@ class ImageBrowserPage @Inject constructor(webInterface: WebInterface, loaders:
                } else if (soneRequest.parameters["mode"] == "gallery") {
                        templateContext["galleryRequested"] = true
                        soneRequest.core.sones
-                                       .map(Sone::getRootAlbum)
-                                       .flatMap(Album::getAlbums)
-                                       .flatMap { Album.FLATTENER.apply(it)!! }
+                                       .flatMap(Sone::allAlbums)
                                        .filterNot(Album::isEmpty)
                                        .sortedBy(Album::getTitle)
                                        .also { albums ->
index ae0d7d1..01d1d22 100644 (file)
@@ -28,10 +28,10 @@ class KnownSonesPage @Inject constructor(webInterface: WebInterface, loaders: Lo
                                        .filterNot { soneRequest.parameters["filter"] == "not-own" && it.isLocal }
                                        .sortedWith(
                                                        when (soneRequest.parameters["sort"]) {
-                                                               "images" -> Sone.IMAGE_COUNT_COMPARATOR
-                                                               "name" -> Sone.NICE_NAME_COMPARATOR.reversed()
-                                                               "posts" -> Sone.POST_COUNT_COMPARATOR
-                                                               else -> Sone.LAST_ACTIVITY_COMPARATOR
+                                                               "images" -> imageCountComparator
+                                                               "name" -> niceNameComparator.reversed()
+                                                               "posts" -> postCountComparator
+                                                               else -> lastActivityComparator
                                                        }.let { comparator ->
                                                                when (soneRequest.parameters["order"]) {
                                                                        "asc" -> comparator.reversed()
index 5be34ac..6244325 100644 (file)
@@ -26,7 +26,7 @@ class LoginPage @Inject constructor(webInterface: WebInterface, loaders: Loaders
                                redirectTo(target)
                        }
                }
-               templateContext["sones"] = soneRequest.core.localSones.sortedWith(Sone.NICE_NAME_COMPARATOR)
+               templateContext["sones"] = soneRequest.core.localSones.sortedWith(niceNameComparator)
                templateContext["identitiesWithoutSone"] = soneRequest.core.identityManager.allOwnIdentities.filterNot { "Sone" in it.contexts }.sortedBy { "${it.nickname}@${it.id}" }
        }
 
index 412258d..9465a6a 100644 (file)
@@ -40,9 +40,11 @@ class OptionsPage @Inject constructor(webInterface: WebInterface, loaders: Loade
                        }
                        val fullAccessRequired = "require-full-access" in soneRequest.parameters
                        val fcpInterfaceActive = "fcp-interface-active" in soneRequest.parameters
+                       val strictFiltering = "strict-filtering" in soneRequest.parameters
 
                        soneRequest.core.preferences.newRequireFullAccess = fullAccessRequired
                        soneRequest.core.preferences.newFcpInterfaceActive = fcpInterfaceActive
+                       soneRequest.core.preferences.newStrictFiltering = strictFiltering
 
                        val postsPerPage = soneRequest.parameters["posts-per-page"]?.toIntOrNull()
                        val charactersPerPost = soneRequest.parameters["characters-per-post"]?.toIntOrNull()
@@ -82,6 +84,7 @@ class OptionsPage @Inject constructor(webInterface: WebInterface, loaders: Loade
                        templateContext["require-full-access"] = preferences.requireFullAccess
                        templateContext["post-cut-off-length"] = preferences.postCutOffLength
                        templateContext["posts-per-page"] = preferences.postsPerPage
+                       templateContext["strict-filtering"] = preferences.strictFiltering
                }
        }
 
index 19eba1c..9bbc3e5 100644 (file)
@@ -58,7 +58,7 @@ class SearchPage(webInterface: WebInterface, loaders: Loaders, templateRenderer:
                val postPagination = cache.get(phrases) {
                        soneRequest.core.sones
                                        .flatMap(Sone::getPosts)
-                                       .filter { Post.FUTURE_POSTS_FILTER.apply(it) }
+                                       .filter(noFuturePost)
                                        .scoreAndPaginate(phrases, soneRequest.core.preferences.postsPerPage) { it.allText(soneNameCache, soneRequest.core::getReplies) }
                }.apply { page = soneRequest.parameters["postPage"].emptyToNull?.toIntOrNull() ?: 0 }
 
@@ -88,7 +88,7 @@ class SearchPage(webInterface: WebInterface, loaders: Loaders, templateRenderer:
 
        private fun Post.allText(soneNameCache: (Sone) -> String, getReplies: (String) -> Collection<PostReply>) =
                        (text + recipient.orNull()?.let { " ${soneNameCache(it)}" } + getReplies(id)
-                                       .filter { PostReply.FUTURE_REPLY_FILTER.apply(it) }
+                                       .filter(noFutureReply)
                                        .map { "${soneNameCache(it.sone)} ${it.text}" }.joinToString(" ", " ")).toLowerCase()
 
        private fun Iterable<Phrase>.indicesFor(text: String, predicate: (Phrase) -> Boolean) =
index 9734a03..86131c3 100644 (file)
@@ -29,7 +29,7 @@ open class SoneTemplatePage(
        protected val translation: Translation = webInterface.translation
 
        protected fun getCurrentSone(toadletContext: ToadletContext, createSession: Boolean = true) =
-                       sessionProvider.getCurrentSone(toadletContext, createSession)
+                       sessionProvider.getCurrentSone(toadletContext)
 
        protected fun setCurrentSone(toadletContext: ToadletContext, sone: Sone?) =
                        sessionProvider.setCurrentSone(toadletContext, sone)
@@ -91,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.sessionManager, core, webInterface))
+                       isEnabled(SoneRequest(toadletContext.uri, Method.GET, HTTPRequestImpl(toadletContext.uri, "GET"), toadletContext, core, webInterface))
 
        open fun isEnabled(soneRequest: SoneRequest) = when {
                requiresLogin && getCurrentSone(soneRequest.toadletContext) == null -> false
index e17e995..cd42c45 100644 (file)
@@ -74,6 +74,8 @@ Page.Options.Option.FcpFullAccessRequired.Description=FCP-Verbindungen nur von 
 Page.Options.Option.FcpFullAccessRequired.Value.No=Nein
 Page.Options.Option.FcpFullAccessRequired.Value.Writing=Für Schreibzugriffe
 Page.Options.Option.FcpFullAccessRequired.Value.Always=Immer
+Page.Options.Section.WebOfTrustOptions.Title=„Web of Trust“ Optionen
+Page.Options.Option.StrictFiltering.Description=Identitäten strenger filtern. Wenn diese Option gewählt ist, werden Identitäten, die von mindestens einer Ihrer lokalen Identitäten einen negativen Vertrauenswert zugewiesen bekommen haben, komplett ignoriert; ansonsten werden Identitäten gezeigt, wenn sie von mindestens einer Ihrer Identitäten einen positiven Vertrauenswert zugewiesen bekommen. (Bitte beachten Sie, dass diese Einstellung ein paar Minuten braucht, um Wirkung zu zeigen!)
 Page.Options.Section.Cleaning.Title=Aufräumen
 Page.Options.Option.ClearOnNextRestart.Description=Setzt die Konfiguration des Sone-Plugins beim nächsten Start zurück. Vorsicht: {strong}Alle Informationen Ihrer Sones werden gelöscht{/strong}, also stellen Sie bitte sicher, dass Sie die notwendigen Sicherungen angefertigt haben! Damit diese Option aktiv wird, muss auch die folgende Option aktiviert werden.
 Page.Options.Option.ReallyClearOnNextRestart.Description=Diese Option muss auf „ja“ gestellt werden, wenn Sie wirklich {strong}wirklich{/strong} sämtliche Informationen des Sone-Plugins beim nächsten Start entfernen möchten.
index 4376025..7c65dcf 100644 (file)
@@ -74,6 +74,8 @@ Page.Options.Option.FcpFullAccessRequired.Description=Require FCP connection fro
 Page.Options.Option.FcpFullAccessRequired.Value.No=No
 Page.Options.Option.FcpFullAccessRequired.Value.Writing=For Write Access
 Page.Options.Option.FcpFullAccessRequired.Value.Always=Always
+Page.Options.Section.WebOfTrustOptions.Title=Web of Trust Settings
+Page.Options.Option.StrictFiltering.Description=Apply stricter filtering of identities. When this is selected, Sone will completely ignore identities that have a negative trust value for any of your local identities, otherwise they will be shown as long as at least one of your local identities has a positive trust value for them. (Note that it will take a couple of minutes for this setting to show effect.)
 Page.Options.Section.Cleaning.Title=Clean Up
 Page.Options.Option.ClearOnNextRestart.Description=Resets the configuration of the Sone plugin at the next restart. Warning! {strong}This will destroy all of your Sones{/strong} so make sure you have backed up everyhing you still need! Also, you need to set the next option to true to actually do it.
 Page.Options.Option.ReallyClearOnNextRestart.Description=This option needs to be set to “yes” if you really, {strong}really{/strong} want to clear the plugin configuration on the next restart.
index db20b73..bc90612 100644 (file)
@@ -74,6 +74,8 @@ Page.Options.Option.FcpFullAccessRequired.Description=Requiere conexión FCP de
 Page.Options.Option.FcpFullAccessRequired.Value.No=No
 Page.Options.Option.FcpFullAccessRequired.Value.Writing=Para acceso de escritura
 Page.Options.Option.FcpFullAccessRequired.Value.Always=Siempre
+Page.Options.Section.WebOfTrustOptions.Title=Web of Trust Settings
+Page.Options.Option.StrictFiltering.Description=Apply stricter filtering of identities. When this is selected, Sone will completely ignore identities that have a negative trust value for any of your local identities, otherwise they will be shown as long as at least one of your local identities has a positive trust value for them. (Note that it will take a couple of minutes for this setting to show effect.)
 Page.Options.Section.Cleaning.Title=Limpiar
 Page.Options.Option.ClearOnNextRestart.Description=Reinicia la configuración del plugin Sone en el siguiente reinicio.Cuidado! {strong}Esto destruirá todos tus Sone{/strong} de modo que asegurate de que has hecho una copia de seguridad de todo lo que necesitas! También tendrás que asignar cierto a la siguiente opción para hacerlo.
 Page.Options.Option.ReallyClearOnNextRestart.Description=Esta opción tiene que ser puesta en "yes" si realmente, {strong}realmente{/strong} quieres limpiar la configuración del plugin en el siguiente reinicio.
index f4ff0d1..bf1eccd 100644 (file)
@@ -26,8 +26,8 @@ Navigation.Menu.Sone.Item.Rescue.Name=Récupération
 Navigation.Menu.Sone.Item.Rescue.Tooltip=Récupération de votre Sone
 Navigation.Menu.Sone.Item.About.Name=A propos
 Navigation.Menu.Sone.Item.About.Tooltip=Informations à propos de Sone
-Navigation.Menu.Sone.Item.Metrics.Name=Metrics
-Navigation.Menu.Sone.Item.Metrics.Tooltip=Metrics collected by Sone
+Navigation.Menu.Sone.Item.Metrics.Name=Métriques
+Navigation.Menu.Sone.Item.Metrics.Tooltip=Métriques collectées par Sone
 
 Page.About.Title=A propos de - Sone
 Page.About.Page.Title=A propos
@@ -74,6 +74,8 @@ Page.Options.Option.FcpFullAccessRequired.Description=Requière une connexion FC
 Page.Options.Option.FcpFullAccessRequired.Value.No=Non
 Page.Options.Option.FcpFullAccessRequired.Value.Writing=Pour accès à l'écriture
 Page.Options.Option.FcpFullAccessRequired.Value.Always=toujours
+Page.Options.Section.WebOfTrustOptions.Title=Web of Trust Settings
+Page.Options.Option.StrictFiltering.Description=Apply stricter filtering of identities. When this is selected, Sone will completely ignore identities that have a negative trust value for any of your local identities, otherwise they will be shown as long as at least one of your local identities has a positive trust value for them. (Note that it will take a couple of minutes for this setting to show effect.)
 Page.Options.Section.Cleaning.Title=Nettoyer
 Page.Options.Option.ClearOnNextRestart.Description=Réinitialiser la configuration du plugin Sone au prochain redémarrage. Attention! {strong}Cela détruira tous vos Sones{/strong}. Soyez sûr d'avoir sauvegardé tout ce dont vous avez besoin! Vous devez également choisir "Oui" à l'option suivante pour procéder à la réinitialisation.
 Page.Options.Option.ReallyClearOnNextRestart.Description=Choisir "Oui" pour cette option si vous voulez vraiment{strong}vraiment{/strong} effacer la configuration au prochain redémarrage.
diff --git a/src/main/resources/i18n/sone.it.properties b/src/main/resources/i18n/sone.it.properties
new file mode 100644 (file)
index 0000000..d21adaa
--- /dev/null
@@ -0,0 +1,468 @@
+Navigation.Menu.Sone.Name=Sone
+Navigation.Menu.Sone.Tooltip=Freenet Social Network Plugin
+Navigation.Menu.Sone.Item.Login.Name=Accedi
+Navigation.Menu.Sone.Item.Login.Tooltip=Accedi al tuo "Sone"
+Navigation.Menu.Sone.Item.Index.Name=Il tuo "Sone"
+Navigation.Menu.Sone.Item.Index.Tooltip=Visualizza il tuo "Sone"
+Navigation.Menu.Sone.Item.New.Name=Nuovi messaggi e Risposte
+Navigation.Menu.Sone.Item.New.Tooltip=Visualizza nuovi messaggi e risposte
+Navigation.Menu.Sone.Item.CreateSone.Name=Crea "Sone"
+Navigation.Menu.Sone.Item.CreateSone.Tooltip=Crea un nuovo "Sone"
+Navigation.Menu.Sone.Item.KnownSones.Name="Sone" conosciuti
+Navigation.Menu.Sone.Item.KnownSones.Tooltip=Visualizza tutti i "Sone" conosciuti
+Navigation.Menu.Sone.Item.Bookmarks.Name=Preferiti
+Navigation.Menu.Sone.Item.Bookmarks.Tooltip=Visualizza messaggi preferiti
+Navigation.Menu.Sone.Item.EditProfile.Name=Modifica Profilo
+Navigation.Menu.Sone.Item.EditProfile.Tooltip=Modifica il profilo del tuo "Sone"
+Navigation.Menu.Sone.Item.ImageBrowser.Name=Immagini
+Navigation.Menu.Sone.Item.ImageBrowser.Tooltip=Gestisci le tue immagini
+Navigation.Menu.Sone.Item.DeleteSone.Name=Cancella "Sone"
+Navigation.Menu.Sone.Item.DeleteSone.Tooltip=Cancella il "Sone" corrente
+Navigation.Menu.Sone.Item.Logout.Name=Esci
+Navigation.Menu.Sone.Item.Logout.Tooltip=Disconnettiti dal "Sone" corrente
+Navigation.Menu.Sone.Item.Options.Name=Opzioni
+Navigation.Menu.Sone.Item.Options.Tooltip=Opzioni per il plugin "Sone"
+Navigation.Menu.Sone.Item.Rescue.Name=Recupera
+Navigation.Menu.Sone.Item.Rescue.Tooltip=Recupera "Sone"
+Navigation.Menu.Sone.Item.About.Name=Informazioni
+Navigation.Menu.Sone.Item.About.Tooltip=Informazioni su "Sone"
+Navigation.Menu.Sone.Item.Metrics.Name=Metriche
+Navigation.Menu.Sone.Item.Metrics.Tooltip=Metriche raccolte da "Sone"
+
+Page.About.Title=Informazioni su Sone
+Page.About.Page.Title=Informazioni
+Page.About.Flattr.Description=Se ti piace Sone e vuoi ringraziarmi con un premio, puoi usare il pulsante Flattr alla fine di ogni pagina. Flattr è un sistema di micro-pagamenti non anonimo che funziona come un barattolo delle mance e la quantità che ogni utente spende è limitata (minimo 2 € al mese). Maggiori informazioni possono essere trovate su {link}flattr.com{/link}.
+Page.About.Homepage.Title=Homepage
+Page.About.Homepage.Description=Puoi trovare maggiori informazioni e il codice sorgente di Sone sulla {link}homepage{/link}.
+Page.About.License.Title=Licenza
+
+Page.Options.Title=Opzioni - "Sone"
+Page.Options.Page.Title=Opzioni
+Page.Options.Page.Description=Queste opzioni influenzano il comportamento del plugin "Sone" in esecuzione
+Page.Options.Section.SoneSpecificOptions.Title=Opzioni specifiche di "Sone"
+Page.Options.Section.SoneSpecificOptions.NotLoggedIn=Queste opzioni sono disponibili solo se sei {link}collegato{/link}
+Page.Options.Section.SoneSpecificOptions.LoggedIn=Queste opzioni sono valide solo se ti sei identificato e sono valide solo per l'account con il quale ti sei identificato.
+Page.Options.Option.AutoFollow.Description=Se un novo Sone viene trovato seguilo automaticamente. Da notare che verranno seguiti solo i Sone scoperti dopo l'attivazione di questa opzione!
+Page.Options.Option.EnableSoneInsertNotifications.Description=Se abilitato, verrà visualizzato una notifica ogni volta che il tuo Sone sarà in fase di caricamento o sarà terminato il caricamento.
+Page.Options.Option.ShowNotificationNewSones.Description=Visualizza notifiche per i nuovi "Sone".
+Page.Options.Option.ShowNotificationNewPosts.Description=Visualizza notifiche per i nuovi messaggi.
+Page.Options.Option.ShowNotificationNewReplies.Description=Visualizza notifiche per le nuove risposte.
+Page.Options.Section.AvatarOptions.Title=Opzioni Avatar
+Page.Options.Option.ShowAvatars.Description=Qui puoi disabilitare gli avatar customizzati, in base alla selezione fatta. Se un avatar è disabilitato verrà visualizzato l'avatar autogenerato.
+Page.Options.Option.ShowAvatars.Never.Description=Non visualizzare mai gli avatar personalizzati.
+Page.Options.Option.ShowAvatars.Followed.Description=Visualizza gli avatar solo per i "Sone" che segui.
+Page.Options.Option.ShowAvatars.ManuallyTrusted.Description=Visualizza solo gli avatar per i Sone a cui hai assegnato un valore di trust maggiore di 0.
+Page.Options.Option.ShowAvatars.Trusted.Description=Visualizza gli avatar solo per i "Sone" che hanno un livello di fiducia maggiore di 0.
+Page.Options.Option.ShowAvatars.Always.Description=Visualizza sempre gli avatar personalizzati. Stai attento: alcuni avatar possono contenere immagini fastidiose od offensive.
+Page.Options.Section.LoadLinkedImagesOptions.Title=Carica le immagini collegate
+Page.Options.Option.LoadLinkedImages.Description="Sone" può cercare di caricare in automatico le immagini linkate nei messaggi e nelle risposte. Il caricamento avverrà sempre da Freenet, mai da internet!
+Page.Options.Option.LoadLinkedImages.Never.Description=Non caricare mai le immagini linkate.
+Page.Options.Option.LoadLinkedImages.Followed.Description=Carica le immagini linkate solo per i "Sone" che segui.
+Page.Options.Option.LoadLinkedImages.ManuallyTrusted.Description=Visualizza solo le immagini linkate da Sone a cui hai assegnto un valore di trust maggiore di 0.
+Page.Options.Option.LoadLinkedImages.Trusted.Description=Visualizza le immagini solo da Sone che hanno un livello di trust maggiore di 0.
+Page.Options.Option.LoadLinkedImages.Always.Description=Carica sempre le immaigni. Attenzione: alcune immagini potrebbero essere offensive.
+Page.Options.Section.RuntimeOptions.Title=Comportamento durante l'esecuzione
+Page.Options.Option.InsertionDelay.Description=Il numero di secondi che il processo di caricamento del Sone aspetta, dopo una modifica, prima di iniziare il caricamento.
+Page.Options.Option.PostsPerPage.Description=Il numero di messaggi da visualizzare su una pagina prima di visualizzare i controlli di paginazione.
+Page.Options.Option.ImagesPerPage.Description=Il numero di immagini da visualizzare su una pagina prima di visualizzare i controlli di paginazione.
+Page.Options.Option.CharactersPerPost.Description=The number of characters to display from a post before cutting it off and showing a link to expand it (-1 to disable). The actual length of the snippet is determined by the option below.
+Page.Options.Option.PostCutOffLength.Description=The number of characters that are displayed if a post is deemed too long (see option above). Ignored if “number of characters to display” is disabled (set to -1).
+Page.Options.Option.RequireFullAccess.Description=Whether to deny access to Sone to any host that has not been granted full access.
+Page.Options.Section.FcpOptions.Title=Settaggi interfaccia FCP
+Page.Options.Option.FcpInterfaceActive.Description=Activate the FCP interface to allow other plugins and remote clients to access your Sone plugin.
+Page.Options.Option.FcpFullAccessRequired.Description=Require FCP connection from allowed hosts (see your {link}node’s configuration, section “FCP”{/link})
+Page.Options.Option.FcpFullAccessRequired.Value.No=No
+Page.Options.Option.FcpFullAccessRequired.Value.Writing=Per l'accesso in scrittura
+Page.Options.Option.FcpFullAccessRequired.Value.Always=Sempre
+Page.Options.Section.WebOfTrustOptions.Title=Web of Trust Settings
+Page.Options.Option.StrictFiltering.Description=Apply stricter filtering of identities. When this is selected, Sone will completely ignore identities that have a negative trust value for any of your local identities, otherwise they will be shown as long as at least one of your local identities has a positive trust value for them. (Note that it will take a couple of minutes for this setting to show effect.)
+Page.Options.Section.Cleaning.Title=Pulisci
+Page.Options.Option.ClearOnNextRestart.Description=Resets the configuration of the Sone plugin at the next restart. Warning! {strong}This will destroy all of your Sones{/strong} so make sure you have backed up everyhing you still need! Also, you need to set the next option to true to actually do it.
+Page.Options.Option.ReallyClearOnNextRestart.Description=This option needs to be set to “yes” if you really, {strong}really{/strong} want to clear the plugin configuration on the next restart.
+Page.Options.Warnings.ValueNotChanged=This option was not changed because the value you specified was not valid.
+Page.Options.Button.Save=Salva
+
+Page.Login.Title=Login - Sone
+Page.Login.Page.Title=Accedi
+Page.Login.Label.SelectSone=Seleziona Sone:
+Page.Login.Option.NoSone=Seleziona Sone...
+
+Page.Login.CreateSone.Title=Crea Sone
+
+Page.CreateSone.Title=Crea Sone - Sone
+
+Page.DeleteSone.Title=Cancella Sone - Sone
+Page.DeleteSone.Page.Title=Cancellare il Sone "{sone}"?
+Page.DeleteSone.Page.Description=This will not delete the Sone from Freenet (because that is impossible), it will merely disconnect your web of trust identity from Sone.
+Page.DeleteSone.Button.Yes=Si, cancella.
+Page.DeleteSone.Button.No=No, non cancellare.
+
+Page.Index.Title=Il tuo Sone - Sone
+Page.Index.Label.Text=Testo del messaggio:
+Page.Index.Label.Sender=Mittente:
+Page.Index.Button.Post=Pubblica!
+Page.Index.PostList.Title=Feed dei messaggi
+Page.Index.PostList.Text.NoPostYet=Nessuno ha anocara scritto un messaggio ancora. Dovresti iniziare adesso!
+Page.Index.PostList.Text.FollowSomeSones=Or maybe you are not following any Sones? Take a look at the list of {link}known Sones{/link} and follow whoever looks interesting!
+Page.Index.PostList.Text.AutoFollowOption=You also have the option of automatically following newly discovered Sones. Take a look at the {link}options{/link} to activate the auto-follow feature!
+
+Page.New.Title=Nuovi messaggi e risposte - Sone
+Page.New.Page.Title=Nuovi messaggi e risposte
+Page.New.NothingNew=Al momento non c'è nulla di nuovo.
+
+Page.KnownSones.Title=Sone conosciuti - Sone
+Page.KnownSones.Page.Title=Sone conosciuti
+Page.KnownSones.Text.NoKnownSones=Al momento non ci sono Sone conosciuti che corrispondono al filtro.
+Page.KnownSones.Label.Sort=Ordina:
+Page.KnownSones.Label.FilterSones=Filtra i Sone:
+Page.KnownSones.Sort.Field.Name=Nome
+Page.KnownSones.Sort.Field.LastActivity=Ultima attività
+Page.KnownSones.Sort.Field.Posts=Numero di messaggi
+Page.KnownSones.Sort.Field.Images=Numero di immagini
+Page.KnownSones.Sort.Order.Ascending=Ascendente
+Page.KnownSones.Sort.Order.Descending=Discendente
+Page.KnownSones.Filter.Followed=Visualizza solo i Sone seguiti
+Page.KnownSones.Filter.NotFollowed=Nascondi i Sone seguiti
+Page.KnownSones.Filter.New=Visualizza solo i nuovi Sone
+Page.KnownSones.Filter.NotNew=Nascondi i nuovi Sone
+Page.KnownSones.Filter.Own=Visualizza solo i Sone locali
+Page.KnownSones.Filter.NotOwn=Visualizza solo i Sone remoti
+Page.KnownSones.Button.Apply=Applica
+Page.KnownSones.Button.FollowAllSones=Segui tutti i Sone su questa pagina
+Page.KnownSones.Button.UnfollowAllSones=Smetti di seguire tutti i Sone su questa pagina
+
+Page.EditProfile.Title=Modifica profilo - Sone
+Page.EditProfile.Page.Title=Modifica profilo
+Page.EditProfile.Page.Description=Su questa pagina puoi inserire i dati del tuo profilo
+Page.EditProfile.Page.Hint.Optionality=And remember, every single field of this profile is optional! You are not required to enter a single thing here! Also, everything you enter here will probably be stored in Freenet for a very long time!
+Page.EditProfile.Label.FirstName=Nome:
+Page.EditProfile.Label.MiddleName=Secondo nome(i):
+Page.EditProfile.Label.LastName=Cognome:
+Page.EditProfile.Birthday.Title=Compleanno:
+Page.EditProfile.Birthday.Label.Day=Giorno:
+Page.EditProfile.Birthday.Label.Month=Mese:
+Page.EditProfile.Birthday.Label.Year=Anno:
+Page.EditProfile.Avatar.Title=Avatar
+Page.EditProfile.Avatar.Description=You can select one of your uploaded images to be shown as avatar. It should not be larger than 64×64 pixels because that is the largest size shown for other people (80×80 pixels is used for the page header).
+Page.EditProfile.Avatar.Delete=Nessun avatar
+Page.EditProfile.Fields.Title=Campi personalizzati
+Page.EditProfile.Fields.Description=Here you can enter custom fields into your profile. These fields can contain anything you want and be as terse or as verbose as you wish. Just remember that when it comes to anonymity, sometimes less is more.
+Page.EditProfile.Fields.Button.Edit=modifica
+Page.EditProfile.Fields.Button.MoveUp=sposta sù
+Page.EditProfile.Fields.Button.MoveDown=sposta giù
+Page.EditProfile.Fields.Button.Delete=cancella
+Page.EditProfile.Fields.Button.ReallyDelete=cancella davvero
+Page.EditProfile.Fields.AddField.Title=Aggiungi campo
+Page.EditProfile.Fields.AddField.Label.Name=Nome:
+Page.EditProfile.Fields.AddField.Button.AddField=Aggiungi campo
+Page.EditProfile.Button.Save=Salva profilo
+Page.EditProfile.Error.DuplicateFieldName=Il nome “{fieldName}” esiste già.
+
+Page.EditProfileField.Title=Modifica campo del profilo - Sone
+Page.EditProfileField.Page.Title=Modifica campo del profilo
+Page.EditProfileField.Text=Inserisci un nome per questo campo del profilo.
+Page.EditProfileField.Error.DuplicateFieldName=Il nome del campo inserito è già in uso.
+Page.EditProfileField.Button.Save=Cambia
+Page.EditProfileField.Button.Reset=Rimetti il vecchio nome
+Page.EditProfileField.Button.Cancel=Non cambiare nome
+
+Page.DeleteProfileField.Title=Cancella il campo del profilo - Sone
+Page.DeleteProfileField.Page.Title=Cancella il campo del profilo
+Page.DeleteProfileField.Text=Vuoi davvero cancellare questo campo del profilo?
+Page.DeleteProfileField.Button.Yes=Si, cancella
+Page.DeleteProfileField.Button.No=No, non cancellare
+
+Page.CreatePost.Title=Scrivi un messaggio - Sone
+Page.CreatePost.Page.Title=Scrivi un messaggio
+Page.CreatePost.Label.Text=Testo del messaggio:
+Page.CreatePost.Button.Post=Pubblica!
+Page.CreatePost.Error.EmptyText=Non hai inserito alcun messaggio, che è un peccato. Dovresti provare a scrivere di più!
+
+Page.CreateReply.Title=Rispondi - Sone
+Page.CreateReply.Page.Title=Riscpondi
+Page.CreateReply.Error.EmptyText=Non hai inserito alcun messaggio, che è un peccato. Dovresti provare a scrivere di più!
+Page.CreateReply.Label.Text=Testo della risposta:
+Page.CreateReply.Button.Post=Pubblica risposta!
+
+Page.ViewSone.Title=Visualizza Sone - Sone
+Page.ViewSone.Page.TitleWithoutSone=Visualizza Sone sconosciuto
+Page.ViewSone.NoSone.Description=There is currently no known Sone with the ID {sone}. If you were looking for a specific Sone, make sure that it is visible in your web of trust:
+Page.ViewSone.UnknownSone.Description=This Sone has not yet been retrieved. Please check back in a short time.
+Page.ViewSone.UnknownSone.LinkToWebOfTrust=Even though the Sone is still unknown, its Web of Trust profile might already be available:
+Page.ViewSone.WriteAMessage=You can write a message to this Sone here. Please note that everybody will be able to read this message!
+Page.ViewSone.PostList.Title=Posts by {sone}
+Page.ViewSone.PostList.Text.NoPostYet=This Sone has not yet posted anything.
+Page.ViewSone.Profile.Title=Profilo
+Page.ViewSone.Profile.Label.Name=Nome
+Page.ViewSone.Profile.Label.Albums=Album
+Page.ViewSone.Profile.Albums.Text.All=Tutti gli album
+Page.ViewSone.Profile.Name.WoTLink=web of trust profile
+Page.ViewSone.Replies.Title=Messaggi a cui {sone} ha risposto
+
+Page.ViewPost.Title=Visualizza messaggio - Sone
+Page.ViewPost.Page.Title=Visualizza messaggi di {sone}
+Page.ViewPost.Page.TitleUnknownSone=Visualizza messaggio sconosciuto
+Page.ViewPost.Text.UnknownPost=This post has not yet been downloaded.
+
+Page.Like.Title=Like Post - Sone
+Page.Unlike.Title=Unlike Post - Sone
+
+Page.DeletePost.Title=Cancella Sone - Sone
+Page.DeletePost.Page.Title=Cancella Sone
+Page.DeletePost.Text.PostWillBeGone=Deleting a post will remove it from your Sone. It will not remove it from Freenet because that is not possible. Older versions of your Sone will always include the deleted post.
+Page.DeletePost.Button.Yes=Si, cancella.
+Page.DeletePost.Button.No=No, non cancellare.
+
+Page.DeleteReply.Title=Cancella risposta - Sone
+Page.DeleteReply.Page.Title=Cancella risposta
+Page.DeleteReply.Text.PostWillBeGone=Deleting a reply will remove it from your Sone. It will not remove it from Freenet because that is not possible. Older versions of your Sone will always include the deleted reply.
+Page.DeleteReply.Button.Yes=Si, cancella.
+Page.DeleteReply.Button.No=No, do not delete.
+
+Page.LockSone.Title=Lock Sone - Sone
+
+Page.UnlockSone.Title=Unlock Sone - Sone
+
+Page.FollowSone.Title=Follow Sone - Sone
+
+Page.UnfollowSone.Title=Unfollow Sone - Sone
+
+Page.ImageBrowser.Title=Image Browser - Sone
+Page.ImageBrowser.Album.Title=Album “{album}”
+Page.ImageBrowser.Album.Error.NotFound.Text=The requested album could not be found. It is possible that it has not yet been downloaded, or that it has been deleted.
+Page.ImageBrowser.Sone.Title=Albums of {sone}
+Page.ImageBrowser.Sone.Error.NotFound.Text=The requested Sone could not be found. It is possible that it has not yet been downloaded.
+Page.ImageBrowser.Header.Albums=Albums
+Page.ImageBrowser.Header.Images=Images
+Page.ImageBrowser.Link.All=All Sones
+Page.ImageBrowser.CreateAlbum.Button.CreateAlbum=Create Album
+Page.ImageBrowser.Album.Edit.Title=Edit Album
+Page.ImageBrowser.Album.Delete.Title=Delete Album
+Page.ImageBrowser.Album.Label.AlbumImage=Album Image:
+Page.ImageBrowser.Album.Label.Title=Title:
+Page.ImageBrowser.Album.Label.Description=Description:
+Page.ImageBrowser.Album.AlbumImage.Choose=Choose Album Image…
+Page.ImageBrowser.Album.Button.Save=Save Album
+Page.ImageBrowser.Album.Button.Delete=Delete Album
+Page.ImageBrowser.Image.Edit.Title=Edit Image
+Page.ImageBrowser.Image.Title.Label=Title:
+Page.ImageBrowser.Image.Description.Label=Description:
+Page.ImageBrowser.Image.Button.MoveLeft=◀
+Page.ImageBrowser.Image.Button.Save=Save Image
+Page.ImageBrowser.Image.Button.MoveRight=►
+Page.ImageBrowser.Image.Delete.Title=Delete Image
+Page.ImageBrowser.Image.Button.Delete=Delete Image
+
+Page.CreateAlbum.Title=Create Album - Sone
+Page.CreateAlbum.Page.Title=Create Album
+Page.CreateAlbum.Error.NameMissing=Sembra che ti sia dimenticato di inserire un nome per il nuovo album.
+
+Page.UploadImage.Title=Upload Image - Sone
+Page.UploadImage.Error.InvalidImage=The image you were trying to upload could not be recognized. Please upload only JPEG (*.jpg or *.jpeg), or PNG (*.png) images.
+
+Page.EditImage.Title=Edit Image - Sone
+
+Page.DeleteImage.Title=Delete Image - Sone
+Page.DeleteImage.Page.Title=Delete Image
+Page.DeleteImage.Text.ImageWillBeGone=This will remove the image “{image}” from your album “{album}”. If it has already been inserted into Freenet it can not be removed from there forcefully. Do you want to delete the image?
+Page.DeleteImage.Button.Yes=Si, cancella immagine.
+Page.DeleteImage.Button.No=No, don’t delete image.
+
+Page.EditAlbum.Title=Edit Album - Sone
+
+Page.DeleteAlbum.Title=Delete Album - Sone
+Page.DeleteAlbum.Page.Title=Delete Album
+Page.DeleteAlbum.Text.AlbumWillBeGone=This will remove your album “{title}”. Do you really want to do that?
+Page.DeleteAlbum.Button.Yes=Si, cancella album.
+Page.DeleteAlbum.Button.No=No, don’t delete album.
+
+Page.MarkAsKnown.Title=Mark as Known - Sone
+
+Page.Bookmark.Title=Bookmark - Sone
+Page.Unbookmark.Title=Remove Bookmark - Sone
+Page.Bookmarks.Title=Bookmarks - Sone
+Page.Bookmarks.Page.Title=Bookmarks
+Page.Bookmarks.Text.NoBookmarks=You don’t have any bookmarks defined right now. You can bookmark posts by clicking the star below the post.
+Page.Bookmarks.Text.PostsNotLoaded=Some of your bookmarked posts have not been shown because they could not be loaded. This can happen if you restarted Sone recently or if the originating Sone has deleted the post. If you are reasonable sure that these posts do not exist anymore, you can {link}unbookmark them{/link}.
+
+Page.Search.Title=Search - Sone
+Page.Search.Page.Title=Search Results
+Page.Search.Text.SoneHits=The following Sones match your search terms.
+Page.Search.Text.PostHits=The following posts match your search terms.
+Page.Search.Text.NoHits=No Sones or posts matched your search terms.
+
+Page.Rescue.Title=Rescue Sone - Sone
+Page.Rescue.Page.Title=Rescue Sone “{0}”
+Page.Rescue.Text.Description=The Rescue Mode lets you restore previous versions of your Sone. This can be necessary if your configuration was lost.
+Page.Rescue.Text.Procedure=The Rescue Mode works by fetching the latest inserted edition of your Sone. If an edition was successfully fetched it will be loaded into your Sone, letting you control your posts, profile, and other settings (you could do that in a second browser tab or window). If the fetched edition is not the one you want to restore, instruct the Rescue Mode to fetch the next older edition below.
+Page.Rescue.Text.Fetching=The Sone Rescuer is currently fetching edition {0} of your Sone.
+Page.Rescue.Text.Fetched=The Sone Rescuer has downloaded edition {0} of your Sone. Please check your posts, replies, and profile. If you like what the current Sone contains, just unlock it.
+Page.Rescue.Text.FetchedLast=The Sone rescuer has downloaded the last available edition. If it did not manage to restore your Sone you are probably out of luck now.
+Page.Rescue.Text.NotFetched=The Sone Rescuer could not download edition {0} of your Sone. Please either try again with edition {0}, or try the next older edition.
+Page.Rescue.Label.NextEdition=Next edition
+Page.Rescue.Button.Fetch=Fetch edition
+
+Page.NoPermission.Title=Unauthorized Access - Sone
+Page.NoPermission.Page.Title=Unauthorized Access
+Page.NoPermission.Text.NoPermission=You tried to do something that you do not have sufficient authorization for. Please refrain from such actions in the future or we will be forced to take counter-measures!
+
+Page.EmptyImageTitle.Title=Title Must Not Be Empty - Sone
+Page.EmptyImageTitle.Page.Title=Title Must Not Be Empty
+Page.EmptyImageTitle.Text.EmptyImageTitle=You have to give your image a title. Please go back to the previous page and enter a title.
+
+Page.EmptyAlbumTitle.Title=Title Must Not Be Empty - Sone
+Page.EmptyAlbumTitle.Page.Title=Title Must Not Be Empty
+Page.EmptyAlbumTitle.Text.EmptyAlbumTitle=You have to give your album a title. Please go back to the previous page and enter a title.
+
+Page.DismissNotification.Title=Dismiss Notification - Sone
+
+Page.WotPluginMissing.Text.WotRequired=Because the Web of Trust is an integral part of Sone, the Web of Trust plugin has to be loaded in order to run Sone.
+Page.WotPluginMissing.Text.LoadPlugin=Please load the Web of Trust plugin in the {link}plugin manager{/link}.
+
+Page.Logout.Title=Logout - Sone
+
+Page.Invalid.Title=Invalid Action Performed - Sone
+Page.Invalid.Page.Title=Invalid Action Performed
+Page.Invalid.Text=An invalid action was performed, or the action was valid but the parameters were not. Please go back to the {link}index page{/link} and try again. If the error persists you have probably found a bug.
+
+Page.Metrics.Title=Metrics
+Page.Metrics.Page.Title=Metrics
+Page.Metrics.SoneInsertDuration.Title=Sone Insert Duration
+Page.Metrics.SoneParseDuration.Title=Sone Parse Duration
+Page.Metrics.ConfigurationSaveDuration.Title=Configuration Save Duration
+
+View.Search.Button.Search=Search
+
+View.CreateSone.Text.WotIdentityRequired=To create a Sone you need an identity from the {link}Web of Trust plugin{/link}.
+View.CreateSone.Select.Default=Select an identity
+View.CreateSone.Text.NoIdentities=You do not have any Web of Trust identities. Please head over to the {link}Web of Trust plugin{/link} and create an identity.
+View.CreateSone.Text.NoNonSoneIdentities=You do not have any Web of Trust identities that are not already a Sone. Use one of the remaining Web of Trust identities to create a new Sone or head over to the {link}Web of Trust plugin{/link} to create a new identity.
+View.CreateSone.Button.Create=Create Sone
+View.CreateSone.Text.Error.NoIdentity=You have not selected an identity.
+
+View.Sone.Label.LastUpdate=Last update:
+View.Sone.Text.UnknownDate=unknown
+View.Sone.Stats.Posts={0,number} {0,choice,0#posts|1#post|1<posts}
+View.Sone.Stats.Replies={0,number} {0,choice,0#replies|1#reply|1<replies}
+View.Sone.Stats.Images={0,number} {0,choice,0#images|1#image|1<images}
+View.Sone.Button.UnlockSone=unlock
+View.Sone.Button.UnlockSone.Tooltip=Allow this Sone to be inserted now
+View.Sone.Button.LockSone=lock
+View.Sone.Button.LockSone.Tooltip=Prevents this Sone from being inserted right now
+View.Sone.Button.UnfollowSone=unfollow
+View.Sone.Button.FollowSone=follow
+View.Sone.Status.Modified=This Sone was modified and waits to be inserted.
+View.Sone.Status.Unknown=This Sone has not yet been retrieved.
+View.Sone.Status.Idle=This Sone is idle, i.e. not being inserted or downloaded.
+View.Sone.Status.Downloading=This Sone is currently being downloaded.
+View.Sone.Status.Inserting=This Sone is currently being inserted.
+
+View.SoneMenu.Link.AllAlbums=all albums
+View.SoneMenu.WebOfTrustLink=web of trust profile
+
+View.Post.UnknownAuthor=(sconociuto)
+View.Post.WebOfTrustLink=WoT Profile
+View.Post.Permalink=link post
+View.Post.PermalinkAuthor=link author
+View.Post.Bookmarks.PostIsBookmarked=Post is bookmarked, click to remove from bookmarks
+View.Post.Bookmarks.PostIsNotBookmarked=Post is not bookmarked, click to bookmark
+View.Post.DeleteLink=Delete
+View.Post.SendReply=Post Reply!
+View.Post.Reply.DeleteLink=Delete
+View.Post.LikeLink=Like
+View.Post.UnlikeLink=Unlike
+View.Post.ShowSource=Toggle Parser
+View.Post.NotDownloaded=This post has not yet been downloaded, or it has been deleted.
+View.Post.ShowMore=show more
+View.Post.ShowLess=show less
+
+View.UpdateStatus.Text.ChooseSenderIdentity=Choose the sender identity
+
+View.CreateAlbum.Title=Create Album
+View.CreateAlbum.Label.Name=Nome:
+View.CreateAlbum.Label.Description=Description:
+
+View.UploadImage.Title=Upload Image
+View.UploadImage.Label.Title=Title:
+View.UploadImage.Label.Description=Description:
+View.UploadImage.Button.UploadImage=Upload Image
+
+View.Time.InTheFuture=in the future
+View.Time.AFewSecondsAgo=a few seconds ago
+View.Time.HalfAMinuteAgo=all'incirca mezzo minuto fa
+View.Time.AMinuteAgo=all'incirca un minuto fa
+View.Time.XMinutesAgo={0} minutes ago
+View.Time.HalfAnHourAgo=half an hour ago
+View.Time.AnHourAgo=all'incirca un'ora fa
+View.Time.XHoursAgo={0} hours ago
+View.Time.ADayAgo=all'incirca un giorno fa
+View.Time.XDaysAgo={0} days ago
+View.Time.AWeekAgo=all'incirca una settimana fa
+View.Time.XWeeksAgo={0} weeks ago
+View.Time.AMonthAgo=all'incirca un mese fa
+View.Time.XMonthsAgo={0} months ago
+View.Time.AYearAgo=all'incirca un anno fa
+View.Time.XYearsAgo={0} years ago
+
+WebInterface.DefaultText.StatusUpdate=What’s on your mind?
+WebInterface.DefaultText.Message=Write a Message…
+WebInterface.DefaultText.Reply=Write a Reply…
+WebInterface.DefaultText.FirstName=Nome
+WebInterface.DefaultText.MiddleName=Secondo nome(i)
+WebInterface.DefaultText.LastName=Cognome
+WebInterface.DefaultText.BirthDay=Day
+WebInterface.DefaultText.BirthMonth=Month
+WebInterface.DefaultText.BirthYear=Year
+WebInterface.DefaultText.FieldName=Nome del campo
+WebInterface.DefaultText.Option.InsertionDelay=Time to wait after a Sone is modified before insert (in seconds)
+WebInterface.DefaultText.Search=What are you looking for?
+WebInterface.DefaultText.CreateAlbum.Name=Album title
+WebInterface.DefaultText.CreateAlbum.Description=Album description
+WebInterface.DefaultText.EditAlbum.Title=Album title
+WebInterface.DefaultText.EditAlbum.Description=Album description
+WebInterface.DefaultText.UploadImage.Title=Image title
+WebInterface.DefaultText.UploadImage.Description=Image description
+WebInterface.DefaultText.EditImage.Title=Image title
+WebInterface.DefaultText.EditImage.Description=Image description
+WebInterface.DefaultText.Option.PostsPerPage=Number of posts to show on a page
+WebInterface.DefaultText.Option.ImagesPerPage=Number of images to show on a page
+WebInterface.DefaultText.Option.CharactersPerPost=Number of characters a post must have to be shortened
+WebInterface.DefaultText.Option.PostCutOffLength=Number of characters for the snippet of the shortened post
+WebInterface.Button.Comment=Comment
+WebInterface.Confirmation.DeletePostButton=Si, cancella!
+WebInterface.Confirmation.DeleteReplyButton=Si, cancella!
+WebInterface.SelectBox.Choose=Choose…
+WebInterface.SelectBox.Yes=Si
+WebInterface.SelectBox.No=No
+WebInterface.ClickToShow.Replies=Click here to show hidden replies.
+WebInterface.VersionInformation.CurrentVersion=Current Version:
+WebInterface.VersionInformation.LatestVersion=Latest Version:
+WebInterface.VersionInformation.Homepage=Homepage
+
+Notification.ClickHereToRead=Clicca qui per leggere il testo completo della notifica
+Notification.FirstStart.Text=This seems to be the first time you start Sone. To start, create a new Sone from a web of trust identity and start following other Sones.
+Notification.Startup.Text=Sone is currently starting up. It may take a while to retrieve all identities and Sones from the web of trust. If you are missing some elements, please be patient, they will probably reappear very soon.
+Notification.ConfigNotRead.Text=The configuration file “sone.properties” could not be read, probably because it was not saved correctly. This can happen on versions prior to Sone 0.3.3 and there is nothing you can do about it.
+Notification.Button.Dismiss=Dismiss
+Notification.NewSone.ShortText=New Sones have been discovered:
+Notification.NewSone.Text=New Sones have been discovered:
+Notification.NewPost.ShortText=New posts have been discovered.
+Notification.NewPost.Text=New posts have been discovered by the following Sones:
+Notification.NewPost.Button.MarkRead=Mark as read
+Notification.NewReply.ShortText=New replies have been discovered.
+Notification.NewReply.Text=New replies have been discovered for posts by the following Sones:
+Notification.SoneIsBeingRescued.Text=The following Sones are currently being rescued:
+Notification.SoneRescued.Text=The following Sones have been rescued:
+Notification.SoneRescued.Text.RememberToUnlock=Please remember to control the posts and replies you have given and don’t forget to unlock your Sones!
+Notification.LockedSones.Text=The following Sones have been locked for more than 5 minutes. Please check if you really want to keep these Sones locked:
+Notification.NewVersion.Text=Version {version} of the Sone plugin was found. Download it from USK@nwa8lHa271k2QvJ8aa0Ov7IHAV-DFOCFgmDt3X6BpCI,DuQSUZiI~agF8c-6tjsFFGuZ8eICrzWCILB60nT8KKo,AQACAAE/sone/{edition}​!
+Notification.NewVersion.Disruptive.Text=It is {em}highly recommended{/em} that you update to this version as it may be possible that you are missing a lot of content with your current version!
+Notification.InsertingImages.Text=The following images are being inserted:
+Notification.InsertedImages.Text=The following images have been inserted:
+Notification.ImageInsertFailed.Text=The following images could not be inserted:
+Notification.Mention.ShortText=You have been mentioned.
+Notification.Mention.Text=You have been mentioned in the following posts:
+Notification.SoneIsInserting.Text=Your Sone sone://{0} is now being inserted.
+Notification.SoneIsInserted.Text=Your Sone sone://{0} has been inserted in {1,number} {1,choice,0#seconds|1#second|1<seconds}.
+Notification.SoneInsertAborted.Text=Your Sone sone://{0} could not be inserted.
+Notification.SoneLockedOnStartup.Text=Versions prior to v81 had a bug that resulted in empty Sones. To prevent buggy Sones from being inserted they have automatically been locked. Please check your Sones, use the Rescue Mode if necessary, and unlock your Sones once you are satisfied with the results. Locked Sones are:
index 996f7f4..535140f 100644 (file)
@@ -74,6 +74,8 @@ Page.Options.Option.FcpFullAccessRequired.Description=許可されたホスト
 Page.Options.Option.FcpFullAccessRequired.Value.No=いいえ
 Page.Options.Option.FcpFullAccessRequired.Value.Writing=書き込みのアクセスの場合
 Page.Options.Option.FcpFullAccessRequired.Value.Always=常に
+Page.Options.Section.WebOfTrustOptions.Title=Web of Trust Settings
+Page.Options.Option.StrictFiltering.Description=Apply stricter filtering of identities. When this is selected, Sone will completely ignore identities that have a negative trust value for any of your local identities, otherwise they will be shown as long as at least one of your local identities has a positive trust value for them. (Note that it will take a couple of minutes for this setting to show effect.)
 Page.Options.Section.Cleaning.Title=クリーンアップ
 Page.Options.Option.ClearOnNextRestart.Description=次回のプラグインの再起動時に設定を初期化する。注意:{strong}これを行うとあなたの全てのSoneが破棄されます{/strong}。実行の前に必要なものは全てバックアップされていることを確認してください。また、次項の設定も同時に有効にする必要があります。
 Page.Options.Option.ReallyClearOnNextRestart.Description={strong}本当{/strong}にプラグインの設定を初期化する場合はこの項目も有効にしてください。
index 9ddb54d..967faca 100644 (file)
@@ -74,6 +74,8 @@ Page.Options.Option.FcpFullAccessRequired.Description=Påkrev FCP tilkobling fra
 Page.Options.Option.FcpFullAccessRequired.Value.No=Nei
 Page.Options.Option.FcpFullAccessRequired.Value.Writing=For skrivetilgang
 Page.Options.Option.FcpFullAccessRequired.Value.Always=Alltid
+Page.Options.Section.WebOfTrustOptions.Title=Web of Trust Settings
+Page.Options.Option.StrictFiltering.Description=Apply stricter filtering of identities. When this is selected, Sone will completely ignore identities that have a negative trust value for any of your local identities, otherwise they will be shown as long as at least one of your local identities has a positive trust value for them. (Note that it will take a couple of minutes for this setting to show effect.)
 Page.Options.Section.Cleaning.Title=Rydd opp
 Page.Options.Option.ClearOnNextRestart.Description=Nullstill konfigurasjonen til Sone-tillegget ved neste omstart. Advarsel! {strong}Dette vil ødelegge alle dine Soner{/strong} så forsikre deg om at du har tatt backup av alt du fremdeles trenger! Du vil måtte sette det neste valget til 'true' for faktisk å nullstille.
 Page.Options.Option.ReallyClearOnNextRestart.Description=Denne innstillingen må bli satt til «ja» hvis du virkelig {strong}virkelig{/strong} ønsker å slette Sone-tilleggets innstillinger ved neste omstart.
index cbc8f44..253497b 100644 (file)
@@ -74,6 +74,8 @@ Page.Options.Option.FcpFullAccessRequired.Description=Wymagane połączenie FCP
 Page.Options.Option.FcpFullAccessRequired.Value.No=Nie
 Page.Options.Option.FcpFullAccessRequired.Value.Writing=Dostęp z zapisem
 Page.Options.Option.FcpFullAccessRequired.Value.Always=Zawsze
+Page.Options.Section.WebOfTrustOptions.Title=Web of Trust Settings
+Page.Options.Option.StrictFiltering.Description=Apply stricter filtering of identities. When this is selected, Sone will completely ignore identities that have a negative trust value for any of your local identities, otherwise they will be shown as long as at least one of your local identities has a positive trust value for them. (Note that it will take a couple of minutes for this setting to show effect.)
 Page.Options.Section.Cleaning.Title=Wyczyść
 Page.Options.Option.ClearOnNextRestart.Description=Przy kolejnym uruchomieniu zresetuj ustawienia wtyczki Sone. Uwaga!{strong}Wszystkie twoje profile Sone zostaną zniszczone{/strong}upewnij się, że wykonano kopie zapasowe wszystkich niezbędnych danych! Wymagane wybranie odpowiedzi "tak" w następnej opcji.
 Page.Options.Option.ReallyClearOnNextRestart.Description=Wymagane wybranie opcji"tak"jeśli {strong}naprawdę{/strong} jesteś zdecydowany zresetować ustawienia wtyczki konfiguracyjnej przy kolejnym uruchomieniu.
index 7e3c113..354c0d2 100644 (file)
@@ -74,6 +74,8 @@ Page.Options.Option.FcpFullAccessRequired.Description=Требовать сое
 Page.Options.Option.FcpFullAccessRequired.Value.No=Нет
 Page.Options.Option.FcpFullAccessRequired.Value.Writing=Для доступа на запись
 Page.Options.Option.FcpFullAccessRequired.Value.Always=Всегда
+Page.Options.Section.WebOfTrustOptions.Title=Web of Trust Settings
+Page.Options.Option.StrictFiltering.Description=Apply stricter filtering of identities. When this is selected, Sone will completely ignore identities that have a negative trust value for any of your local identities, otherwise they will be shown as long as at least one of your local identities has a positive trust value for them. (Note that it will take a couple of minutes for this setting to show effect.)
 Page.Options.Section.Cleaning.Title=Очистка
 Page.Options.Option.ClearOnNextRestart.Description=Сбрасывает настройки дополнения Sone во время следующего перезапуска. Предупреждение! {strong}Это уничтожит все ваши Sone{/strong}, так что удостоверьтесь, что вы сохранили резервные копии всего, что вам еще нужно! Кроме того, вам нужно установить следующую настройку в значение true, чтобы действительно это сделать.
 Page.Options.Option.ReallyClearOnNextRestart.Description=Эта опция должна быть установлена в "yes", если вы действительно, {strong}действительно{/strong} хотите очистить настройки дополнения во время следующего перезапуска.
index c4baf2a..9ef5ceb 100644 (file)
                        </select>
                </p>
 
+               <h2><%= Page.Options.Section.WebOfTrustOptions.Title|l10n|html></h2>
+
+               <p><input type="checkbox" name="strict-filtering"<%if strict-filtering> checked="checked"<%/if> /> <%= Page.Options.Option.StrictFiltering.Description|l10n|html></p>
+
                <p><button type="submit"><%= Page.Options.Button.Save|l10n|html></button></p>
 
        </form>
diff --git a/src/test/java/net/pterodactylus/sone/core/SoneUriTest.java b/src/test/java/net/pterodactylus/sone/core/SoneUriTest.java
deleted file mode 100644 (file)
index 3525c9b..0000000
+++ /dev/null
@@ -1,36 +0,0 @@
-package net.pterodactylus.sone.core;
-
-import static freenet.keys.InsertableClientSSK.createRandom;
-import static net.pterodactylus.sone.core.SoneUri.create;
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.is;
-import static org.hamcrest.Matchers.nullValue;
-
-import freenet.crypt.DummyRandomSource;
-import freenet.keys.FreenetURI;
-
-import org.junit.Test;
-
-/**
- * Unit test for {@link SoneUri}.
- */
-public class SoneUriTest {
-
-       @Test
-       public void callConstructorForIncreasedTestCoverage() {
-               new SoneUri();
-       }
-
-       @Test
-       public void returnedUriHasCorrectDocNameAndMetaStrings() {
-               FreenetURI uri = createRandom(new DummyRandomSource(), "test-0").getURI().uskForSSK();
-               assertThat(create(uri.toString()).getDocName(), is("Sone"));
-               assertThat(create(uri.toString()).getAllMetaStrings(), is(new String[0]));
-       }
-
-       @Test
-       public void malformedUriReturnsNull() {
-               assertThat(create("not a key"), nullValue());
-       }
-
-}
index d7d9b48..5f8d93d 100644 (file)
@@ -6,7 +6,6 @@ import static org.hamcrest.MatcherAssert.assertThat;
 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.*;
 
 import java.util.concurrent.CountDownLatch;
@@ -17,7 +16,6 @@ import net.pterodactylus.sone.core.WebOfTrustUpdaterImpl.SetPropertyJob;
 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.WebOfTrustConnector;
 
diff --git a/src/test/java/net/pterodactylus/sone/freenet/wot/IdentityManagerTest.kt b/src/test/java/net/pterodactylus/sone/freenet/wot/IdentityManagerTest.kt
deleted file mode 100644 (file)
index 3768efd..0000000
+++ /dev/null
@@ -1,33 +0,0 @@
-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<EventBus>()
-       private val webOfTrustConnector = mock<WebOfTrustConnector>()
-       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()
-       }
-
-}
index 72f0bf7..e85c131 100644 (file)
@@ -21,7 +21,6 @@ import net.pterodactylus.util.web.Method;
 import net.pterodactylus.util.web.Page;
 import net.pterodactylus.util.web.Response;
 
-import freenet.clients.http.SessionManager;
 import freenet.clients.http.ToadletContext;
 import freenet.support.api.HTTPRequest;
 
@@ -69,8 +68,7 @@ public class DebugLoadersTest {
                Method method = Method.GET;
                HTTPRequest httpRequest = mock(HTTPRequest.class);
                ToadletContext toadletContext = mock(ToadletContext.class);
-               SessionManager sessionManager = mock(SessionManager.class);
-               FreenetRequest request = new FreenetRequest(uri, method, httpRequest, toadletContext, sessionManager);
+               FreenetRequest request = new FreenetRequest(uri, method, httpRequest, toadletContext);
                OutputStream outputStream = new ByteArrayOutputStream();
                Response response = new Response(outputStream);
                page.handleRequest(request, response);
index d91e9e3..5c71b73 100644 (file)
@@ -21,7 +21,6 @@ import net.pterodactylus.util.web.Method;
 import net.pterodactylus.util.web.Page;
 import net.pterodactylus.util.web.Response;
 
-import freenet.clients.http.SessionManager;
 import freenet.clients.http.ToadletContext;
 import freenet.support.api.HTTPRequest;
 
@@ -50,8 +49,7 @@ public class DefaultLoadersTest {
                Method method = Method.GET;
                HTTPRequest httpRequest = mock(HTTPRequest.class);
                ToadletContext toadletContext = mock(ToadletContext.class);
-               SessionManager sessionManager = mock(SessionManager.class);
-               FreenetRequest request = new FreenetRequest(uri, method, httpRequest, toadletContext, sessionManager);
+               FreenetRequest request = new FreenetRequest(uri, method, httpRequest, toadletContext);
                OutputStream outputStream = new ByteArrayOutputStream();
                Response response = new Response(outputStream);
                staticPage.handleRequest(request, response);
index 65fb3a6..bd571e7 100644 (file)
@@ -29,7 +29,6 @@ import net.pterodactylus.sone.data.Image;
 import net.pterodactylus.sone.data.Post;
 import net.pterodactylus.sone.data.PostReply;
 
-import com.google.common.base.Optional;
 import org.hamcrest.Description;
 import org.hamcrest.Matcher;
 import org.hamcrest.TypeSafeDiagnosingMatcher;
index 35623a7..0319db9 100644 (file)
@@ -3,8 +3,6 @@ package net.pterodactylus.sone.text;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.is;
 
-import org.hamcrest.MatcherAssert;
-import org.hamcrest.Matchers;
 import org.junit.Test;
 
 /**
diff --git a/src/test/java/net/pterodactylus/sone/utils/DefaultOptionTest.java b/src/test/java/net/pterodactylus/sone/utils/DefaultOptionTest.java
deleted file mode 100644 (file)
index 3c82c40..0000000
+++ /dev/null
@@ -1,85 +0,0 @@
-package net.pterodactylus.sone.utils;
-
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.is;
-import static org.hamcrest.Matchers.nullValue;
-
-import javax.annotation.Nullable;
-
-import com.google.common.base.Predicate;
-import org.junit.Test;
-
-/**
- * Unit test for {@link DefaultOption}.
- */
-public class DefaultOptionTest {
-
-       private final Object defaultValue = new Object();
-       private final Object acceptedValue = new Object();
-       private final Predicate<Object> matchesAcceptedValue = new Predicate<Object>() {
-               @Override
-               public boolean apply(@Nullable Object object) {
-                       return acceptedValue.equals(object);
-               }
-       };
-
-       @Test
-       public void defaultOptionReturnsDefaultValueWhenUnset() {
-               DefaultOption<Object> defaultOption = new DefaultOption<>(defaultValue);
-               assertThat(defaultOption.get(), is(defaultValue));
-       }
-
-       @Test
-       public void defaultOptionReturnsNullForRealWhenUnset() {
-               DefaultOption<Object> defaultOption = new DefaultOption<>(defaultValue);
-               assertThat(defaultOption.getReal(), nullValue());
-       }
-
-       @Test
-       public void defaultOptionWillReturnSetValue() {
-               DefaultOption<Object> defaultOption = new DefaultOption<>(defaultValue);
-               Object newValue = new Object();
-               defaultOption.set(newValue);
-               assertThat(defaultOption.get(), is(newValue));
-       }
-
-       @Test
-       public void defaultOptionWithValidatorAcceptsValidValues() {
-               DefaultOption<Object> defaultOption = new DefaultOption<>(defaultValue, matchesAcceptedValue);
-               defaultOption.set(acceptedValue);
-               assertThat(defaultOption.get(), is(acceptedValue));
-       }
-
-       @Test(expected = IllegalArgumentException.class)
-       public void defaultOptionWithValidatorRejectsInvalidValues() {
-               DefaultOption<Object> defaultOption = new DefaultOption<>(defaultValue, matchesAcceptedValue);
-               defaultOption.set(new Object());
-       }
-
-       @Test
-       public void defaultOptionValidatesObjectsCorrectly() {
-               DefaultOption<Object> defaultOption = new DefaultOption<>(defaultValue, matchesAcceptedValue);
-               assertThat(defaultOption.validate(acceptedValue), is(true));
-               assertThat(defaultOption.validate(new Object()), is(false));
-       }
-
-       @Test
-       public void settingToNullWillRestoreDefaultValue() {
-               DefaultOption<Object> defaultOption = new DefaultOption<>(defaultValue);
-               defaultOption.set(null);
-               assertThat(defaultOption.get(), is(defaultValue));
-       }
-
-       @Test
-       public void validateWithoutValidatorWillValidateNull() {
-               DefaultOption<Object> defaultOption = new DefaultOption<>(defaultValue);
-               assertThat(defaultOption.validate(null), is(true));
-       }
-
-       @Test
-       public void validateWithValidatorWillValidateNull() {
-               DefaultOption<Object> defaultOption = new DefaultOption<>(defaultValue, matchesAcceptedValue);
-               assertThat(defaultOption.validate(null), is(true));
-       }
-
-}
diff --git a/src/test/java/net/pterodactylus/sone/utils/IntegerRangePredicateTest.java b/src/test/java/net/pterodactylus/sone/utils/IntegerRangePredicateTest.java
deleted file mode 100644 (file)
index 2bea3f7..0000000
+++ /dev/null
@@ -1,53 +0,0 @@
-package net.pterodactylus.sone.utils;
-
-import static net.pterodactylus.sone.utils.IntegerRangePredicate.range;
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.is;
-
-import net.pterodactylus.sone.test.TestUtil;
-
-import org.junit.Test;
-
-/**
- * Unit test for {@link IntegerRangePredicate}.
- */
-public class IntegerRangePredicateTest {
-
-       private final IntegerRangePredicate predicate =
-                       new IntegerRangePredicate(-50, 50);
-
-       @Test
-       public void predicateMatchesNumberWithinBounds() {
-               assertThat(predicate.apply(17), is(true));
-       }
-
-       @Test
-       public void predicateMatchesLowerBoundary() {
-               assertThat(predicate.apply(-50), is(true));
-       }
-
-       @Test
-       public void predicateDoesNotMatchOneBelowLowerBoundary() {
-               assertThat(predicate.apply(-51), is(false));
-       }
-
-       @Test
-       public void predicateMatchesUpperBoundary() {
-               assertThat(predicate.apply(50), is(true));
-       }
-
-       @Test
-       public void predicateDoesNotMatchesOneAboveUpperBoundary() {
-               assertThat(predicate.apply(51), is(false));
-       }
-
-       @Test
-       public void staticCreatorMethodCreatesPredicate() {
-               IntegerRangePredicate predicate = range(-50, 50);
-               assertThat(TestUtil.<Integer>getPrivateField(predicate, "lowerBound"),
-                               is(-50));
-               assertThat(TestUtil.<Integer>getPrivateField(predicate, "upperBound"),
-                               is(50));
-       }
-
-}
index 90e3fa6..29ade5f 100644 (file)
@@ -1,6 +1,5 @@
 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.*
index 9eb123c..39273d0 100644 (file)
@@ -52,7 +52,8 @@ class CoreTest {
                val eventBus = mock<EventBus>()
                val database = mock<Database>()
                val metricRegistry = MetricRegistry()
-               val core = Core(configuration, freenetInterface, identityManager, soneDownloader, imageInserter, updateChecker, webOfTrustUpdater, eventBus, database, metricRegistry)
+               val soneUriCreator = SoneUriCreator()
+               val core = Core(configuration, freenetInterface, identityManager, soneDownloader, imageInserter, updateChecker, webOfTrustUpdater, eventBus, database, metricRegistry, soneUriCreator)
                val ownIdentity = mock<OwnIdentity>()
                val identity = mock<Identity>()
                whenever(identity.id).thenReturn("sone-id")
@@ -169,7 +170,8 @@ class CoreTest {
                val webOfTrustUpdater = mock<WebOfTrustUpdater>()
                val database = mock<Database>()
                val metricRegistry = MetricRegistry()
-               return Core(configuration, freenetInterface, identityManager, soneDownloader, imageInserter, updateChecker, webOfTrustUpdater, eventBus, database, metricRegistry)
+               val soneUriCreator = SoneUriCreator()
+               return Core(configuration, freenetInterface, identityManager, soneDownloader, imageInserter, updateChecker, webOfTrustUpdater, eventBus, database, metricRegistry, soneUriCreator)
        }
 
 }
index e02cddb..9cbaff0 100644 (file)
@@ -16,6 +16,7 @@ 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.freenet.wot.DefaultIdentity
 import net.pterodactylus.sone.test.*
 import net.pterodactylus.sone.test.Matchers.*
 import net.pterodactylus.sone.test.TestUtil.*
@@ -43,6 +44,10 @@ class FreenetInterfaceTest {
        @JvmField
        val expectionException: ExpectedException = ExpectedException.none()
 
+       @Rule
+       @JvmField
+       val silencedLogging = silencedLogging()
+
        @Suppress("UnstableApiUsage")
        private val eventBus = mock<EventBus>()
        private val node = mock<Node>()
@@ -52,7 +57,7 @@ class FreenetInterfaceTest {
        private val uskManager = mock<USKManager>()
        private val sone = mock<Sone>()
        private val callbackCaptor: ArgumentCaptor<USKCallback> = forClass(USKCallback::class.java)
-       private val image = mock<Image>()
+       private val image: Image = ImageImpl()
        private val insertToken: InsertToken
        private val bucket = mock<Bucket>()
        private val clientGetCallback: ArgumentCaptor<ClientGetCallback> = forClass(ClientGetCallback::class.java)
@@ -60,6 +65,7 @@ class FreenetInterfaceTest {
        private val fetchResult = mock<FetchResult>()
        private val backgroundFetchCallback = mock<BackgroundFetchCallback>()
        private val clientGetter = mock<ClientGetter>()
+       private val soneUriCreator = SoneUriCreator()
        private val freenetInterface: FreenetInterface
 
        init {
@@ -68,7 +74,7 @@ class FreenetInterfaceTest {
                setField(node, "random", randomSource)
                setField(nodeClientCore, "uskManager", uskManager)
                setField(nodeClientCore, "clientContext", mock<ClientContext>())
-               freenetInterface = FreenetInterface(eventBus, node)
+               freenetInterface = FreenetInterface(eventBus, node, soneUriCreator)
                insertToken = freenetInterface.InsertToken(image)
                insertToken.setBucket(bucket)
        }
@@ -83,7 +89,7 @@ class FreenetInterfaceTest {
        fun setupSone() {
                val insertSsk = createRandom(randomSource, "test-0")
                whenever(sone.id).thenReturn(insertSsk.uri.routingKey.asFreenetBase64)
-               whenever(sone.requestUri).thenReturn(insertSsk.uri.uskForSSK())
+               whenever(sone.identity).thenReturn(DefaultIdentity("id", "name", insertSsk.uri.toString()))
        }
 
        @Before
@@ -184,7 +190,6 @@ class FreenetInterfaceTest {
 
        @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))
        }
@@ -258,16 +263,15 @@ class FreenetInterfaceTest {
        }
 
        @Test
-       fun `unregistering aregistered sone unregisters the sone`() {
-               freenetInterface.registerActiveUsk(sone.requestUri, mock())
+       fun `unregistering a registered sone unregisters the sone`() {
+               freenetInterface.registerActiveUsk(soneUriCreator.getRequestUri(sone), 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)
+       fun `unregistering a sone with a wrong request key will not unsubscribe`() {
+               freenetInterface.registerUsk(FreenetURI("KSK@GPLv3.txt"), null)
                freenetInterface.unregisterUsk(sone)
                verify(uskManager, never()).unsubscribe(any(USK::class.java), any(USKCallback::class.java))
        }
index 60bb7e2..6e2c7c5 100644 (file)
@@ -2,8 +2,8 @@ package net.pterodactylus.sone.core
 
 import net.pterodactylus.sone.core.FreenetInterface.InsertToken
 import net.pterodactylus.sone.core.FreenetInterface.InsertTokenSupplier
-import net.pterodactylus.sone.data.Image
 import net.pterodactylus.sone.data.TemporaryImage
+import net.pterodactylus.sone.data.impl.*
 import net.pterodactylus.sone.test.getInstance
 import net.pterodactylus.sone.test.mock
 import net.pterodactylus.sone.test.whenever
@@ -23,7 +23,7 @@ import org.mockito.Mockito.verify
 class ImageInserterTest {
 
        private val temporaryImage = mock<TemporaryImage>().apply { whenever(id).thenReturn("image-id") }
-       private val image = mock<Image>().apply { whenever(id).thenReturn("image-id") }
+       private val image = ImageImpl("image-id")
        private val freenetInterface = mock<FreenetInterface>()
        private val insertToken = mock<InsertToken>()
        private val insertTokenSupplier: InsertTokenSupplier = mock<InsertTokenSupplier>().apply { whenever(apply(any())).thenReturn(insertToken) }
index 0cd7e6d..cb8df38 100644 (file)
@@ -28,6 +28,7 @@ class PreferencesLoaderTest {
                setupBooleanValue("RequireFullAccess", true)
                setupBooleanValue("ActivateFcpInterface", true)
                setupIntValue("FcpFullAccessRequired", 1)
+               setupBooleanValue("StrictFiltering", true)
        }
 
        private fun setupIntValue(optionName: String, value: Int) {
@@ -49,6 +50,7 @@ class PreferencesLoaderTest {
                assertThat(preferences.requireFullAccess, equalTo(true))
                assertThat(preferences.fcpInterfaceActive, equalTo(true))
                assertThat(preferences.fcpFullAccessRequired, equalTo(FullAccessRequired.WRITING))
+               assertThat(preferences.strictFiltering, equalTo(true))
        }
 
        @Test
index 245ff03..dafe782 100644 (file)
@@ -1,28 +1,35 @@
 package net.pterodactylus.sone.core
 
-import com.google.common.eventbus.*
-import net.pterodactylus.sone.core.event.*
-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.*
-import org.mockito.ArgumentMatchers.any
-import org.mockito.Mockito.atLeastOnce
-import org.mockito.Mockito.never
-import org.mockito.Mockito.verify
+import com.google.common.eventbus.EventBus
+import com.google.common.eventbus.Subscribe
+import net.pterodactylus.sone.core.event.InsertionDelayChangedEvent
+import net.pterodactylus.sone.core.event.StrictFilteringActivatedEvent
+import net.pterodactylus.sone.core.event.StrictFilteringDeactivatedEvent
+import net.pterodactylus.sone.fcp.FcpInterface.FullAccessRequired
+import net.pterodactylus.sone.fcp.FcpInterface.FullAccessRequired.ALWAYS
+import net.pterodactylus.sone.fcp.FcpInterface.FullAccessRequired.NO
+import net.pterodactylus.sone.fcp.FcpInterface.FullAccessRequired.WRITING
+import net.pterodactylus.sone.fcp.event.FcpInterfaceActivatedEvent
+import net.pterodactylus.sone.fcp.event.FcpInterfaceDeactivatedEvent
+import net.pterodactylus.sone.fcp.event.FullAccessRequiredChanged
+import net.pterodactylus.util.config.Configuration
+import net.pterodactylus.util.config.MapConfigurationBackend
+import org.hamcrest.Matcher
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.emptyIterable
+import org.hamcrest.Matchers.equalTo
+import org.hamcrest.Matchers.hasItem
+import org.hamcrest.Matchers.instanceOf
+import org.hamcrest.Matchers.nullValue
+import org.junit.Test
 
 /**
  * Unit test for [Preferences].
  */
 class PreferencesTest {
 
-       private val eventBus = mock<EventBus>()
+       private val eventBus = EventBus()
        private val preferences = Preferences(eventBus)
-       private val eventsCaptor = capture<Any>()
 
        @Test
        fun `preferences retain insertion delay`() {
@@ -32,9 +39,14 @@ class PreferencesTest {
 
        @Test
        fun `preferences sends event on setting insertion delay`() {
+               val events = mutableListOf<InsertionDelayChangedEvent>()
+               eventBus.register(object {
+                       @Subscribe
+                       fun insertionDelayChangedEvent(event: InsertionDelayChangedEvent) =
+                                       events.add(event)
+               })
                preferences.newInsertionDelay = 15
-               verify(eventBus, atLeastOnce()).post(eventsCaptor.capture())
-               assertThat(eventsCaptor.allValues, hasItem(InsertionDelayChangedEvent(15)))
+               assertThat(events, hasItem(InsertionDelayChangedEvent(15)))
        }
 
        @Test(expected = IllegalArgumentException::class)
@@ -44,13 +56,19 @@ class PreferencesTest {
 
        @Test
        fun `no event is sent when invalid insertion delay is set`() {
+               val events = mutableListOf<InsertionDelayChangedEvent>()
+               eventBus.register(object {
+                       @Subscribe
+                       fun insertionDelayChanged(event: InsertionDelayChangedEvent) =
+                                       events.add(event)
+               })
                try {
                        preferences.newInsertionDelay = -15
                } catch (iae: IllegalArgumentException) {
                        /* ignore. */
                }
 
-               verify(eventBus, never()).post(any())
+               assertThat(events, emptyIterable())
        }
 
        @Test
@@ -66,9 +84,7 @@ 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())
+               verifySavedOption(nullValue()) { it.getIntValue("Option/InsertionDelay").getValue(null) }
        }
 
        @Test
@@ -189,23 +205,41 @@ class PreferencesTest {
 
        @Test
        fun `preferences retain fcp interface active of true`() {
+               val events = mutableListOf<FcpInterfaceActivatedEvent>()
+               eventBus.register(object {
+                       @Subscribe
+                       fun fcpInterfaceActivatedEvent(event: FcpInterfaceActivatedEvent) =
+                                       events.add(event)
+               })
                preferences.newFcpInterfaceActive = true
                assertThat(preferences.fcpInterfaceActive, equalTo(true))
-               verify(eventBus).post(any(FcpInterfaceActivatedEvent::class.java))
+               assertThat(events, hasItem<FcpInterfaceActivatedEvent>(instanceOf(FcpInterfaceActivatedEvent::class.java)))
        }
 
        @Test
        fun `preferences retain fcp interface active of false`() {
+               val events = mutableListOf<FcpInterfaceDeactivatedEvent>()
+               eventBus.register(object {
+                       @Subscribe
+                       fun fcpInterfaceDeactivatedEvent(event: FcpInterfaceDeactivatedEvent) =
+                                       events.add(event)
+               })
                preferences.newFcpInterfaceActive = false
                assertThat(preferences.fcpInterfaceActive, equalTo(false))
-               verify(eventBus).post(any(FcpInterfaceDeactivatedEvent::class.java))
+               assertThat(events, hasItem<FcpInterfaceDeactivatedEvent>(instanceOf(FcpInterfaceDeactivatedEvent::class.java)))
        }
 
        @Test
        fun `preferences return default value when fcp interface active is set to null`() {
+               val events = mutableListOf<FcpInterfaceDeactivatedEvent>()
+               eventBus.register(object {
+                       @Subscribe
+                       fun fcpInterfaceDeactivatedEvent(event: FcpInterfaceDeactivatedEvent) =
+                                       events.add(event)
+               })
                preferences.newFcpInterfaceActive = null
                assertThat(preferences.fcpInterfaceActive, equalTo(false))
-               verify(eventBus).post(any(FcpInterfaceDeactivatedEvent::class.java))
+               assertThat(events, hasItem<FcpInterfaceDeactivatedEvent>(instanceOf(FcpInterfaceDeactivatedEvent::class.java)))
        }
 
        @Test
@@ -215,38 +249,34 @@ class PreferencesTest {
 
        @Test
        fun `preferences retain fcp full access required of no`() {
-               preferences.newFcpFullAccessRequired = NO
-               assertThat(preferences.fcpFullAccessRequired, equalTo(NO))
                verifyFullAccessRequiredChangedEvent(NO)
        }
 
-       private fun verifyFullAccessRequiredChangedEvent(
-                       fullAccessRequired: FullAccessRequired) {
-               verify(eventBus).post(eventsCaptor.capture())
-               assertThat(eventsCaptor.value, instanceOf(FullAccessRequiredChanged::class.java))
-               assertThat((eventsCaptor.value as FullAccessRequiredChanged).fullAccessRequired,
-                               equalTo(fullAccessRequired))
+       private fun verifyFullAccessRequiredChangedEvent(set: FullAccessRequired?, expected: FullAccessRequired = set!!) {
+               val events = mutableListOf<FullAccessRequiredChanged>()
+               eventBus.register(object {
+                       @Subscribe
+                       fun fullAccessRequiredChanged(event: FullAccessRequiredChanged) =
+                                       events.add(event)
+               })
+               preferences.newFcpFullAccessRequired = set
+               assertThat(preferences.fcpFullAccessRequired, equalTo(expected))
+               assertThat(events.single().fullAccessRequired, equalTo(expected))
        }
 
        @Test
        fun `preferences retain fcp full access required of writing`() {
-               preferences.newFcpFullAccessRequired = WRITING
-               assertThat(preferences.fcpFullAccessRequired, equalTo(WRITING))
                verifyFullAccessRequiredChangedEvent(WRITING)
        }
 
        @Test
        fun `preferences retain fcp full access required of always`() {
-               preferences.newFcpFullAccessRequired = ALWAYS
-               assertThat(preferences.fcpFullAccessRequired, equalTo(ALWAYS))
                verifyFullAccessRequiredChangedEvent(ALWAYS)
        }
 
        @Test
        fun `preferences return default value when fcp full access required is set to null`() {
-               preferences.newFcpFullAccessRequired = null
-               assertThat(preferences.fcpFullAccessRequired, equalTo(ALWAYS))
-               verifyFullAccessRequiredChangedEvent(ALWAYS)
+               verifyFullAccessRequiredChangedEvent(null, ALWAYS)
        }
 
        @Test
@@ -264,10 +294,78 @@ class PreferencesTest {
                testPreferencesChangedEvent("PostsPerPage", { preferences.newPostsPerPage = it }, 31)
        }
 
+       @Test
+       fun `default strict filtering is false`() {
+               assertThat(preferences.strictFiltering, equalTo(false))
+       }
+
+       @Test
+       fun `strict filtering can be set`() {
+               preferences.newStrictFiltering = true
+               assertThat(preferences.strictFiltering, equalTo(true))
+       }
+
+       @Test
+       fun `strict filtering returns to default on null`() {
+               preferences.newStrictFiltering = true
+               preferences.newStrictFiltering = null
+               assertThat(preferences.strictFiltering, equalTo(false))
+       }
+
+       @Test
+       fun `event is generated when strict filtering is activated`() {
+               val events = mutableListOf<StrictFilteringActivatedEvent>()
+               eventBus.register(object {
+                       @Subscribe fun strictFilteringActivatedEvent(event: StrictFilteringActivatedEvent) =
+                                       events.add(event)
+               })
+               preferences.newStrictFiltering = true
+               assertThat(events, hasItem<StrictFilteringActivatedEvent>(instanceOf(StrictFilteringActivatedEvent::class.java)))
+       }
+
+       @Test
+       fun `event is generated when strict filtering is deactivated`() {
+               val events = mutableListOf<StrictFilteringDeactivatedEvent>()
+               eventBus.register(object {
+                       @Subscribe fun strictFilteringDeactivatedEvent(event: StrictFilteringDeactivatedEvent) =
+                                       events.add(event)
+               })
+               preferences.newStrictFiltering = false
+               assertThat(events, hasItem<StrictFilteringDeactivatedEvent>(instanceOf(StrictFilteringDeactivatedEvent::class.java)))
+       }
+
+       @Test
+       fun `default strict filtering is saved as null`() {
+               verifySavedOption(nullValue()) { it.getBooleanValue("Option/StrictFiltering").value }
+       }
+
+       @Test
+       fun `activated strict filtering is saved as true`() {
+               preferences.newStrictFiltering = true
+               verifySavedOption(equalTo(true)) { it.getBooleanValue("Option/StrictFiltering").value }
+       }
+
+       @Test
+       fun `deactivated strict filtering is saved as false`() {
+               preferences.newStrictFiltering = false
+               verifySavedOption(equalTo(false)) { it.getBooleanValue("Option/StrictFiltering").value }
+       }
+
+       private fun <T> verifySavedOption(matcher: Matcher<T>, getter: (Configuration) -> T) {
+               val configuration = Configuration(MapConfigurationBackend())
+               preferences.saveTo(configuration)
+               assertThat(getter(configuration), matcher)
+       }
+
        private fun <T : Any> testPreferencesChangedEvent(name: String, setter: (T) -> Unit, value: T) {
+               val events = mutableListOf<PreferenceChangedEvent>()
+               eventBus.register(object {
+                       @Subscribe
+                       fun preferenceChanged(event: PreferenceChangedEvent) =
+                                       events.add(event)
+               })
                setter(value)
-               verify(eventBus, atLeastOnce()).post(eventsCaptor.capture())
-               assertThat(eventsCaptor.allValues, hasItem(PreferenceChangedEvent(name, value)))
+               assertThat(events, hasItem(PreferenceChangedEvent(name, value)))
        }
 
 }
index 2f90250..3da9e88 100644 (file)
@@ -4,12 +4,12 @@ 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.freenet.wot.*
 import net.pterodactylus.sone.main.*
 import net.pterodactylus.sone.test.*
 import org.hamcrest.MatcherAssert.*
@@ -38,6 +38,9 @@ class SoneInserterTest {
        private val core = mock<Core>()
        private val eventBus = mock<EventBus>()
        private val freenetInterface = mock<FreenetInterface>()
+       private val soneUriCreator = object : SoneUriCreator() {
+               override fun getInsertUri(sone: Sone): FreenetURI = expectedInsertUri
+       }
 
        @Before
        fun setupCore() {
@@ -49,14 +52,15 @@ class SoneInserterTest {
        @Test
        fun `insertion delay is forwarded to sone inserter`() {
                val eventBus = AsyncEventBus(directExecutor())
-               eventBus.register(SoneInserter(core, eventBus, freenetInterface, metricRegistry, "SoneId"))
+               eventBus.register(SoneInserter(core, eventBus, freenetInterface, metricRegistry, soneUriCreator, "SoneId"))
                eventBus.post(InsertionDelayChangedEvent(15))
                assertThat(SoneInserter.getInsertionDelay().get(), equalTo(15))
        }
 
        private fun createSone(insertUri: FreenetURI, fingerprint: String = "fingerprint"): Sone {
+               val ownIdentity = DefaultOwnIdentity("", "", "", insertUri.toString())
                val sone = mock<Sone>()
-               whenever(sone.insertUri).thenReturn(insertUri)
+               whenever(sone.identity).thenReturn(ownIdentity)
                whenever(sone.fingerprint).thenReturn(fingerprint)
                whenever(sone.rootAlbum).thenReturn(mock())
                whenever(core.getSone(anyString())).thenReturn(sone)
@@ -67,47 +71,46 @@ class SoneInserterTest {
        fun `isModified is true if modification detector says so`() {
                val soneModificationDetector = mock<SoneModificationDetector>()
                whenever(soneModificationDetector.isModified).thenReturn(true)
-               val soneInserter = SoneInserter(core, eventBus, freenetInterface, metricRegistry, "SoneId", soneModificationDetector, 1)
+               val soneInserter = SoneInserter(core, eventBus, freenetInterface, metricRegistry, soneUriCreator, "SoneId", soneModificationDetector, 1)
                assertThat(soneInserter.isModified, equalTo(true))
        }
 
        @Test
        fun `isModified is false if modification detector says so`() {
                val soneModificationDetector = mock<SoneModificationDetector>()
-               val soneInserter = SoneInserter(core, eventBus, freenetInterface, metricRegistry, "SoneId", soneModificationDetector, 1)
+               val soneInserter = SoneInserter(core, eventBus, freenetInterface, metricRegistry, soneUriCreator, "SoneId", soneModificationDetector, 1)
                assertThat(soneInserter.isModified, equalTo(false))
        }
 
        @Test
        fun `last fingerprint is stored correctly`() {
-               val soneInserter = SoneInserter(core, eventBus, freenetInterface, metricRegistry, "SoneId")
+               val soneInserter = SoneInserter(core, eventBus, freenetInterface, metricRegistry, soneUriCreator, "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")
+               val soneInserter = SoneInserter(core, eventBus, freenetInterface, metricRegistry, soneUriCreator, "SoneId")
                soneInserter.stop()
                soneInserter.serviceRun()
        }
 
        @Test
        fun `sone inserter inserts a sone if it is eligible`() {
-               val insertUri = mock<FreenetURI>()
                val finalUri = mock<FreenetURI>()
                val sone = createSone(insertUri)
                val soneModificationDetector = mock<SoneModificationDetector>()
                whenever(soneModificationDetector.isEligibleForInsert).thenReturn(true)
-               whenever(freenetInterface.insertDirectory(eq(insertUri), any<HashMap<String, Any>>(), eq("index.html"))).thenReturn(finalUri)
-               val soneInserter = SoneInserter(core, eventBus, freenetInterface, metricRegistry, "SoneId", soneModificationDetector, 1)
+               whenever(freenetInterface.insertDirectory(eq(expectedInsertUri), any<HashMap<String, Any>>(), eq("index.html"))).thenReturn(finalUri)
+               val soneInserter = SoneInserter(core, eventBus, freenetInterface, metricRegistry, soneUriCreator, "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<HashMap<String, Any>>(), eq("index.html"))
+               verify(freenetInterface).insertDirectory(eq(expectedInsertUri), any<HashMap<String, 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))
@@ -117,19 +120,18 @@ class SoneInserterTest {
 
        @Test
        fun `sone inserter bails out if it is stopped while inserting`() {
-               val insertUri = mock<FreenetURI>()
                val finalUri = mock<FreenetURI>()
                val sone = createSone(insertUri)
                val soneModificationDetector = mock<SoneModificationDetector>()
                whenever(soneModificationDetector.isEligibleForInsert).thenReturn(true)
-               val soneInserter = SoneInserter(core, eventBus, freenetInterface, metricRegistry, "SoneId", soneModificationDetector, 1)
-               whenever(freenetInterface.insertDirectory(eq(insertUri), any<HashMap<String, Any>>(), eq("index.html"))).thenAnswer {
+               val soneInserter = SoneInserter(core, eventBus, freenetInterface, metricRegistry, soneUriCreator, "SoneId", soneModificationDetector, 1)
+               whenever(freenetInterface.insertDirectory(eq(expectedInsertUri), any<HashMap<String, Any>>(), eq("index.html"))).thenAnswer {
                        soneInserter.stop()
                        finalUri
                }
                soneInserter.serviceRun()
                val soneEvents = ArgumentCaptor.forClass(SoneEvent::class.java)
-               verify(freenetInterface).insertDirectory(eq(insertUri), any<HashMap<String, Any>>(), eq("index.html"))
+               verify(freenetInterface).insertDirectory(eq(expectedInsertUri), any<HashMap<String, 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))
@@ -140,10 +142,9 @@ class SoneInserterTest {
 
        @Test
        fun `sone inserter does not insert sone if it is not eligible`() {
-               val insertUri = mock<FreenetURI>()
                createSone(insertUri)
                val soneModificationDetector = mock<SoneModificationDetector>()
-               val soneInserter = SoneInserter(core, eventBus, freenetInterface, metricRegistry, "SoneId", soneModificationDetector, 1)
+               val soneInserter = SoneInserter(core, eventBus, freenetInterface, metricRegistry, soneUriCreator, "SoneId", soneModificationDetector, 1)
                Thread(Runnable {
                        try {
                                Thread.sleep(500)
@@ -154,25 +155,24 @@ class SoneInserterTest {
                        soneInserter.stop()
                }).start()
                soneInserter.serviceRun()
-               verify(freenetInterface, never()).insertDirectory(eq(insertUri), any<HashMap<String, Any>>(), eq("index.html"))
+               verify(freenetInterface, never()).insertDirectory(eq(expectedInsertUri), any<HashMap<String, 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<FreenetURI>()
                val sone = createSone(insertUri)
                val soneModificationDetector = mock<SoneModificationDetector>()
                whenever(soneModificationDetector.isEligibleForInsert).thenReturn(true)
-               val soneInserter = SoneInserter(core, eventBus, freenetInterface, metricRegistry, "SoneId", soneModificationDetector, 1)
+               val soneInserter = SoneInserter(core, eventBus, freenetInterface, metricRegistry, soneUriCreator, "SoneId", soneModificationDetector, 1)
                val soneException = SoneException(Exception())
-               whenever(freenetInterface.insertDirectory(eq(insertUri), any<HashMap<String, Any>>(), eq("index.html"))).thenAnswer {
+               whenever(freenetInterface.insertDirectory(eq(expectedInsertUri), any<HashMap<String, Any>>(), eq("index.html"))).thenAnswer {
                        soneInserter.stop()
                        throw soneException
                }
                soneInserter.serviceRun()
                val soneEvents = ArgumentCaptor.forClass(SoneEvent::class.java)
-               verify(freenetInterface).insertDirectory(eq(insertUri), any<HashMap<String, Any>>(), eq("index.html"))
+               verify(freenetInterface).insertDirectory(eq(expectedInsertUri), any<HashMap<String, 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))
@@ -184,7 +184,7 @@ class SoneInserterTest {
        @Test
        fun `sone inserter exits if sone is unknown`() {
                val soneModificationDetector = mock<SoneModificationDetector>()
-               val soneInserter = SoneInserter(core, eventBus, freenetInterface, metricRegistry, "SoneId", soneModificationDetector, 1)
+               val soneInserter = SoneInserter(core, eventBus, freenetInterface, metricRegistry, soneUriCreator, "SoneId", soneModificationDetector, 1)
                whenever(soneModificationDetector.isEligibleForInsert).thenReturn(true)
                whenever(core.getSone("SoneId")).thenReturn(null)
                soneInserter.serviceRun()
@@ -193,7 +193,7 @@ class SoneInserterTest {
        @Test
        fun `sone inserter catches exception and continues`() {
                val soneModificationDetector = mock<SoneModificationDetector>()
-               val soneInserter = SoneInserter(core, eventBus, freenetInterface, metricRegistry, "SoneId", soneModificationDetector, 1)
+               val soneInserter = SoneInserter(core, eventBus, freenetInterface, metricRegistry, soneUriCreator, "SoneId", soneModificationDetector, 1)
                val stopInserterAndThrowException = Answer<Optional<Sone>> {
                        soneInserter.stop()
                        throw NullPointerException()
@@ -212,14 +212,14 @@ class SoneInserterTest {
                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)
+               val templateContent = String(manifestElement.data.inputStream.readBytes(), 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`() {
+       fun `invalid template returns a null manifest element`() {
                val soneProperties = HashMap<String, Any>()
                val manifestCreator = ManifestCreator(core, soneProperties)
                assertThat(manifestCreator.createManifestElement("test.txt",
@@ -241,13 +241,12 @@ class SoneInserterTest {
 
        @Test
        fun `successful insert updates metrics`() {
-               val insertUri = mock<FreenetURI>()
                val finalUri = mock<FreenetURI>()
                createSone(insertUri)
                val soneModificationDetector = mock<SoneModificationDetector>()
                whenever(soneModificationDetector.isEligibleForInsert).thenReturn(true)
-               whenever(freenetInterface.insertDirectory(eq(insertUri), any<HashMap<String, Any>>(), eq("index.html"))).thenReturn(finalUri)
-               val soneInserter = SoneInserter(core, eventBus, freenetInterface, metricRegistry,"SoneId", soneModificationDetector, 1)
+               whenever(freenetInterface.insertDirectory(eq(expectedInsertUri), any<HashMap<String, Any>>(), eq("index.html"))).thenReturn(finalUri)
+               val soneInserter = SoneInserter(core, eventBus, freenetInterface, metricRegistry, soneUriCreator, "SoneId", soneModificationDetector, 1)
                doAnswer {
                        soneInserter.stop()
                        null
@@ -259,12 +258,11 @@ class SoneInserterTest {
 
        @Test
        fun `unsuccessful insert does not update histogram but records error`() {
-               val insertUri = mock<FreenetURI>()
                createSone(insertUri)
                val soneModificationDetector = mock<SoneModificationDetector>()
                whenever(soneModificationDetector.isEligibleForInsert).thenReturn(true)
-               val soneInserter = SoneInserter(core, eventBus, freenetInterface, metricRegistry, "SoneId", soneModificationDetector, 1)
-               whenever(freenetInterface.insertDirectory(eq(insertUri), any<HashMap<String, Any>>(), eq("index.html"))).thenAnswer {
+               val soneInserter = SoneInserter(core, eventBus, freenetInterface, metricRegistry, soneUriCreator, "SoneId", soneModificationDetector, 1)
+               whenever(freenetInterface.insertDirectory(eq(expectedInsertUri), any<HashMap<String, Any>>(), eq("index.html"))).thenAnswer {
                        soneInserter.stop()
                        throw SoneException(Exception())
                }
@@ -276,3 +274,6 @@ class SoneInserterTest {
        }
 
 }
+
+val insertUri = createInsertUri
+val expectedInsertUri = createInsertUri
index 336d855..38419a2 100644 (file)
@@ -5,6 +5,7 @@ import com.google.common.base.Optional.*
 import freenet.crypt.*
 import freenet.keys.InsertableClientSSK.*
 import net.pterodactylus.sone.data.*
+import net.pterodactylus.sone.data.impl.AlbumImpl
 import net.pterodactylus.sone.database.memory.*
 import net.pterodactylus.sone.freenet.wot.*
 import net.pterodactylus.sone.test.*
@@ -41,6 +42,7 @@ class SoneParserTest {
                whenever(sone.identity).thenReturn(identity)
                whenever(sone.requestUri).thenAnswer { clientSSK.uri.setKeyType("USK").setDocName("Sone") }
                whenever(sone.time).thenReturn(currentTimeMillis() - DAYS.toMillis(1))
+               whenever(sone.rootAlbum).thenReturn(AlbumImpl(sone))
        }
 
        @Test
diff --git a/src/test/kotlin/net/pterodactylus/sone/core/SoneUriCreatorTest.kt b/src/test/kotlin/net/pterodactylus/sone/core/SoneUriCreatorTest.kt
new file mode 100644 (file)
index 0000000..ae8f25b
--- /dev/null
@@ -0,0 +1,88 @@
+package net.pterodactylus.sone.core
+
+import com.google.inject.Guice
+import net.pterodactylus.sone.data.impl.IdOnlySone
+import net.pterodactylus.sone.freenet.wot.DefaultIdentity
+import net.pterodactylus.sone.freenet.wot.DefaultOwnIdentity
+import net.pterodactylus.sone.test.createInsertUri
+import net.pterodactylus.sone.test.createRequestUri
+import net.pterodactylus.sone.test.getInstance
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.emptyArray
+import org.hamcrest.Matchers.equalTo
+import org.hamcrest.Matchers.notNullValue
+import org.hamcrest.Matchers.nullValue
+import kotlin.test.Test
+
+/**
+ * Unit test for [SoneUriCreator].
+ */
+class SoneUriCreatorTest {
+
+       private val soneUriCreator = SoneUriCreator()
+
+       private val requestUri = soneUriCreator.getRequestUri(sone)
+       private val insertUri = soneUriCreator.getInsertUri(sone)
+
+       @Test
+       fun `generated request URI is a USK`() {
+               assertThat(requestUri.keyType, equalTo("USK"))
+       }
+
+       @Test
+       fun `generated request URI has correct doc name`() {
+               assertThat(requestUri.docName, equalTo("Sone"))
+       }
+
+       @Test
+       fun `generated request URI has no meta strings`() {
+               assertThat(requestUri.allMetaStrings, emptyArray())
+       }
+
+       @Test
+       fun `generated request URI has correct edition`() {
+               assertThat(requestUri.suggestedEdition, equalTo(123L))
+       }
+
+       @Test
+       fun `insert URI is null if sone’s identity is not an own identity`() {
+               val remoteSone = object : IdOnlySone("id") {
+                       override fun getIdentity() = DefaultIdentity("id", "name", createRequestUri.toString())
+               }
+               assertThat(soneUriCreator.getInsertUri(remoteSone), nullValue())
+       }
+
+       @Test
+       fun `generated insert URI is a USK`() {
+               assertThat(insertUri!!.keyType, equalTo("USK"))
+       }
+
+       @Test
+       fun `generated insert URI has correct doc name`() {
+               assertThat(insertUri!!.docName, equalTo("Sone"))
+       }
+
+       @Test
+       fun `generated insert URI has no meta strings`() {
+               assertThat(insertUri!!.allMetaStrings, emptyArray())
+       }
+
+       @Test
+       fun `generated insert URI has correct edition`() {
+               assertThat(insertUri!!.suggestedEdition, equalTo(123L))
+       }
+
+       @Test
+       fun `creator can be created by guice`() {
+               val injector = Guice.createInjector()
+               assertThat(injector.getInstance<SoneUriCreator>(), notNullValue())
+       }
+
+}
+
+private val sone = object : IdOnlySone("id") {
+       override fun getIdentity() =
+                       DefaultOwnIdentity("id", "name", createRequestUri.toString(), createInsertUri.toString())
+
+       override fun getLatestEdition() = 123L
+}
index 9e7c71e..356ea9c 100644 (file)
@@ -123,14 +123,14 @@ class UpdatedSoneProcessorTest {
        @Test
        fun `updated Sone processor does not mark new reply as known if sone was not followed after reply`() {
                updatedSoneProcessor.updateSone(newSone)
-               verify(postReplies[2], never()).isKnown = true
+               verify(database, never()).setPostReplyKnown(postReplies[2])
        }
 
        @Test
        fun `updated Sone processor marks new reply as known if sone was followed after reply`() {
                whenever(database.getFollowingTime("sone")).thenReturn(3500L)
                updatedSoneProcessor.updateSone(newSone)
-               verify(postReplies[2]).isKnown = true
+               verify(database).setPostReplyKnown(postReplies[2])
        }
 
        @Test
diff --git a/src/test/kotlin/net/pterodactylus/sone/data/AlbumTest.kt b/src/test/kotlin/net/pterodactylus/sone/data/AlbumTest.kt
new file mode 100644 (file)
index 0000000..fe11c2c
--- /dev/null
@@ -0,0 +1,136 @@
+/**
+ * Sone - AlbumTest.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 <http://www.gnu.org/licenses/>.
+ */
+
+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 `Album.kt`.
+ */
+class AlbumTest {
+
+       @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, key: String? = null) = ImageImpl(id).modify().setSone(sone).setKey(key).update()
+
+       @Test
+       fun `allAlbums returns itself and all its subalbums`() {
+               val sone = IdOnlySone("sone")
+               val album = AlbumImpl(sone)
+               val firstNestedAlbum = AlbumImpl(sone)
+               val secondNestedAlbum = AlbumImpl(sone)
+               val albumNestedInFirst = AlbumImpl(sone)
+               album.addAlbum(firstNestedAlbum)
+               album.addAlbum(secondNestedAlbum)
+               firstNestedAlbum.addAlbum(albumNestedInFirst)
+               val albums = album.allAlbums
+               assertThat(albums, containsInAnyOrder<Album>(album, firstNestedAlbum, secondNestedAlbum, albumNestedInFirst))
+               assertThat(albums.indexOf(firstNestedAlbum), greaterThan(albums.indexOf(album)))
+               assertThat(albums.indexOf(secondNestedAlbum), greaterThan(albums.indexOf(album)))
+               assertThat(albums.indexOf(albumNestedInFirst), greaterThan(albums.indexOf(firstNestedAlbum)))
+       }
+
+       @Test
+       fun `notEmpty finds album without images is empty`() {
+               val sone = IdOnlySone("sone")
+               val album = AlbumImpl(sone)
+               assertThat(notEmpty(album), equalTo(false))
+       }
+
+       @Test
+       fun `notEmpty finds album with one inserted image is not empty`() {
+               val sone = IdOnlySone("sone")
+               val album = AlbumImpl(sone)
+               album.addImage(createImage(sone, "1", "key"))
+               assertThat(notEmpty(album), equalTo(true))
+       }
+
+       @Test
+       fun `notEmpty finds album with one not-inserted image is empty`() {
+               val sone = IdOnlySone("sone")
+               val album = AlbumImpl(sone)
+               album.addImage(createImage(sone, "1"))
+               assertThat(notEmpty(album), equalTo(false))
+       }
+
+       @Test
+       fun `notEmpty finds album with empty subalbums is empty`() {
+               val sone = IdOnlySone("sone")
+               val album = AlbumImpl(sone)
+               val firstNestedAlbum = AlbumImpl(sone)
+               album.addAlbum(firstNestedAlbum)
+               assertThat(notEmpty(album), equalTo(false))
+       }
+
+       @Test
+       fun `notEmpty finds album with subalbum with not inserted image is empty`() {
+               val sone = IdOnlySone("sone")
+               val album = AlbumImpl(sone)
+               val firstNestedAlbum = AlbumImpl(sone)
+               firstNestedAlbum.addImage(createImage(sone, "1"))
+               album.addAlbum(firstNestedAlbum)
+               assertThat(notEmpty(album), equalTo(false))
+       }
+
+       @Test
+       fun `notEmpty finds album with subalbum with inserted image is not empty`() {
+               val sone = IdOnlySone("sone")
+               val album = AlbumImpl(sone)
+               val firstNestedAlbum = AlbumImpl(sone)
+               firstNestedAlbum.addImage(createImage(sone, "1", "key"))
+               album.addAlbum(firstNestedAlbum)
+               assertThat(notEmpty(album), equalTo(true))
+       }
+
+       @Test
+       fun `allImages returns images from album`() {
+               val sone = IdOnlySone("sone")
+               val album = AlbumImpl(sone)
+               val image1 = createImage(sone, "1").also(album::addImage)
+               val image2 = createImage(sone, "2").also(album::addImage)
+               assertThat(album.allImages, contains(image1, image2))
+       }
+
+       @Test
+       fun `allImages returns images from subalbum`() {
+               val sone = IdOnlySone("sone")
+               val album1 = AlbumImpl(sone)
+               val album2 = AlbumImpl(sone).also(album1::addAlbum)
+               val image1 = createImage(sone, "1").also(album1::addImage)
+               val image2 = createImage(sone, "2").also(album2::addImage)
+               assertThat(album1.allImages, contains(image1, image2))
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/data/AlbumsTest.kt b/src/test/kotlin/net/pterodactylus/sone/data/AlbumsTest.kt
deleted file mode 100644 (file)
index 26af4e9..0000000
+++ /dev/null
@@ -1,48 +0,0 @@
-/**
- * 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 <http://www.gnu.org/licenses/>.
- */
-
-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/PostTest.kt b/src/test/kotlin/net/pterodactylus/sone/data/PostTest.kt
new file mode 100644 (file)
index 0000000..73f8498
--- /dev/null
@@ -0,0 +1,49 @@
+package net.pterodactylus.sone.data
+
+import net.pterodactylus.sone.test.createPost
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.equalTo
+import org.hamcrest.Matchers.greaterThan
+import org.hamcrest.Matchers.lessThan
+import java.util.concurrent.TimeUnit.DAYS
+import kotlin.test.Test
+
+/**
+ * Unit test for the utilities in `Post.kt`.
+ */
+class PostTest {
+
+       @Test
+       fun `noFuturePost filter recognizes post from future`() {
+               val post = createPost(time = System.currentTimeMillis() + DAYS.toMillis(1))
+               assertThat(noFuturePost(post), equalTo(false))
+       }
+
+       @Test
+       fun `noFuturePost filter recognizes post not from future`() {
+               val post = createPost(time = System.currentTimeMillis())
+               assertThat(noFuturePost(post), equalTo(true))
+       }
+
+       @Test
+       fun `newestFirst comparator returns less-than 0 if first is newer than second`() {
+               val newerPost = createPost(time = 2000)
+               val olderPost = createPost(time = 1000)
+               assertThat(newestPostFirst.compare(newerPost, olderPost), lessThan(0))
+       }
+
+       @Test
+       fun `newestFirst comparator returns greater-than 0 if first is older than second`() {
+               val newerPost = createPost(time = 2000)
+               val olderPost = createPost(time = 1000)
+               assertThat(newestPostFirst.compare(olderPost, newerPost), greaterThan(0))
+       }
+
+       @Test
+       fun `newestFirst comparator returns 0 if first and second are the same age`() {
+               val post1 = createPost(time = 1000)
+               val post2 = createPost(time = 1000)
+               assertThat(newestPostFirst.compare(post2, post1), equalTo(0))
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/data/ReplyTest.kt b/src/test/kotlin/net/pterodactylus/sone/data/ReplyTest.kt
new file mode 100644 (file)
index 0000000..d7f7138
--- /dev/null
@@ -0,0 +1,63 @@
+/**
+ * Sone - ReplyTest.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 <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.data
+
+import net.pterodactylus.sone.test.emptyPostReply
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.equalTo
+import org.hamcrest.Matchers.greaterThan
+import org.hamcrest.Matchers.lessThan
+import java.util.concurrent.TimeUnit.DAYS
+import kotlin.test.Test
+
+class ReplyTest {
+
+       @Test
+       fun `newestReplyFirst comparator returns less-than 0 is first reply is newer than second`() {
+               val newerReply = emptyPostReply(time = 2000)
+               val olderReply = emptyPostReply(time = 1000)
+               assertThat(newestReplyFirst.compare(newerReply, olderReply), lessThan(0))
+       }
+
+       @Test
+       fun `newestReplyFirst comparator returns greater-than 0 is first reply is older than second`() {
+               val newerReply = emptyPostReply(time = 2000)
+               val olderReply = emptyPostReply(time = 1000)
+               assertThat(newestReplyFirst.compare(olderReply, newerReply), greaterThan(0))
+       }
+
+       @Test
+       fun `newestReplyFirst comparator returns 0 is first and second reply have same age`() {
+               val reply1 = emptyPostReply(time = 1000)
+               val reply2 = emptyPostReply(time = 1000)
+               assertThat(newestReplyFirst.compare(reply1, reply2), equalTo(0))
+       }
+
+       @Test
+       fun `noFutureReply filter recognizes reply from the future`() {
+               val futureReply = emptyPostReply(time = System.currentTimeMillis() + DAYS.toMillis(1))
+               assertThat(noFutureReply(futureReply), equalTo(false))
+       }
+
+       @Test
+       fun `noFutureReply filter recognizes reply from the present`() {
+               val futureReply = emptyPostReply(time = System.currentTimeMillis())
+               assertThat(noFutureReply(futureReply), equalTo(true))
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/data/SoneTest.kt b/src/test/kotlin/net/pterodactylus/sone/data/SoneTest.kt
new file mode 100644 (file)
index 0000000..fb23121
--- /dev/null
@@ -0,0 +1,172 @@
+/**
+ * Sone - SoneTest.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 <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.data
+
+import net.pterodactylus.sone.data.impl.*
+import net.pterodactylus.sone.test.*
+import org.hamcrest.MatcherAssert.*
+import org.hamcrest.Matchers.*
+import kotlin.test.*
+
+/**
+ * Unit test for functions in Sone.
+ */
+class SoneTest {
+
+       @Test
+       fun `nice name comparator correctly compares Sones by their nice name`() {
+               val sone1 = object : IdOnlySone("1") {
+                       override fun getProfile() = Profile(this).apply { firstName = "Left" }
+               }
+               val sone2 = object : IdOnlySone("2") {
+                       override fun getProfile() = Profile(this).apply { firstName = "Right" }
+               }
+               assertThat(niceNameComparator.compare(sone1, sone2), lessThan(0))
+       }
+
+       @Test
+       fun `nice name comparator correctly compares Sones by their ID if nice name is the same`() {
+               val sone1 = object : IdOnlySone("1") {
+                       override fun getProfile() = Profile(this).apply { firstName = "Left" }
+               }
+               val sone2 = object : IdOnlySone("2") {
+                       override fun getProfile() = Profile(this).apply { firstName = "Left" }
+               }
+               assertThat(niceNameComparator.compare(sone1, sone2), lessThan(0))
+       }
+
+       @Test
+       fun `nice name comparator treats Sones as equal if nice name and ID are the same`() {
+               val sone1 = object : IdOnlySone("1") {
+                       override fun getProfile() = Profile(this).apply { firstName = "Left" }
+               }
+               val sone2 = object : IdOnlySone("1") {
+                       override fun getProfile() = Profile(this).apply { firstName = "Left" }
+               }
+               assertThat(niceNameComparator.compare(sone1, sone2), equalTo(0))
+       }
+
+       @Test
+       fun `last activity comparator correctly compares Sones by last activity`() {
+               val sone1 = object : IdOnlySone("1") {
+                       override fun getTime() = 1000L
+               }
+               val sone2 = object : IdOnlySone("2") {
+                       override fun getTime() = 2000L
+               }
+               assertThat(lastActivityComparator.compare(sone1, sone2), greaterThan(0))
+       }
+
+       @Test
+       fun `last activity comparator treats Sones as equal if last activity is the same`() {
+               val sone1 = object : IdOnlySone("1") {
+                       override fun getTime() = 1000L
+               }
+               val sone2 = object : IdOnlySone("2") {
+                       override fun getTime() = 1000L
+               }
+               assertThat(lastActivityComparator.compare(sone1, sone2), equalTo(0))
+       }
+
+       @Test
+       fun `post count comparator sorts sones with different number of posts correctly`() {
+               val sone1 = object : IdOnlySone("1") {
+                       override fun getPosts() = listOf(createPost(), createPost())
+               }
+               val sone2 = object : IdOnlySone("2") {
+                       override fun getPosts() = listOf(createPost(), createPost(), createPost())
+               }
+               assertThat(postCountComparator.compare(sone1, sone2), greaterThan(0))
+       }
+
+       @Test
+       fun `post count comparator compares replies if posts are not different`() {
+               val sone1 = object : IdOnlySone("1") {
+                       override fun getPosts() = listOf(createPost(), createPost())
+                       override fun getReplies() = setOf(emptyPostReply(), emptyPostReply())
+               }
+               val sone2 = object : IdOnlySone("2") {
+                       override fun getPosts() = listOf(createPost(), createPost())
+                       override fun getReplies() = setOf(emptyPostReply(), emptyPostReply(), emptyPostReply())
+               }
+               assertThat(postCountComparator.compare(sone1, sone2), greaterThan(0))
+       }
+
+       @Test
+       fun `post count comparator sorts sone with same amount of posts and replies as equal`() {
+               val sone1 = object : IdOnlySone("1") {
+                       override fun getPosts() = listOf(createPost(), createPost())
+                       override fun getReplies() = setOf(emptyPostReply(), emptyPostReply())
+               }
+               val sone2 = object : IdOnlySone("2") {
+                       override fun getPosts() = listOf(createPost(), createPost())
+                       override fun getReplies() = setOf(emptyPostReply(), emptyPostReply())
+               }
+               assertThat(postCountComparator.compare(sone1, sone2), equalTo(0))
+       }
+
+       @Test
+       fun `image count comparator sorts Sones correctly if number of images is different`() {
+               val sone1 = object : IdOnlySone("1") {
+                       override fun getRootAlbum() = AlbumImpl(this).also { it.addImage(createImage(this)) }
+               }
+               val sone2 = object : IdOnlySone("2") {
+                       override fun getRootAlbum() = AlbumImpl(this).also { it.addImage(createImage(this)); it.addImage(createImage(this)) }
+               }
+               assertThat(imageCountComparator.compare(sone1, sone2), greaterThan(0))
+       }
+
+       @Test
+       fun `image count comparator treats Sones as equal if number of images is the same`() {
+               val sone1 = object : IdOnlySone("1") {
+                       override fun getRootAlbum() = AlbumImpl(this).also { it.addImage(createImage(this)) }
+               }
+               val sone2 = object : IdOnlySone("2") {
+                       override fun getRootAlbum() = AlbumImpl(this).also { it.addImage(createImage(this)) }
+               }
+               assertThat(imageCountComparator.compare(sone1, sone2), equalTo(0))
+       }
+
+       @Test
+       fun `allAlbums returns all albums of a Sone but the root album`() {
+               val sone = object : IdOnlySone("1") {
+                       private val rootAlbum = AlbumImpl(this)
+                       override fun getRootAlbum() = rootAlbum
+               }
+               val album1 = AlbumImpl(sone).also(sone.rootAlbum::addAlbum)
+               val album11 = AlbumImpl(sone).also(album1::addAlbum)
+               val album2 = AlbumImpl(sone).also(sone.rootAlbum::addAlbum)
+               assertThat(sone.allAlbums, contains<Album>(album1, album11, album2))
+       }
+
+       @Test
+       fun `allImages returns all images of a Sone`() {
+               val sone = object : IdOnlySone("1") {
+                       private val rootAlbum = AlbumImpl(this)
+                       override fun getRootAlbum() = rootAlbum
+               }
+               val album1 = AlbumImpl(sone).also(sone.rootAlbum::addAlbum)
+               val album11 = AlbumImpl(sone).also(album1::addAlbum)
+               val album2 = AlbumImpl(sone).also(sone.rootAlbum::addAlbum)
+               val image1 = createImage(sone).also(album1::addImage)
+               val image11 = createImage(sone).also(album11::addImage)
+               val image2 = createImage(sone).also(album2::addImage)
+               assertThat(sone.allImages, containsInAnyOrder(image1, image11, image2))
+       }
+
+}
index cc4778f..973047f 100644 (file)
@@ -57,8 +57,8 @@ class MemoryDatabaseTest {
                assertThat(memoryDatabase.getPostReply("reply2"), isPostReply("reply2", "post2", 4000L, "reply2"))
                assertThat(memoryDatabase.getPostReply("reply3"), isPostReply("reply3", "post1", 5000L, "reply3"))
                assertThat(memoryDatabase.getPostReply("reply4"), nullValue())
-               assertThat(memoryDatabase.getAlbum("album1"), isAlbum("album1", null, "album1", "album-description1"))
-               assertThat(memoryDatabase.getAlbum("album2"), isAlbum("album2", null, "album2", "album-description2"))
+               assertThat(memoryDatabase.getAlbum("album1"), isAlbum("album1", "root", "album1", "album-description1"))
+               assertThat(memoryDatabase.getAlbum("album2"), isAlbum("album2", "root", "album2", "album-description2"))
                assertThat(memoryDatabase.getAlbum("album3"), isAlbum("album3", "album1", "album3", "album-description3"))
                assertThat(memoryDatabase.getAlbum("album4"), nullValue())
                assertThat(memoryDatabase.getImage("image1"), isImage("image1", 1000L, "KSK@image1", "image1", "image-description1", 16, 9))
@@ -123,9 +123,10 @@ class MemoryDatabaseTest {
                                .setDescription("album-description3")
                                .update()
                firstAlbum.addAlbum(thirdAlbum)
-               val rootAlbum = mock<Album>()
-               whenever(rootAlbum.id).thenReturn("root")
-               whenever(rootAlbum.albums).thenReturn(listOf(firstAlbum, secondAlbum))
+               val rootAlbum = AlbumImpl(sone, "root").also {
+                       it.addAlbum(firstAlbum)
+                       it.addAlbum(secondAlbum)
+               }
                whenever(sone.rootAlbum).thenReturn(rootAlbum)
                val firstImage = TestImageBuilder().withId("image1")
                                .build()
@@ -404,7 +405,7 @@ class MemoryDatabaseTest {
                prepareConfigurationValues()
                val postReply = mock<PostReply>()
                whenever(postReply.id).thenReturn("post-reply-id")
-               memoryDatabase.setPostReplyKnown(postReply, true)
+               memoryDatabase.setPostReplyKnown(postReply)
                assertThat(configuration.getStringValue("KnownReplies/0/ID").value, equalTo("post-reply-id"))
                assertThat(configuration.getStringValue("KnownReplies/1/ID").value, equalTo<Any>(null))
        }
@@ -434,8 +435,8 @@ class MemoryDatabaseTest {
                prepareConfigurationValues()
                val postReply = mock<PostReply>()
                whenever(postReply.id).thenReturn("post-reply-id")
-               memoryDatabase.setPostReplyKnown(postReply, true)
-               memoryDatabase.setPostReplyKnown(postReply, true)
+               memoryDatabase.setPostReplyKnown(postReply)
+               memoryDatabase.setPostReplyKnown(postReply)
                verify(configuration, times(1)).getStringValue("KnownReplies/1/ID")
        }
 
index cfbda94..fabdd09 100644 (file)
@@ -1,7 +1,5 @@
 package net.pterodactylus.sone.fcp
 
-import com.google.common.base.Optional.absent
-import com.google.common.base.Optional.of
 import net.pterodactylus.sone.core.Core
 import net.pterodactylus.sone.data.Post
 import net.pterodactylus.sone.test.mock
index b3a8447..0e0e336 100644 (file)
@@ -42,7 +42,7 @@ class IdentityLoaderTest {
        @Test
        fun loadingIdentities() {
                val identityLoader = IdentityLoader(webOfTrustConnector, Context("Test"))
-               val identities = identityLoader.loadIdentities()
+               val identities = identityLoader.loadTrustedIdentities()
                assertThat(identities.keys, hasSize(4))
                assertThat(identities.keys, containsInAnyOrder(ownIdentities[0], ownIdentities[1], ownIdentities[2], ownIdentities[3]))
                verifyIdentitiesForOwnIdentity(identities, ownIdentities[0], createTrustedIdentitiesForFirstOwnIdentity())
@@ -54,7 +54,7 @@ class IdentityLoaderTest {
        @Test
        fun loadingIdentitiesWithoutContext() {
                val identityLoaderWithoutContext = IdentityLoader(webOfTrustConnector)
-               val identities = identityLoaderWithoutContext.loadIdentities()
+               val identities = identityLoaderWithoutContext.loadTrustedIdentities()
                assertThat(identities.keys, hasSize(4))
                assertThat(identities.keys, containsInAnyOrder(ownIdentities[0], ownIdentities[1], ownIdentities[2], ownIdentities[3]))
                verifyIdentitiesForOwnIdentity(identities, ownIdentities[0], createTrustedIdentitiesForFirstOwnIdentity())
@@ -106,6 +106,7 @@ private open class TestWebOfTrustConnector : WebOfTrustConnector {
 
        override fun loadAllOwnIdentities() = emptySet<OwnIdentity>()
        override fun loadTrustedIdentities(ownIdentity: OwnIdentity, context: String?) = emptySet<Identity>()
+       override fun loadAllIdentities(ownIdentity: OwnIdentity, context: String?) = emptySet<Identity>()
        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
diff --git a/src/test/kotlin/net/pterodactylus/sone/freenet/wot/IdentityManagerTest.kt b/src/test/kotlin/net/pterodactylus/sone/freenet/wot/IdentityManagerTest.kt
new file mode 100644 (file)
index 0000000..3768efd
--- /dev/null
@@ -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<EventBus>()
+       private val webOfTrustConnector = mock<WebOfTrustConnector>()
+       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()
+       }
+
+}
index c628334..01d1285 100644 (file)
@@ -4,7 +4,9 @@ import com.codahale.metrics.*
 import com.google.common.base.*
 import com.google.common.eventbus.*
 import com.google.inject.Guice.*
+import com.google.inject.Injector
 import com.google.inject.name.Names.*
+import freenet.clients.http.SessionManager
 import freenet.l10n.*
 import net.pterodactylus.sone.core.*
 import net.pterodactylus.sone.database.*
@@ -13,6 +15,7 @@ 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.sone.web.SessionProvider
 import net.pterodactylus.util.config.*
 import net.pterodactylus.util.version.Version
 import org.hamcrest.MatcherAssert.*
@@ -41,14 +44,7 @@ class SoneModuleTest {
                whenever(l10n()).thenReturn(l10n)
        }
 
-       private val injector by lazy {
-               createInjector(
-                               SoneModule(sonePlugin, EventBus()),
-                               FreenetInterface::class.isProvidedByDeepMock(),
-                               PluginRespiratorFacade::class.isProvidedByDeepMock(),
-                               PluginConnector::class.isProvidedByDeepMock()
-               )
-       }
+       private val injector by lazy { createInjector() }
 
        @AfterTest
        fun removePropertiesFromCurrentDirectory() {
@@ -198,16 +194,20 @@ class SoneModuleTest {
        @Test
        fun `core is registered with event bus`() {
                val eventBus = mock<EventBus>()
-               val injector = createInjector(
-                               SoneModule(sonePlugin, eventBus),
-                               FreenetInterface::class.isProvidedByDeepMock(),
-                               PluginRespiratorFacade::class.isProvidedByDeepMock(),
-                               PluginConnector::class.isProvidedByDeepMock()
-               )
+               val injector = createInjector(eventBus)
                val core = injector.getInstance<Core>()
                verify(eventBus).register(core)
        }
 
+       private fun createInjector(eventBus: EventBus = EventBus()): Injector =
+                       createInjector(
+                                       SoneModule(sonePlugin, eventBus),
+                                       FreenetInterface::class.isProvidedByDeepMock(),
+                                       PluginRespiratorFacade::class.isProvidedByDeepMock(),
+                                       PluginConnector::class.isProvidedByDeepMock(),
+                                       SessionManager::class.isProvidedByMock()
+                       )
+
        @Test
        fun `metrics registry is created as singleton`() {
                injector.verifySingletonInstance<MetricRegistry>()
@@ -228,4 +228,14 @@ class SoneModuleTest {
                injector.verifySingletonInstance<TickerShutdown>()
        }
 
+       @Test
+       fun `sone URI creator is created as singleton`() {
+               injector.verifySingletonInstance<SoneUriCreator>()
+       }
+
+       @Test
+       fun `session provider is created as singleton`() {
+               injector.verifySingletonInstance<SessionProvider>()
+       }
+
 }
index f7da592..21a3d00 100644 (file)
@@ -1,13 +1,9 @@
 package net.pterodactylus.sone.template
 
-import net.pterodactylus.sone.data.Album
-import net.pterodactylus.sone.data.Image
-import net.pterodactylus.sone.test.mock
-import net.pterodactylus.sone.test.whenever
+import net.pterodactylus.sone.data.impl.*
 import org.hamcrest.MatcherAssert.assertThat
 import org.hamcrest.Matchers.equalTo
 import org.hamcrest.Matchers.nullValue
-import org.junit.Before
 import org.junit.Test
 
 /**
@@ -16,16 +12,6 @@ import org.junit.Test
 class ImageAccessorTest {
 
        private val accessor = ImageAccessor()
-       private val album = mock<Album>()
-       private val images = listOf(mock<Image>(), mock())
-
-       @Before
-       fun setupImages() {
-               whenever(album.images).thenReturn(images)
-               images.forEach {
-                       whenever(it.album).thenReturn(album)
-               }
-       }
 
        @Test
        fun `accessor returns next image for first image`() {
@@ -53,3 +39,7 @@ class ImageAccessorTest {
        }
 
 }
+
+private val sone = IdOnlySone("sone")
+private val album = AlbumImpl(sone)
+private val images = listOf(ImageImpl().modify().setSone(sone).update(), ImageImpl().modify().setSone(sone).update()).onEach(album::addImage)
index 4513ce0..53d473e 100644 (file)
@@ -1,7 +1,6 @@
 package net.pterodactylus.sone.template
 
 import net.pterodactylus.sone.core.Core
-import net.pterodactylus.sone.data.Image
 import net.pterodactylus.sone.data.Profile
 import net.pterodactylus.sone.data.Sone
 import net.pterodactylus.sone.data.SoneOptions.DefaultSoneOptions
index 87cfce0..2ddab15 100644 (file)
@@ -10,6 +10,7 @@ import net.pterodactylus.sone.data.Sone.SoneStatus.downloading
 import net.pterodactylus.sone.data.Sone.SoneStatus.idle
 import net.pterodactylus.sone.data.Sone.SoneStatus.inserting
 import net.pterodactylus.sone.data.Sone.SoneStatus.unknown
+import net.pterodactylus.sone.data.impl.*
 import net.pterodactylus.sone.freenet.L10nText
 import net.pterodactylus.sone.freenet.wot.Identity
 import net.pterodactylus.sone.freenet.wot.OwnIdentity
@@ -217,24 +218,24 @@ class SoneAccessorTest {
 
        @Test
        fun `accessor returns all images in the correct order`() {
-               val images = listOf(mock<Image>(), mock(), mock(), mock(), mock())
-               val firstAlbum = createAlbum(listOf(), listOf(images[0], images[3]))
-               val secondAlbum = createAlbum(listOf(), listOf(images[1], images[4], images[2]))
-               val rootAlbum = createAlbum(listOf(firstAlbum, secondAlbum), listOf())
+               val images = (0 until 5).map { ImageImpl().modify().setSone(sone).update() }
+               val firstAlbum = createAlbum(emptyList(), listOf(images[0], images[3]))
+               val secondAlbum = createAlbum(emptyList(), listOf(images[1], images[4], images[2]))
+               val rootAlbum = createAlbum(listOf(firstAlbum, secondAlbum), emptyList())
                whenever(sone.rootAlbum).thenReturn(rootAlbum)
                assertAccessorReturnValueMatches("allImages", contains(images[0], images[3], images[1], images[4], images[2]))
        }
 
        private fun createAlbum(albums: List<Album>, images: List<Image>) =
-                       mock<Album>().apply {
-                               whenever(this.albums).thenReturn(albums)
-                               whenever(this.images).thenReturn(images)
+                       AlbumImpl(sone).also {
+                               albums.forEach(it::addAlbum)
+                               images.forEach(it::addImage)
                        }
 
        @Test
        fun `accessor returns all albums in the correct order`() {
-               val albums = listOf(mock<Album>(), mock(), mock(), mock(), mock())
-               val rootAlbum = createAlbum(albums, listOf())
+               val albums = (0 until 5).map { AlbumImpl(sone)  }
+               val rootAlbum = createAlbum(albums, emptyList())
                whenever(sone.rootAlbum).thenReturn(rootAlbum)
                assertAccessorReturnValueMatches("albums", contains(*albums.toTypedArray()))
        }
index b1c2cc5..96a3cd5 100644 (file)
@@ -1,7 +1,6 @@
 package net.pterodactylus.sone.template
 
 import net.pterodactylus.sone.freenet.*
-import net.pterodactylus.sone.test.*
 import org.hamcrest.MatcherAssert.*
 import org.hamcrest.Matchers.*
 import org.junit.*
diff --git a/src/test/kotlin/net/pterodactylus/sone/test/Logging.kt b/src/test/kotlin/net/pterodactylus/sone/test/Logging.kt
new file mode 100644 (file)
index 0000000..2a2c6b7
--- /dev/null
@@ -0,0 +1,25 @@
+package net.pterodactylus.sone.test
+
+import org.junit.rules.TestRule
+import org.junit.runners.model.Statement
+import java.util.logging.Level
+import java.util.logging.Logger.getLogger
+
+/**
+ * Silences the `net.pterodactylus.sone` [logger][java.util.logging.Logger] during a test.
+ */
+fun silencedLogging() = TestRule { base, _ ->
+       object : Statement() {
+               override fun evaluate() {
+                       getLogger("net.pterodactylus.sone").let { logger ->
+                               val oldLevel = logger.level
+                               logger.level = Level.OFF
+                               try {
+                                       base.evaluate()
+                               } finally {
+                                       logger.level = oldLevel
+                               }
+                       }
+               }
+       }
+}
index 85ed9e7..1f479f6 100644 (file)
@@ -5,7 +5,6 @@ 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.*
 
 /**
index 48fb9bf..a25fa5d 100644 (file)
@@ -23,7 +23,6 @@ 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()
@@ -32,6 +31,8 @@ val remoteSone2 = createRemoteSone()
 val localSone1 = createLocalSone()
 val localSone2 = createLocalSone()
 
+val createRequestUri: FreenetURI get() = InsertableClientSSK.createRandom(DummyRandomSource(), "").uri
+val createInsertUri: FreenetURI get() = InsertableClientSSK.createRandom(DummyRandomSource(), "").insertURI
 fun createId() = InsertableClientSSK.createRandom(DummyRandomSource(), "").uri.routingKey.asFreenetBase64
 
 fun createLocalSone(id: String? = createId()) = object : IdOnlySone(id) {
@@ -41,21 +42,24 @@ fun createLocalSone(id: String? = createId()) = object : IdOnlySone(id) {
 }
 fun createRemoteSone(id: String? = createId()) = IdOnlySone(id)
 
-fun createPost(text: String = "", sone: Sone = remoteSone1, known: Boolean = false): Post.EmptyPost {
+fun createPost(text: String = "", sone: Sone = remoteSone1, known: Boolean = false, time: Long = 1): Post.EmptyPost {
        return object : Post.EmptyPost("post-id") {
                override fun getSone() = sone
                override fun getText() = text
                override fun isKnown() = known
+               override fun getTime() = time
        }
 }
 
-fun emptyPostReply(text: String = "", post: Post? = createPost(), sone: Sone = remoteSone1, known: Boolean = false) = object : PostReply {
+fun emptyPostReply(text: String = "", post: Post? = createPost(), sone: Sone = remoteSone1, known: Boolean = false, time: Long = 1) = object : PostReply {
        override val id = "reply-id"
        override fun getSone() = sone
        override fun getPostId() = post!!.id
        override fun getPost(): Optional<Post> = Optional.fromNullable(post)
-       override fun getTime() = 1L
+       override fun getTime() = time
        override fun getText() = text
        override fun isKnown() = known
-       override fun setKnown(known: Boolean): PostReply = this
 }
+
+fun createImage(sone: Sone): Image =
+               ImageImpl().modify().setSone(sone).update()
diff --git a/src/test/kotlin/net/pterodactylus/sone/utils/AutoCloseableBucketTest.kt b/src/test/kotlin/net/pterodactylus/sone/utils/AutoCloseableBucketTest.kt
deleted file mode 100644 (file)
index a705844..0000000
+++ /dev/null
@@ -1,26 +0,0 @@
-package net.pterodactylus.sone.utils
-
-import freenet.support.api.Bucket
-import net.pterodactylus.sone.test.mock
-import org.hamcrest.MatcherAssert.assertThat
-import org.hamcrest.Matchers.equalTo
-import org.junit.Test
-import org.mockito.Mockito.verify
-
-class AutoCloseableBucketTest {
-
-       private val bucket = mock<Bucket>()
-       private val autoCloseableBucket = AutoCloseableBucket(bucket)
-
-       @Test
-       fun `bucket can be retrieved`() {
-               assertThat(autoCloseableBucket.bucket, equalTo(bucket))
-       }
-
-       @Test
-       fun `bucket will be free’d when close is called`() {
-               autoCloseableBucket.close()
-               verify(bucket).free()
-       }
-
-}
diff --git a/src/test/kotlin/net/pterodactylus/sone/utils/DefaultOptionTest.kt b/src/test/kotlin/net/pterodactylus/sone/utils/DefaultOptionTest.kt
new file mode 100644 (file)
index 0000000..e445b39
--- /dev/null
@@ -0,0 +1,77 @@
+package net.pterodactylus.sone.utils
+
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.equalTo
+import org.hamcrest.Matchers.nullValue
+import org.hamcrest.Matchers.sameInstance
+import org.junit.Test
+
+/**
+ * Unit test for [DefaultOption].
+ */
+class DefaultOptionTest {
+
+       private val defaultValue = Any()
+       private val acceptedValue = Any()
+       private val matchesAcceptedValue = { it: Any -> it == acceptedValue }
+
+       @Test
+       fun `default option returns default value when unset`() {
+               val defaultOption = DefaultOption(defaultValue)
+               assertThat(defaultOption.get(), sameInstance(defaultValue))
+       }
+
+       @Test
+       fun `default option returns null for real when unset`() {
+               val defaultOption = DefaultOption(defaultValue)
+               assertThat(defaultOption.real, nullValue())
+       }
+
+       @Test
+       fun `default option will return set value`() {
+               val defaultOption = DefaultOption(defaultValue)
+               val newValue = Any()
+               defaultOption.set(newValue)
+               assertThat(defaultOption.get(), sameInstance(newValue))
+       }
+
+       @Test
+       fun `default option with validator accepts valid values`() {
+               val defaultOption = DefaultOption(defaultValue, matchesAcceptedValue)
+               defaultOption.set(acceptedValue)
+               assertThat(defaultOption.get(), sameInstance(acceptedValue))
+       }
+
+       @Test(expected = IllegalArgumentException::class)
+       fun `default option with validator rejects invalid values`() {
+               val defaultOption = DefaultOption(defaultValue, matchesAcceptedValue)
+               defaultOption.set(Any())
+       }
+
+       @Test
+       fun `default option validates objects correctly`() {
+               val defaultOption = DefaultOption(defaultValue, matchesAcceptedValue)
+               assertThat(defaultOption.validate(acceptedValue), equalTo(true))
+               assertThat(defaultOption.validate(Any()), equalTo(false))
+       }
+
+       @Test
+       fun `setting to null will restore default value`() {
+               val defaultOption = DefaultOption(defaultValue)
+               defaultOption.set(null)
+               assertThat(defaultOption.get(), sameInstance(defaultValue))
+       }
+
+       @Test
+       fun `validate without validator will validate null`() {
+               val defaultOption = DefaultOption(defaultValue)
+               assertThat(defaultOption.validate(null), equalTo(true))
+       }
+
+       @Test
+       fun `validate with validator will validate null`() {
+               val defaultOption = DefaultOption(defaultValue, matchesAcceptedValue)
+               assertThat(defaultOption.validate(null), equalTo(true))
+       }
+
+}
index 76dd443..f60d45f 100644 (file)
@@ -47,7 +47,7 @@ class OptionalsTest {
        }
 
        @Test
-       fun `null as optional is asent optional`() {
+       fun `null as optional is absent optional`() {
                val optional = null.asOptional()
                assertThat(optional.isPresent, equalTo(false))
        }
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/FreenetSessionProviderTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/FreenetSessionProviderTest.kt
new file mode 100644 (file)
index 0000000..835b14d
--- /dev/null
@@ -0,0 +1,126 @@
+/**
+ * Sone - FreenetSessionProviderTest.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 <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web
+
+import com.google.inject.Guice
+import freenet.clients.http.SessionManager
+import freenet.clients.http.ToadletContext
+import net.pterodactylus.sone.data.Sone
+import net.pterodactylus.sone.data.impl.IdOnlySone
+import net.pterodactylus.sone.database.SoneProvider
+import net.pterodactylus.sone.test.deepMock
+import net.pterodactylus.sone.test.eq
+import net.pterodactylus.sone.test.getInstance
+import net.pterodactylus.sone.test.isProvidedByMock
+import net.pterodactylus.sone.test.mock
+import net.pterodactylus.sone.test.whenever
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.equalTo
+import org.hamcrest.Matchers.notNullValue
+import org.hamcrest.Matchers.nullValue
+import org.hamcrest.Matchers.sameInstance
+import org.junit.Test
+import org.mockito.ArgumentMatchers.anyString
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+
+/**
+ * Unit test for FreenetSessionProviderTest.
+ */
+class FreenetSessionProviderTest {
+
+       private var soneProvider: SoneProvider = DelegatingSoneProvider(mock())
+       private val sessionManager: SessionManager = deepMock()
+       private val provider by lazy { FreenetSessionProvider(soneProvider, sessionManager) }
+       private val toadletContext = mock<ToadletContext>()
+
+       @Test
+       fun `provider returns null for current sone if no sone exists`() {
+               assertThat(provider.getCurrentSone(toadletContext), nullValue())
+       }
+
+       @Test
+       fun `provider returns singular sone if one sone exists`() {
+               val localSone: Sone = IdOnlySone("local")
+               soneProvider = object : DelegatingSoneProvider(mock()) {
+                       override val localSones: Collection<Sone> = listOf(localSone)
+               }
+               assertThat(provider.getCurrentSone(toadletContext), sameInstance(localSone))
+       }
+
+       @Test
+       fun `provider returns null if more than one sones exist but none is stored in the session`() {
+               soneProvider = object : DelegatingSoneProvider(mock()) {
+                       override val localSones: Collection<Sone> = listOf(IdOnlySone("1"), IdOnlySone("2"))
+               }
+               assertThat(provider.getCurrentSone(toadletContext), nullValue())
+       }
+
+       @Test
+       fun `provider returns sone if more than one sones exist and one is stored in the session`() {
+               val localSone = object : IdOnlySone("1") {
+                       override fun isLocal() = true
+               }
+               soneProvider = object : DelegatingSoneProvider(mock()) {
+                       override val localSones: Collection<Sone> = listOf(localSone, IdOnlySone("2"))
+                       override val soneLoader: (String) -> Sone? get() = { id -> localSone.takeIf { id == "1" } }
+               }
+               whenever(sessionManager.useSession(toadletContext).getAttribute("Sone.CurrentSone")).thenReturn("1")
+               assertThat(provider.getCurrentSone(toadletContext), equalTo<Sone>(localSone))
+       }
+
+       @Test
+       fun `provider sets sone ID in existing session`() {
+               val localSone: Sone = IdOnlySone("local")
+               provider.setCurrentSone(toadletContext, localSone)
+               verify(sessionManager.useSession(toadletContext)).setAttribute("Sone.CurrentSone", "local")
+       }
+
+       @Test
+       fun `provider sets sone ID in session it created`() {
+               val localSone: Sone = IdOnlySone("local")
+               whenever(sessionManager.useSession(toadletContext)).thenReturn(null)
+               provider.setCurrentSone(toadletContext, localSone)
+               verify(sessionManager.createSession(anyString(), eq(toadletContext))).setAttribute("Sone.CurrentSone", "local")
+       }
+
+       @Test
+       fun `provider removes sone ID in existing session`() {
+               provider.setCurrentSone(toadletContext, null)
+               verify(sessionManager.useSession(toadletContext)).removeAttribute("Sone.CurrentSone")
+       }
+
+       @Test
+       fun `provider does not create session if sone is to be removed and session does not exist`() {
+               whenever(sessionManager.useSession(toadletContext)).thenReturn(null)
+               provider.setCurrentSone(toadletContext, null)
+               verify(sessionManager.createSession(anyString(), eq(toadletContext)), never()).removeAttribute(anyString())
+       }
+
+       @Test
+       fun `provider can be created by guice`() {
+               val injector = Guice.createInjector(
+                               SessionManager::class.isProvidedByMock(),
+                               SoneProvider::class.isProvidedByMock()
+               )
+               assertThat(injector.getInstance<FreenetSessionProvider>(), notNullValue())
+       }
+
+}
+
+private open class DelegatingSoneProvider(private val soneProvider: SoneProvider) : SoneProvider by soneProvider
index 8840caf..9c0860c 100644 (file)
@@ -2,7 +2,6 @@ package net.pterodactylus.sone.web
 
 import com.google.inject.Guice.*
 import freenet.client.*
-import freenet.clients.http.*
 import freenet.support.api.*
 import net.pterodactylus.sone.core.*
 import net.pterodactylus.sone.data.*
@@ -13,7 +12,6 @@ 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.*
@@ -38,8 +36,7 @@ class WebInterfaceModuleTest {
                        SoneTextParser::class.isProvidedByMock(),
                        ElementLoader::class.isProvidedByMock(),
                        Loaders::class.isProvidedBy(loaders),
-                       HighLevelSimpleClient::class.isProvidedByMock(),
-                       SessionManager::class.isProvidedByMock()
+                       HighLevelSimpleClient::class.isProvidedByMock()
        )
        private val injector = createInjector(webInterfaceModule, *additionalModules)!!
        private val templateContext by lazy { injector.getInstance<TemplateContextFactory>().createTemplateContext()!! }
index b79e6df..e5f1c82 100644 (file)
@@ -1,6 +1,5 @@
 package net.pterodactylus.sone.web.ajax
 
-import com.google.common.base.Optional
 import net.pterodactylus.sone.data.Post
 import net.pterodactylus.sone.data.Sone
 import net.pterodactylus.sone.test.getInstance
index 1528d60..850cc9d 100644 (file)
@@ -1,10 +1,7 @@
 package net.pterodactylus.sone.web.ajax
 
-import net.pterodactylus.sone.data.Album
-import net.pterodactylus.sone.data.Album.Modifier.AlbumTitleMustNotBeEmpty
 import net.pterodactylus.sone.data.Sone
 import net.pterodactylus.sone.data.impl.AlbumImpl
-import net.pterodactylus.sone.test.deepMock
 import net.pterodactylus.sone.test.getInstance
 import net.pterodactylus.sone.test.mock
 import net.pterodactylus.sone.test.whenever
@@ -20,8 +17,7 @@ import org.junit.Test
 class EditAlbumAjaxPageTest : JsonPageTest("editAlbum.ajax", pageSupplier = ::EditAlbumAjaxPage) {
 
        private val sone = mock<Sone>()
-       private val localSone = mock<Sone>().apply { whenever(isLocal).thenReturn(true) }
-       private val album = mock<Album>().apply { whenever(id).thenReturn("album-id") }
+       private val album = AlbumImpl(sone, "album-id")
 
        @Test
        fun `request without album results in invalid-album-id`() {
@@ -30,7 +26,6 @@ class EditAlbumAjaxPageTest : JsonPageTest("editAlbum.ajax", pageSupplier = ::Ed
 
        @Test
        fun `request with non-local album results in not-authorized`() {
-               whenever(album.sone).thenReturn(sone)
                addAlbum(album)
                addRequestParameter("album", "album-id")
                assertThatJsonFailed("not-authorized")
@@ -38,11 +33,11 @@ class EditAlbumAjaxPageTest : JsonPageTest("editAlbum.ajax", pageSupplier = ::Ed
 
        @Test
        fun `request with moveLeft moves album to the left`() {
-               whenever(album.sone).thenReturn(localSone)
-               val swappedAlbum = mock<Album>().apply { whenever(id).thenReturn("swapped") }
-               val parentAlbum = mock<Album>()
-               whenever(parentAlbum.moveAlbumUp(album)).thenReturn(swappedAlbum)
-               whenever(album.parent).thenReturn(parentAlbum)
+               setupLocalSone()
+               AlbumImpl(sone).also {
+                       it.addAlbum(AlbumImpl(sone, "swapped"))
+                       it.addAlbum(album)
+               }
                addAlbum(album)
                addRequestParameter("album", "album-id")
                addRequestParameter("moveLeft", "true")
@@ -53,11 +48,11 @@ class EditAlbumAjaxPageTest : JsonPageTest("editAlbum.ajax", pageSupplier = ::Ed
 
        @Test
        fun `request with moveRight moves album to the right`() {
-               whenever(album.sone).thenReturn(localSone)
-               val swappedAlbum = mock<Album>().apply { whenever(id).thenReturn("swapped") }
-               val parentAlbum = mock<Album>()
-               whenever(parentAlbum.moveAlbumDown(album)).thenReturn(swappedAlbum)
-               whenever(album.parent).thenReturn(parentAlbum)
+               setupLocalSone()
+               AlbumImpl(sone).also {
+                       it.addAlbum(album)
+                       it.addAlbum(AlbumImpl(sone, "swapped"))
+               }
                addAlbum(album)
                addRequestParameter("album", "album-id")
                addRequestParameter("moveRight", "true")
@@ -68,9 +63,7 @@ class EditAlbumAjaxPageTest : JsonPageTest("editAlbum.ajax", pageSupplier = ::Ed
 
        @Test
        fun `request with missing title results in invalid-title`() {
-               whenever(album.sone).thenReturn(localSone)
-               whenever(album.modify()).thenReturn(deepMock())
-               whenever(album.modify().setTitle("")).thenThrow(AlbumTitleMustNotBeEmpty::class.java)
+               setupLocalSone()
                addAlbum(album)
                addRequestParameter("album", "album-id")
                assertThatJsonFailed("invalid-album-title")
@@ -95,4 +88,8 @@ class EditAlbumAjaxPageTest : JsonPageTest("editAlbum.ajax", pageSupplier = ::Ed
            assertThat(baseInjector.getInstance<EditAlbumAjaxPage>(), notNullValue())
        }
 
+       private fun setupLocalSone() {
+               whenever(sone.isLocal).thenReturn(true)
+       }
+
 }
index 26436a3..08b8f45 100644 (file)
@@ -1,9 +1,7 @@
 package net.pterodactylus.sone.web.ajax
 
-import net.pterodactylus.sone.data.Album
-import net.pterodactylus.sone.data.Image
 import net.pterodactylus.sone.data.Sone
-import net.pterodactylus.sone.data.impl.ImageImpl
+import net.pterodactylus.sone.data.impl.*
 import net.pterodactylus.sone.template.ParserFilter
 import net.pterodactylus.sone.template.RenderFilter
 import net.pterodactylus.sone.template.ShortenFilter
@@ -38,9 +36,8 @@ class EditImageAjaxPageTest : JsonPageTest("editImage.ajax") {
 
        @Test
        fun `request with non-local image results in not-authorized`() {
-               val image = mock<Image>()
                val sone = mock<Sone>()
-               whenever(image.sone).thenReturn(sone)
+               val image = ImageImpl().modify().setSone(sone).update()
                addImage(image, "image-id")
                addRequestParameter("image", "image-id")
                assertThatJsonFailed("not-authorized")
@@ -48,13 +45,12 @@ class EditImageAjaxPageTest : JsonPageTest("editImage.ajax") {
 
        @Test
        fun `moving an image to the left returns the correct values`() {
-               val image = mock<Image>().apply { whenever(id).thenReturn("image-id") }
                val sone = mock<Sone>().apply { whenever(isLocal).thenReturn(true) }
-               whenever(image.sone).thenReturn(sone)
-               val swapped = mock<Image>().apply { whenever(id).thenReturn("swapped") }
-               val album = mock<Album>()
-               whenever(album.moveImageUp(image)).thenReturn(swapped)
-               whenever(image.album).thenReturn(album)
+               val image = ImageImpl("image-id").modify().setSone(sone).update()
+               AlbumImpl(sone).also {
+                       it.addImage(ImageImpl("swapped").modify().setSone(sone).update())
+                       it.addImage(image)
+               }
                addImage(image)
                addRequestParameter("image", "image-id")
                addRequestParameter("moveLeft", "true")
@@ -66,13 +62,12 @@ class EditImageAjaxPageTest : JsonPageTest("editImage.ajax") {
 
        @Test
        fun `moving an image to the right returns the correct values`() {
-               val image = mock<Image>().apply { whenever(id).thenReturn("image-id") }
                val sone = mock<Sone>().apply { whenever(isLocal).thenReturn(true) }
-               whenever(image.sone).thenReturn(sone)
-               val swapped = mock<Image>().apply { whenever(id).thenReturn("swapped") }
-               val album = mock<Album>()
-               whenever(album.moveImageDown(image)).thenReturn(swapped)
-               whenever(image.album).thenReturn(album)
+               val image = ImageImpl("image-id").modify().setSone(sone).update()
+               AlbumImpl(sone).also {
+                       it.addImage(image)
+                       it.addImage(ImageImpl("swapped").modify().setSone(sone).update())
+               }
                addImage(image)
                addRequestParameter("image", "image-id")
                addRequestParameter("moveRight", "true")
@@ -84,9 +79,8 @@ class EditImageAjaxPageTest : JsonPageTest("editImage.ajax") {
 
        @Test
        fun `request with empty title results in invalid-image-title`() {
-               val image = mock<Image>().apply { whenever(id).thenReturn("image-id") }
                val sone = mock<Sone>().apply { whenever(isLocal).thenReturn(true) }
-               whenever(image.sone).thenReturn(sone)
+               val image = ImageImpl("image-id").modify().setSone(sone).update()
                addImage(image)
                addRequestParameter("image", "image-id")
                assertThatJsonFailed("invalid-image-title")
index f902533..ffd6f15 100644 (file)
@@ -80,9 +80,7 @@ open class TestObjects {
 
        init {
                whenever(webInterface.templateContextFactory).thenReturn(TemplateContextFactory())
-               whenever(webInterface.getCurrentSone(ArgumentMatchers.eq(toadletContext), ArgumentMatchers.anyBoolean())).thenReturn(currentSone)
-               whenever(webInterface.getCurrentSoneCreatingSession(toadletContext)).thenReturn(currentSone)
-               whenever(webInterface.getCurrentSoneWithoutCreatingSession(toadletContext)).thenReturn(currentSone)
+               whenever(webInterface.getCurrentSone(ArgumentMatchers.eq(toadletContext))).thenReturn(currentSone)
                whenever(webInterface.core).thenReturn(core)
                whenever(webInterface.formPassword).then { formPassword }
                whenever(webInterface.getNotifications(currentSone)).thenAnswer { notifications.values }
@@ -139,9 +137,7 @@ open class TestObjects {
        }
 
        protected fun unsetCurrentSone() {
-               whenever(webInterface.getCurrentSone(ArgumentMatchers.eq(toadletContext), ArgumentMatchers.anyBoolean())).thenReturn(null)
-               whenever(webInterface.getCurrentSoneWithoutCreatingSession(toadletContext)).thenReturn(null)
-               whenever(webInterface.getCurrentSoneCreatingSession(toadletContext)).thenReturn(null)
+               whenever(webInterface.getCurrentSone(ArgumentMatchers.eq(toadletContext))).thenReturn(null)
        }
 
        protected fun postRequest() {
index 3295bdd..1e3caab 100644 (file)
@@ -1,14 +1,11 @@
 package net.pterodactylus.sone.web.page
 
 import freenet.clients.http.*
-import freenet.clients.http.SessionManager.*
 import freenet.support.api.*
-import net.pterodactylus.sone.test.*
 import net.pterodactylus.util.web.*
 import org.hamcrest.MatcherAssert.*
 import org.hamcrest.Matchers.*
 import org.junit.*
-import org.mockito.*
 import org.mockito.Mockito.*
 import java.net.*
 
@@ -18,8 +15,7 @@ class FreenetRequestTest {
        private val method = Method.GET
        private val httpRequest = mock(HTTPRequest::class.java)
        private val toadletContext = mock(ToadletContext::class.java)
-       private val sessionManager = mock<SessionManager>()
-       private val request = FreenetRequest(uri, method, httpRequest, toadletContext, sessionManager)
+       private val request = FreenetRequest(uri, method, httpRequest, toadletContext)
 
        @Test
        fun `uri is retained correctly`() {
@@ -41,30 +37,4 @@ class FreenetRequestTest {
                assertThat(request.toadletContext, equalTo(toadletContext))
        }
 
-       @Test
-       fun `null is returned if no session exists`() {
-               assertThat(request.existingSession, nullValue())
-       }
-
-       @Test
-       fun `existing session can be retrieved`() {
-               val session = mock<Session>()
-               whenever(sessionManager.useSession(toadletContext)).thenReturn(session)
-               assertThat(request.existingSession, sameInstance(session))
-       }
-
-       @Test
-       fun `existing session is returned if it exists`() {
-               val session = mock<Session>()
-               whenever(sessionManager.useSession(toadletContext)).thenReturn(session)
-               assertThat(request.session, sameInstance(session))
-       }
-
-       @Test
-       fun `new session is returned if none exists`() {
-               val session = mock<Session>()
-               whenever(sessionManager.createSession(anyString(), ArgumentMatchers.eq(toadletContext))).thenReturn(session)
-               assertThat(request.session, sameInstance(session))
-       }
-
 }
index 7ac8c01..43b3c15 100644 (file)
@@ -2,21 +2,18 @@ package net.pterodactylus.sone.web.page
 
 import com.google.inject.*
 import freenet.client.*
-import freenet.clients.http.*
 import net.pterodactylus.sone.test.*
-import net.pterodactylus.sone.web.*
 import net.pterodactylus.util.web.*
 import org.hamcrest.MatcherAssert.*
 import org.hamcrest.Matchers.*
 import org.junit.*
 
 private val highLevelSimpleClient = mock<HighLevelSimpleClient>()
-private val sessionManager = mock<SessionManager>()
 private const val pathPrefix = "/some/prefix/"
 
 class PageToadletFactoryTest {
 
-       private val pageToadletFactory = PageToadletFactory(highLevelSimpleClient, sessionManager, pathPrefix)
+       private val pageToadletFactory = PageToadletFactory(highLevelSimpleClient, pathPrefix)
 
        @Test
        fun `page toadlet without menu name is created without menu name`() {
@@ -59,7 +56,6 @@ class PageToadletFactoryTest {
        fun `page toadlet factory can be created by guice`() {
                val injector = Guice.createInjector(
                                HighLevelSimpleClient::class.isProvidedBy(highLevelSimpleClient),
-                               SessionManager::class.isProvidedBy(sessionManager),
                                String::class.withNameIsProvidedBy("/Sone/", "toadletPathPrefix")
                )
            assertThat(injector.getInstance<PageToadletFactory>(), notNullValue())
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/page/PageToadletTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/page/PageToadletTest.kt
new file mode 100644 (file)
index 0000000..cfadfec
--- /dev/null
@@ -0,0 +1,230 @@
+/*
+ * Sone - PageToadletTest.kt - Copyright © 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 <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.page
+
+import freenet.client.HighLevelSimpleClient
+import freenet.clients.http.Cookie
+import freenet.clients.http.FProxyFetchInProgress.REFILTER_POLICY
+import freenet.clients.http.LinkEnabledCallback
+import freenet.clients.http.PageMaker
+import freenet.clients.http.ReceivedCookie
+import freenet.clients.http.Toadlet
+import freenet.clients.http.ToadletContainer
+import freenet.clients.http.ToadletContext
+import freenet.clients.http.bookmark.BookmarkManager
+import freenet.node.useralerts.UserAlertManager
+import freenet.support.HTMLNode
+import freenet.support.MultiValueTable
+import freenet.support.api.Bucket
+import freenet.support.api.BucketFactory
+import freenet.support.api.HTTPRequest
+import freenet.support.io.ArrayBucket
+import net.pterodactylus.sone.test.deepMock
+import net.pterodactylus.sone.test.mock
+import net.pterodactylus.sone.test.whenever
+import net.pterodactylus.util.web.Method
+import net.pterodactylus.util.web.Page
+import net.pterodactylus.util.web.Response
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.arrayContaining
+import org.hamcrest.Matchers.equalTo
+import org.hamcrest.Matchers.sameInstance
+import org.junit.Test
+import org.mockito.ArgumentMatchers.anyLong
+import java.net.URI
+import java.util.Date
+import kotlin.text.Charsets.UTF_8
+
+/**
+ * Unit test for PageToadletTest.
+ */
+class PageToadletTest {
+
+       private val highLevelSimpleClient = mock<HighLevelSimpleClient>()
+       private val httpRequest = mock<HTTPRequest>()
+       private val toadletContext = deepMock<ToadletContext>()
+
+       init {
+               whenever(toadletContext.bucketFactory.makeBucket(anyLong())).then { ArrayBucket() }
+       }
+
+       @Test
+       fun `get request is forwarded to page correctly`() {
+               var capturedRequest: FreenetRequest? = null
+               val page = object : TestPage() {
+                       override fun handleRequest(request: FreenetRequest, response: Response) =
+                                       super.handleRequest(request, response)
+                                                       .also { capturedRequest = request }
+               }
+               val pageToadlet = PageToadlet(highLevelSimpleClient, "MenuName", page, "/path/")
+               pageToadlet.handleMethodGET(URI("/test"), httpRequest, toadletContext)
+               assertThat(capturedRequest!!.uri, equalTo(URI("/test")))
+               assertThat(capturedRequest!!.method, equalTo(Method.GET))
+       }
+
+       @Test
+       fun `post request is forwarded to page correctly`() {
+               var capturedRequest: FreenetRequest? = null
+               val page = object : TestPage() {
+                       override fun handleRequest(request: FreenetRequest, response: Response) =
+                                       super.handleRequest(request, response)
+                                                       .also { capturedRequest = request }
+               }
+               val pageToadlet = PageToadlet(highLevelSimpleClient, "MenuName", page, "/path/")
+               pageToadlet.handleMethodPOST(URI("/test"), httpRequest, toadletContext)
+               assertThat(capturedRequest!!.uri, equalTo(URI("/test")))
+               assertThat(capturedRequest!!.method, equalTo(Method.POST))
+       }
+
+       @Test
+       fun `content written to response is written to context`() {
+               val page = object : TestPage() {
+                       override fun handleRequest(request: FreenetRequest, response: Response) =
+                                       response.apply {
+                                               statusCode = 123
+                                               statusText = "Works"
+                                               contentType = "data/test"
+                                               addHeader("Test", "Value")
+                                               addHeader("More", "true")
+                                               addHeader("Test", "Another")
+                                               write("Content")
+                                       }
+               }
+               val pageToadlet = PageToadlet(highLevelSimpleClient, "MenuName", page, "/path/")
+               var writtenData: ByteArray? = null
+               var capturedReply: CapturedReply? = null
+               val toadletContext = object : DelegatingToadletContext(this.toadletContext) {
+                       override fun sendReplyHeaders(code: Int, desc: String?, mvt: MultiValueTable<String, String>?, mimeType: String?, length: Long) =
+                                       sendReplyHeaders(code, desc, mvt, mimeType, length, false)
+
+                       override fun sendReplyHeaders(code: Int, desc: String?, mvt: MultiValueTable<String, String>?, mimeType: String?, length: Long, forceDisableJavascript: Boolean) {
+                               capturedReply = CapturedReply(code, desc, mvt, mimeType, length)
+                       }
+
+                       override fun writeData(data: ByteArray?, offset: Int, length: Int) {
+                               writtenData = data!!.copyOfRange(offset, offset + length)
+                       }
+
+                       override fun writeData(data: ByteArray?) = writeData(data, 0, data!!.size)
+                       override fun writeData(data: Bucket?) = writeData(data!!.inputStream.readBytes())
+               }
+               pageToadlet.handleMethodGET(URI("/test"), httpRequest, toadletContext)
+               assertThat(capturedReply!!.code, equalTo(123))
+               assertThat(capturedReply!!.status, equalTo("Works"))
+               assertThat(capturedReply!!.mimeType, equalTo("data/test"))
+               assertThat(capturedReply!!.length, equalTo(7L))
+               assertThat(capturedReply!!.headers!!.getArray("Test"), arrayContaining<Any>("Value", "Another"))
+               assertThat(capturedReply!!.headers!!.getArray("More"), arrayContaining<Any>("true"))
+               assertThat(writtenData!!.toString(UTF_8), equalTo("Content"))
+       }
+
+       @Test
+       fun `link-enabled is true for non-callback pages`() {
+               val page = TestPage()
+               val pageToadlet = PageToadlet(highLevelSimpleClient, "MenuName", page, "/path/")
+               assertThat(pageToadlet.isEnabled(toadletContext), equalTo(true))
+       }
+
+       @Test
+       fun `link-enabled is passed through for callback pages`() {
+               var capturedToadletContext: ToadletContext? = null
+               val page = object : TestPage(), LinkEnabledCallback {
+                       override fun isEnabled(ctx: ToadletContext?) = false.also { capturedToadletContext = toadletContext }
+               }
+               val pageToadlet = PageToadlet(highLevelSimpleClient, "MenuName", page, "/path/")
+               assertThat(pageToadlet.isEnabled(toadletContext), equalTo(false))
+               assertThat(capturedToadletContext, sameInstance(toadletContext))
+       }
+
+       @Test
+       fun `link excemption is false for non-freenet pages`() {
+               val page = TestPage()
+               val pageToadlet = PageToadlet(highLevelSimpleClient, "MenuName", page, "/path/")
+               assertThat(pageToadlet.isLinkExcepted(URI("/test")), equalTo(false))
+       }
+
+       @Test
+       fun `link excemption is passed through for freenet pages`() {
+               var capturedUri: URI? = null
+               val page = object : TestPage(), FreenetPage {
+                       override fun isLinkExcepted(link: URI) = true.also { capturedUri = link }
+               }
+               val pageToadlet = PageToadlet(highLevelSimpleClient, "MenuName", page, "/path/")
+               assertThat(pageToadlet.isLinkExcepted(URI("/test")), equalTo(true))
+               assertThat(capturedUri, equalTo(URI("/test")))
+       }
+
+       @Test
+       fun `path is created correctly from prefix and page path`() {
+               val page = object : TestPage() {
+                       override fun getPath() = "test-path"
+               }
+               val pageToadlet = PageToadlet(highLevelSimpleClient, "MenuName", page, "/path/")
+               assertThat(pageToadlet.path(), equalTo("/path/test-path"))
+       }
+
+       @Test
+       fun `menu name is returned correctly`() {
+               val pageToadlet = PageToadlet(highLevelSimpleClient, "MenuName", TestPage(), "/path/")
+               assertThat(pageToadlet.menuName, equalTo("MenuName"))
+       }
+
+}
+
+private data class CapturedReply(val code: Int, val status: String?, val headers: MultiValueTable<String, String>?, val mimeType: String?, val length: Long?)
+
+private open class TestPage : Page<FreenetRequest> {
+       override fun getPath() = ""
+       override fun isPrefixPage() = false
+       override fun handleRequest(request: FreenetRequest, response: Response) = response
+}
+
+private open class DelegatingToadletContext(private val toadletContext: ToadletContext) : ToadletContext {
+       override fun activeToadlet(): Toadlet = toadletContext.activeToadlet()
+       override fun forceDisconnect() = toadletContext.forceDisconnect()
+       override fun sendReplyHeaders(code: Int, desc: String?, mvt: MultiValueTable<String, String>?, mimeType: String?, length: Long) = toadletContext.sendReplyHeaders(code, desc, mvt, mimeType, length)
+       override fun sendReplyHeaders(code: Int, desc: String?, mvt: MultiValueTable<String, String>?, mimeType: String?, length: Long, forceDisableJavascript: Boolean) = toadletContext.sendReplyHeaders(code, desc, mvt, mimeType, length, forceDisableJavascript)
+       @Suppress("DEPRECATION")
+       override fun sendReplyHeaders(code: Int, desc: String?, mvt: MultiValueTable<String, String>?, mimeType: String?, length: Long, mTime: Date?) = toadletContext.sendReplyHeaders(code, desc, mvt, mimeType, length, mTime)
+       override fun getUri(): URI = toadletContext.uri
+       override fun getPageMaker(): PageMaker = toadletContext.pageMaker
+       override fun getBucketFactory(): BucketFactory = toadletContext.bucketFactory
+       override fun getHeaders(): MultiValueTable<String, String> = toadletContext.headers
+       override fun checkFullAccess(toadlet: Toadlet?): Boolean = toadletContext.checkFullAccess(toadlet)
+       override fun doRobots(): Boolean = toadletContext.doRobots()
+       override fun getReFilterPolicy(): REFILTER_POLICY = toadletContext.reFilterPolicy
+       override fun getAlertManager(): UserAlertManager = toadletContext.alertManager
+       override fun checkFormPassword(request: HTTPRequest?, redirectTo: String?): Boolean = toadletContext.checkFormPassword(request, redirectTo)
+       override fun checkFormPassword(request: HTTPRequest?): Boolean = toadletContext.checkFormPassword(request)
+       override fun addFormChild(parentNode: HTMLNode?, target: String?, id: String?): HTMLNode = toadletContext.addFormChild(parentNode, target, id)
+       override fun sendReplyHeadersFProxy(code: Int, desc: String?, mvt: MultiValueTable<String, String>?, mimeType: String?, length: Long) = toadletContext.sendReplyHeadersFProxy(code, desc, mvt, mimeType, length)
+       override fun setCookie(newCookie: Cookie?) = toadletContext.setCookie(newCookie)
+       override fun isAdvancedModeEnabled(): Boolean = toadletContext.isAdvancedModeEnabled
+       override fun disableProgressPage(): Boolean = toadletContext.disableProgressPage()
+       override fun writeData(data: ByteArray?, offset: Int, length: Int) = toadletContext.writeData(data, offset, length)
+       override fun writeData(data: ByteArray?) = toadletContext.writeData(data)
+       override fun writeData(data: Bucket?) = toadletContext.writeData(data)
+       override fun getCookie(domain: URI?, path: URI?, name: String?): ReceivedCookie? = toadletContext.getCookie(domain, path, name)
+       override fun getUniqueId(): String = toadletContext.uniqueId
+       override fun sendReplyHeadersStatic(code: Int, desc: String?, mvt: MultiValueTable<String, String>?, mimeType: String?, length: Long, mTime: Date?) = toadletContext.sendReplyHeadersStatic(code, desc, mvt, mimeType, length, mTime)
+       override fun getBookmarkManager(): BookmarkManager = toadletContext.bookmarkManager
+       override fun isAllowedFullAccess(): Boolean = toadletContext.isAllowedFullAccess
+       override fun hasFormPassword(request: HTTPRequest?): Boolean = toadletContext.hasFormPassword(request)
+       override fun getFormPassword(): String = toadletContext.formPassword
+       override fun getContainer(): ToadletContainer = toadletContext.container
+}
index 5be6ff1..0ec86db 100644 (file)
@@ -1,7 +1,6 @@
 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.test.*
@@ -19,10 +18,9 @@ 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 sessionManager = mock<SessionManager>()
        private val core = mock<Core>()
        private val webInterface = mock<WebInterface>()
-       private val soneRequest = SoneRequest(uri, method, httpRequest, toadletContext, sessionManager, core, webInterface)
+       private val soneRequest = SoneRequest(uri, method, httpRequest, toadletContext, core, webInterface)
 
        @Test
        fun `freenet request properties are retained correctly`() {
@@ -30,7 +28,6 @@ class SoneRequestTest {
                assertThat(soneRequest.method, equalTo(method))
                assertThat(soneRequest.httpRequest, equalTo(httpRequest))
                assertThat(soneRequest.toadletContext, equalTo(toadletContext))
-               assertThat(soneRequest.sessionManager, equalTo(sessionManager))
        }
 
        @Test
@@ -45,13 +42,12 @@ class SoneRequestTest {
 
        @Test
        fun `freenet request is wrapped correctly`() {
-           val freenetRequest = FreenetRequest(uri, method, httpRequest, toadletContext, sessionManager)
+           val freenetRequest = FreenetRequest(uri, method, httpRequest, toadletContext)
                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.sessionManager, equalTo(sessionManager))
                assertThat(wrappedSoneRequest.core, sameInstance(core))
                assertThat(wrappedSoneRequest.webInterface, sameInstance(webInterface))
        }
index 6212c90..a6f5d9d 100644 (file)
@@ -1,9 +1,7 @@
 package net.pterodactylus.sone.web.pages
 
-import com.google.common.base.Optional.*
 import net.pterodactylus.sone.data.*
 import net.pterodactylus.sone.test.*
-import net.pterodactylus.sone.utils.*
 import net.pterodactylus.sone.web.*
 import net.pterodactylus.sone.web.page.*
 import net.pterodactylus.util.web.Method.*
index 55b3d70..9830ffa 100644 (file)
@@ -1,6 +1,6 @@
 package net.pterodactylus.sone.web.pages
 
-import net.pterodactylus.sone.data.*
+import net.pterodactylus.sone.data.impl.*
 import net.pterodactylus.sone.test.*
 import net.pterodactylus.sone.web.*
 import net.pterodactylus.sone.web.page.*
@@ -16,20 +16,14 @@ import org.mockito.Mockito.verify
  */
 class DeleteAlbumPageTest : WebPageTest(::DeleteAlbumPage) {
 
-       private val sone = mock<Sone>()
-       private val album = mock<Album>()
-       private val parentAlbum = mock<Album>()
+       private val album = AlbumImpl(currentSone, "album-id")
+       private val parentAlbum = AlbumImpl(currentSone, "parent-id").also { it.addAlbum(album) }
 
        @Before
        fun setupAlbums() {
-               whenever(sone.id).thenReturn("sone-id")
-               whenever(sone.isLocal).thenReturn(true)
-               whenever(parentAlbum.id).thenReturn("parent-id")
-               whenever(parentAlbum.isRoot).thenReturn(true)
-               whenever(album.id).thenReturn("album-id")
-               whenever(album.sone).thenReturn(sone)
-               whenever(album.parent).thenReturn(parentAlbum)
-               whenever(sone.rootAlbum).thenReturn(parentAlbum)
+               whenever(currentSone.id).thenReturn("sone-id")
+               whenever(currentSone.isLocal).thenReturn(true)
+               whenever(currentSone.rootAlbum).thenReturn(parentAlbum)
        }
 
        @Test
@@ -50,7 +44,6 @@ class DeleteAlbumPageTest : WebPageTest(::DeleteAlbumPage) {
 
        @Test
        fun `get request with valid album ID sets album in template context`() {
-               val album = mock<Album>()
                addAlbum("album-id", album)
                addHttpRequestParameter("album", "album-id")
                page.processTemplate(freenetRequest, templateContext)
@@ -66,7 +59,7 @@ class DeleteAlbumPageTest : WebPageTest(::DeleteAlbumPage) {
        @Test
        fun `post request redirects to no permissions page if album is not local`() {
                setMethod(POST)
-               whenever(sone.isLocal).thenReturn(false)
+               whenever(currentSone.isLocal).thenReturn(false)
                addAlbum("album-id", album)
                addHttpRequestPart("album", "album-id")
                verifyRedirect("noPermission.html")
@@ -94,12 +87,12 @@ class DeleteAlbumPageTest : WebPageTest(::DeleteAlbumPage) {
        @Test
        fun `album is deleted and page redirects to album if parent album is not root album`() {
                setMethod(POST)
-               whenever(parentAlbum.isRoot).thenReturn(false)
-               whenever(sone.rootAlbum).thenReturn(mock())
-               addAlbum("album-id", album)
-               addHttpRequestPart("album", "album-id")
-               verifyRedirect("imageBrowser.html?album=parent-id") {
-                       verify(core).deleteAlbum(album)
+               val subAlbum = AlbumImpl(currentSone, "sub-album-id")
+               album.addAlbum(subAlbum)
+               addAlbum("sub-album-id", subAlbum)
+               addHttpRequestPart("album", "sub-album-id")
+               verifyRedirect("imageBrowser.html?album=album-id") {
+                       verify(core).deleteAlbum(subAlbum)
                }
        }
 
index cecde50..eccf949 100644 (file)
@@ -1,6 +1,7 @@
 package net.pterodactylus.sone.web.pages
 
 import net.pterodactylus.sone.data.*
+import net.pterodactylus.sone.data.impl.*
 import net.pterodactylus.sone.test.*
 import net.pterodactylus.sone.web.*
 import net.pterodactylus.sone.web.page.*
@@ -15,16 +16,12 @@ import org.mockito.Mockito.*
  */
 class DeleteImagePageTest : WebPageTest(::DeleteImagePage) {
 
-       private val image = mock<Image>()
        private val sone = mock<Sone>()
+       private val image = ImageImpl("image-id").modify().setSone(sone).update()!!
 
        @Before
        fun setupImage() {
-               val album = mock<Album>()
-               whenever(album.id).thenReturn("album-id")
-               whenever(image.id).thenReturn("image-id")
-               whenever(image.sone).thenReturn(sone)
-               whenever(image.album).thenReturn(album)
+               AlbumImpl(sone, "album-id").also { it.addImage(image) }
                whenever(sone.isLocal).thenReturn(true)
        }
 
index 72e814b..97e8865 100644 (file)
@@ -1,8 +1,6 @@
 package net.pterodactylus.sone.web.pages
 
-import net.pterodactylus.sone.data.*
-import net.pterodactylus.sone.data.Album.*
-import net.pterodactylus.sone.data.Album.Modifier.*
+import net.pterodactylus.sone.data.impl.*
 import net.pterodactylus.sone.test.*
 import net.pterodactylus.sone.web.*
 import net.pterodactylus.util.web.Method.*
@@ -16,20 +14,16 @@ import org.mockito.Mockito.*
  */
 class EditAlbumPageTest : WebPageTest(::EditAlbumPage) {
 
-       private val album = mock<Album>()
-       private val parentAlbum = mock<Album>()
-       private val modifier = mockBuilder<Modifier>()
-       private val sone = mock<Sone>()
+       private val album = AlbumImpl(currentSone, "album-id")
+       private val parentAlbum = AlbumImpl(currentSone, "parent-id").also {
+               it.addAlbum(AlbumImpl(currentSone))
+               it.addAlbum(album)
+               it.addAlbum(AlbumImpl(currentSone))
+       }
 
        @Before
        fun setup() {
-               whenever(album.id).thenReturn("album-id")
-               whenever(album.sone).thenReturn(sone)
-               whenever(album.parent).thenReturn(parentAlbum)
-               whenever(album.modify()).thenReturn(modifier)
-               whenever(modifier.update()).thenReturn(album)
-               whenever(parentAlbum.id).thenReturn("parent-id")
-               whenever(sone.isLocal).thenReturn(true)
+               whenever(currentSone.isLocal).thenReturn(true)
                addHttpRequestHeader("Host", "www.te.st")
        }
 
@@ -63,7 +57,7 @@ class EditAlbumPageTest : WebPageTest(::EditAlbumPage) {
        @Test
        fun `post request with album of non-local sone redirects to no permissions page`() {
                setMethod(POST)
-               whenever(sone.isLocal).thenReturn(false)
+               whenever(currentSone.isLocal).thenReturn(false)
                addAlbum("album-id", album)
                addHttpRequestPart("album", "album-id")
                verifyRedirect("noPermission.html")
@@ -76,7 +70,7 @@ class EditAlbumPageTest : WebPageTest(::EditAlbumPage) {
                addHttpRequestPart("album", "album-id")
                addHttpRequestPart("moveLeft", "true")
                verifyRedirect("imageBrowser.html?album=parent-id") {
-                       verify(parentAlbum).moveAlbumUp(album)
+                       assertThat(parentAlbum.albums.indexOf(album), equalTo(0))
                        verify(core).touchConfiguration()
                }
        }
@@ -88,7 +82,7 @@ class EditAlbumPageTest : WebPageTest(::EditAlbumPage) {
                addHttpRequestPart("album", "album-id")
                addHttpRequestPart("moveRight", "true")
                verifyRedirect("imageBrowser.html?album=parent-id") {
-                       verify(parentAlbum).moveAlbumDown(album)
+                       assertThat(parentAlbum.albums.indexOf(album), equalTo(2))
                        verify(core).touchConfiguration()
                }
        }
@@ -98,7 +92,6 @@ class EditAlbumPageTest : WebPageTest(::EditAlbumPage) {
                setMethod(POST)
                addAlbum("album-id", album)
                addHttpRequestPart("album", "album-id")
-               whenever(modifier.setTitle("")).thenThrow(AlbumTitleMustNotBeEmpty())
                verifyRedirect("emptyAlbumTitle.html")
        }
 
@@ -110,9 +103,8 @@ class EditAlbumPageTest : WebPageTest(::EditAlbumPage) {
                addHttpRequestPart("title", "title")
                addHttpRequestPart("description", "description")
                verifyRedirect("imageBrowser.html?album=album-id") {
-                       verify(modifier).setTitle("title")
-                       verify(modifier).setDescription("description")
-                       verify(modifier).update()
+                       assertThat(album.title, equalTo("title"))
+                       assertThat(album.description, equalTo("description"))
                        verify(core).touchConfiguration()
                }
        }
index e43551c..5f59031 100644 (file)
@@ -1,8 +1,7 @@
 package net.pterodactylus.sone.web.pages
 
 import net.pterodactylus.sone.data.*
-import net.pterodactylus.sone.data.Image.*
-import net.pterodactylus.sone.data.Image.Modifier.*
+import net.pterodactylus.sone.data.impl.*
 import net.pterodactylus.sone.test.*
 import net.pterodactylus.sone.web.*
 import net.pterodactylus.util.web.Method.*
@@ -16,19 +15,17 @@ import org.mockito.Mockito.*
  */
 class EditImagePageTest : WebPageTest(::EditImagePage) {
 
-       private val image = mock<Image>()
-       private val modifier = mockBuilder<Modifier>()
        private val sone = mock<Sone>()
-       private val album = mock<Album>()
+       private val image = ImageImpl("image-id").modify().setSone(sone).update()!!
+       private val album = AlbumImpl(sone, "album-id").also {
+               it.addImage(ImageImpl("1").modify().setSone(sone).update())
+               it.addImage(image)
+               it.addImage(ImageImpl("2").modify().setSone(sone).update())
+       }
 
        @Before
        fun setupImage() {
                whenever(sone.isLocal).thenReturn(true)
-               whenever(album.id).thenReturn("album-id")
-               whenever(modifier.update()).thenReturn(image)
-               whenever(image.sone).thenReturn(sone)
-               whenever(image.album).thenReturn(album)
-               whenever(image.modify()).thenReturn(modifier)
        }
 
        @Test
@@ -75,7 +72,7 @@ class EditImagePageTest : WebPageTest(::EditImagePage) {
                addHttpRequestPart("returnPage", "return.html")
                addHttpRequestPart("moveLeft", "true")
                verifyRedirect("return.html") {
-                       verify(album).moveImageUp(image)
+                       assertThat(album.images.indexOf(image), equalTo(0))
                        verify(core).touchConfiguration()
                }
        }
@@ -88,7 +85,7 @@ class EditImagePageTest : WebPageTest(::EditImagePage) {
                addHttpRequestPart("returnPage", "return.html")
                addHttpRequestPart("moveRight", "true")
                verifyRedirect("return.html") {
-                       verify(album).moveImageDown(image)
+                       assertThat(album.images.indexOf(image), equalTo(2))
                        verify(core).touchConfiguration()
                }
        }
@@ -100,7 +97,6 @@ class EditImagePageTest : WebPageTest(::EditImagePage) {
                addHttpRequestPart("image", "image-id")
                addHttpRequestPart("returnPage", "return.html")
                addHttpRequestPart("title", "   ")
-               whenever(modifier.update()).doThrow<ImageTitleMustNotBeEmpty>()
                verifyRedirect("emptyImageTitle.html") {
                        verify(core, never()).touchConfiguration()
                }
@@ -115,9 +111,8 @@ class EditImagePageTest : WebPageTest(::EditImagePage) {
                addHttpRequestPart("title", "Title")
                addHttpRequestPart("description", "Description")
                verifyRedirect("return.html") {
-                       verify(modifier).setTitle("Title")
-                       verify(modifier).setDescription("Description")
-                       verify(modifier).update()
+                       assertThat(image.title, equalTo("Title"))
+                       assertThat(image.description, equalTo("Description"))
                        verify(core).touchConfiguration()
                }
        }
@@ -132,9 +127,8 @@ class EditImagePageTest : WebPageTest(::EditImagePage) {
                addHttpRequestHeader("Host", "www.te.st")
                addHttpRequestPart("description", "Get http://www.te.st/KSK@GPL.txt")
                verifyRedirect("return.html") {
-                       verify(modifier).setTitle("Title")
-                       verify(modifier).setDescription("Get KSK@GPL.txt")
-                       verify(modifier).update()
+                       assertThat(image.title, equalTo("Title"))
+                       assertThat(image.description, equalTo("Get KSK@GPL.txt"))
                        verify(core).touchConfiguration()
                }
        }
index 74c3851..ac909ea 100644 (file)
@@ -1,6 +1,7 @@
 package net.pterodactylus.sone.web.pages
 
 import net.pterodactylus.sone.data.*
+import net.pterodactylus.sone.data.impl.*
 import net.pterodactylus.sone.test.*
 import net.pterodactylus.sone.web.*
 import net.pterodactylus.sone.web.page.*
@@ -21,16 +22,13 @@ class EditProfilePageTest : WebPageTest(::EditProfilePage) {
 
        @Before
        fun setupProfile() {
-               val avatar = mock<Image>()
-               whenever(avatar.id).thenReturn("image-id")
-               whenever(avatar.sone).thenReturn(currentSone)
                profile.firstName = "First"
                profile.middleName = "Middle"
                profile.lastName = "Last"
                profile.birthDay = 31
                profile.birthMonth = 12
                profile.birthYear = 1999
-               profile.setAvatar(avatar)
+               profile.setAvatar(ImageImpl("image-id").modify().setSone(currentSone).update())
                whenever(currentSone.profile).thenReturn(profile)
        }
 
@@ -120,9 +118,7 @@ class EditProfilePageTest : WebPageTest(::EditProfilePage) {
 
        @Test
        fun `post request with new avatar ID and save profile saves the profile and redirects back to profile edit page`() {
-               val newAvatar = mock<Image>()
-               whenever(newAvatar.sone).thenReturn(currentSone)
-               whenever(newAvatar.id).thenReturn("avatar-id")
+               val newAvatar = ImageImpl("avatar-id").modify().setSone(currentSone).update()
                addImage("avatar-id", newAvatar)
                verifySingleFieldCanBeChanged("avatarId", "avatar-id") { profile.avatar }
        }
index 1533f4b..4a2304c 100644 (file)
@@ -1,6 +1,8 @@
 package net.pterodactylus.sone.web.pages
 
 import net.pterodactylus.sone.data.*
+import net.pterodactylus.sone.data.impl.AlbumImpl
+import net.pterodactylus.sone.data.impl.ImageImpl
 import net.pterodactylus.sone.test.*
 import net.pterodactylus.sone.web.*
 import net.pterodactylus.sone.web.page.*
@@ -32,7 +34,7 @@ class ImageBrowserPageTest : WebPageTest(::ImageBrowserPage) {
 
        @Test
        fun `get request with album sets album and page in template context`() {
-               val album = mock<Album>()
+               val album = AlbumImpl(currentSone, "album-id")
                addAlbum("album-id", album)
                addHttpRequestParameter("album", "album-id")
                addHttpRequestParameter("page", "5")
@@ -45,7 +47,7 @@ class ImageBrowserPageTest : WebPageTest(::ImageBrowserPage) {
 
        @Test
        fun `get request with image sets image in template context`() {
-               val image = mock<Image>()
+               val image = ImageImpl()
                addImage("image-id", image)
                addHttpRequestParameter("image", "image-id")
                verifyNoRedirect {
@@ -105,16 +107,13 @@ class ImageBrowserPageTest : WebPageTest(::ImageBrowserPage) {
 
        private fun createSone(firstAlbumTitle: String, secondAlbumTitle: String): Sone {
                return mock<Sone>().apply {
-                       val rootAlbum = mock<Album>()
-                       val firstAlbum = mock<Album>()
-                       val firstImage = mock<Image>().run { whenever(isInserted).thenReturn(true); this }
-                       whenever(firstAlbum.images).thenReturn(listOf(firstImage))
-                       val secondAlbum = mock<Album>()
-                       val secondImage = mock<Image>().run { whenever(isInserted).thenReturn(true); this }
-                       whenever(secondAlbum.images).thenReturn(listOf(secondImage))
-                       whenever(firstAlbum.title).thenReturn(firstAlbumTitle)
-                       whenever(secondAlbum.title).thenReturn(secondAlbumTitle)
-                       whenever(rootAlbum.albums).thenReturn(listOf(firstAlbum, secondAlbum))
+                       val rootAlbum = AlbumImpl(this)
+                       val firstAlbum = AlbumImpl(this).modify().setTitle(firstAlbumTitle).update()
+                       firstAlbum.addImage(ImageImpl("1").modify().setSone(this).setKey("key").update())
+                       val secondAlbum = AlbumImpl(this).modify().setTitle(secondAlbumTitle).update()
+                       secondAlbum.addImage(ImageImpl("2").modify().setSone(this).setKey("key").update())
+                       rootAlbum.addAlbum(firstAlbum)
+                       rootAlbum.addAlbum(secondAlbum)
                        whenever(this.rootAlbum).thenReturn(rootAlbum)
                }
        }
index 14cae74..23987b6 100644 (file)
@@ -1,6 +1,7 @@
 package net.pterodactylus.sone.web.pages
 
 import net.pterodactylus.sone.data.*
+import net.pterodactylus.sone.data.impl.*
 import net.pterodactylus.sone.freenet.wot.*
 import net.pterodactylus.sone.test.*
 import net.pterodactylus.sone.utils.*
@@ -37,11 +38,12 @@ class KnownSonesPageTest : WebPageTest(::KnownSonesPage) {
                whenever(this.time).thenReturn(time)
                whenever(this.posts).thenReturn((0..(posts - 1)).map { mock<Post>() })
                whenever(this.replies).thenReturn((0..(replies - 1)).map { mock<PostReply>() }.toSet())
-               val album = mock<Album>()
-               whenever(album.images).thenReturn(((0..(images - 1)).map { mock<Image>() }))
-               val rootAlbum = mock<Album>().apply {
-                       whenever(albums).thenReturn(listOf(album))
+               val album = AlbumImpl(this)
+               repeat(images) {
+                       ImageImpl().modify().setSone(this).update()
+                                       .also(album::addImage)
                }
+               val rootAlbum = AlbumImpl(this).also { it.addAlbum(album) }
                whenever(this.rootAlbum).thenReturn(rootAlbum)
                whenever(this.profile).thenReturn(mock())
                whenever(id).thenReturn(name.toLowerCase())
index d2929d9..d679e85 100644 (file)
@@ -27,6 +27,7 @@ class OptionsPageTest : WebPageTest(::OptionsPage) {
                core.preferences.newRequireFullAccess = true
                core.preferences.newPostCutOffLength = 51
                core.preferences.newPostsPerPage = 10
+               core.preferences.newStrictFiltering = true
        }
 
        @Before
@@ -76,6 +77,7 @@ class OptionsPageTest : WebPageTest(::OptionsPage) {
                        assertThat(templateContext["require-full-access"], equalTo<Any>(true))
                        assertThat(templateContext["post-cut-off-length"], equalTo<Any>(51))
                        assertThat(templateContext["posts-per-page"], equalTo<Any>(10))
+                       assertThat(templateContext["strict-filtering"], equalTo<Any>(true))
                }
        }
 
@@ -308,6 +310,16 @@ class OptionsPageTest : WebPageTest(::OptionsPage) {
        }
 
        @Test
+       fun `strict filtering can be set to true`() {
+               verifyThatPreferencesCanBeSet("strict-filtering", "checked", true) { core.preferences.strictFiltering }
+       }
+
+       @Test
+       fun `strict filtering can be set to false`() {
+               verifyThatPreferencesCanBeSet("strict-filtering", null, false) { core.preferences.strictFiltering }
+       }
+
+       @Test
        fun `page can be created by dependency injection`() {
                assertThat(baseInjector.getInstance<OptionsPage>(), notNullValue())
        }
index 370c0a7..4c99759 100644 (file)
@@ -1,10 +1,9 @@
 package net.pterodactylus.sone.web.pages
 
 import net.pterodactylus.sone.data.*
-import net.pterodactylus.sone.data.Image.*
+import net.pterodactylus.sone.data.impl.*
 import net.pterodactylus.sone.test.getInstance
 import net.pterodactylus.sone.test.mock
-import net.pterodactylus.sone.test.mockBuilder
 import net.pterodactylus.sone.test.whenever
 import net.pterodactylus.sone.web.*
 import net.pterodactylus.sone.web.page.*
@@ -20,10 +19,7 @@ import org.mockito.Mockito.eq
  */
 class UploadImagePageTest : WebPageTest(::UploadImagePage) {
 
-       private val parentAlbum = mock<Album>().apply {
-               whenever(id).thenReturn("parent-id")
-               whenever(sone).thenReturn(currentSone)
-       }
+       private val parentAlbum = AlbumImpl(currentSone, "parent-id")
 
        @Test
        fun `page returns correct path`() {
@@ -58,9 +54,9 @@ class UploadImagePageTest : WebPageTest(::UploadImagePage) {
        @Test
        fun `post request with parent that is not the current sone results in no permission error page`() {
                setMethod(POST)
+               val remoteAlbum = AlbumImpl(mock(), "parent-id")
+               addAlbum("parent-id", remoteAlbum)
                addHttpRequestPart("parent", "parent-id")
-               whenever(parentAlbum.sone).thenReturn(mock())
-               addAlbum("parent-id", parentAlbum)
                verifyRedirect("noPermission.html")
        }
 
@@ -97,19 +93,14 @@ class UploadImagePageTest : WebPageTest(::UploadImagePage) {
                addHttpRequestHeader("Host", "localhost:8888")
                addUploadedFile("image", "upload-image-value-image.png", "image/png", "upload-image-value-image.png")
                val temporaryImage = TemporaryImage("temp-image")
-               val imageModifier = mockBuilder<Modifier>()
-               val image = mock<Image>().apply {
-                       whenever(modify()).thenReturn(imageModifier)
-               }
+               val image = ImageImpl()
                whenever(core.createTemporaryImage(eq("image/png"), any())).thenReturn(temporaryImage)
                whenever(core.createImage(currentSone, parentAlbum, temporaryImage)).thenReturn(image)
                verifyRedirect("imageBrowser.html?album=parent-id") {
-                       verify(image).modify()
-                       verify(imageModifier).setWidth(2)
-                       verify(imageModifier).setHeight(1)
-                       verify(imageModifier).setTitle("Title")
-                       verify(imageModifier).setDescription("Description @ KSK@foo")
-                       verify(imageModifier).update()
+                       assertThat(image.width, equalTo(2))
+                       assertThat(image.height, equalTo(1))
+                       assertThat(image.title, equalTo("Title"))
+                       assertThat(image.description, equalTo("Description @ KSK@foo"))
                }
        }
 
index 07ae727..badc2bc 100644 (file)
@@ -42,7 +42,6 @@ open class WebPageTest(pageSupplier: (WebInterface, Loaders, TemplateRenderer) -
        val core = webInterface.core
        val eventBus = mock<EventBus>()
        val preferences = Preferences(eventBus)
-       val sessionManager = mock<SessionManager>()
 
        open val page by lazy { pageSupplier(webInterface, loaders, templateRenderer) }
 
@@ -50,7 +49,6 @@ open class WebPageTest(pageSupplier: (WebInterface, Loaders, TemplateRenderer) -
        val freenetRequest = mock<FreenetRequest>()
 
        init {
-               whenever(freenetRequest.sessionManager).thenReturn(sessionManager)
                whenever(freenetRequest.uri).thenReturn(mock())
        }
 
@@ -106,10 +104,7 @@ open class WebPageTest(pageSupplier: (WebInterface, Loaders, TemplateRenderer) -
        }
 
        private fun setupWebInterface() {
-               whenever(webInterface.sessionManager).thenReturn(sessionManager)
-               whenever(webInterface.getCurrentSoneCreatingSession(eq(toadletContext))).thenReturn(currentSone)
-               whenever(webInterface.getCurrentSone(eq(toadletContext), anyBoolean())).thenReturn(currentSone)
-               whenever(webInterface.getCurrentSoneWithoutCreatingSession(eq(toadletContext))).thenReturn(currentSone)
+               whenever(webInterface.getCurrentSone(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)
@@ -175,9 +170,7 @@ open class WebPageTest(pageSupplier: (WebInterface, Loaders, TemplateRenderer) -
        }
 
        fun unsetCurrentSone() {
-               whenever(webInterface.getCurrentSoneCreatingSession(eq(toadletContext))).thenReturn(null)
-               whenever(webInterface.getCurrentSone(eq(toadletContext), anyBoolean())).thenReturn(null)
-               whenever(webInterface.getCurrentSoneWithoutCreatingSession(eq(toadletContext))).thenReturn(null)
+               whenever(webInterface.getCurrentSone(eq(toadletContext))).thenReturn(null)
        }
 
        fun addOwnIdentity(ownIdentity: OwnIdentity) {