🔀 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)
528 files changed:
.builds/jdk-1.8.yml
.gitignore
build.gradle
gradle/wrapper/gradle-wrapper.properties
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/ImageInserter.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.java [deleted file]
src/main/java/net/pterodactylus/sone/core/SoneDownloaderImpl.java
src/main/java/net/pterodactylus/sone/core/SoneException.java
src/main/java/net/pterodactylus/sone/core/SoneInsertException.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/SoneParser.java
src/main/java/net/pterodactylus/sone/core/SoneRescuer.java
src/main/java/net/pterodactylus/sone/core/SoneUri.java [deleted file]
src/main/java/net/pterodactylus/sone/core/UpdateChecker.java [deleted file]
src/main/java/net/pterodactylus/sone/core/WebOfTrustUpdater.java
src/main/java/net/pterodactylus/sone/core/WebOfTrustUpdaterImpl.java
src/main/java/net/pterodactylus/sone/core/event/ImageEvent.java [deleted file]
src/main/java/net/pterodactylus/sone/core/event/ImageInsertAbortedEvent.java [deleted file]
src/main/java/net/pterodactylus/sone/core/event/ImageInsertFailedEvent.java [deleted file]
src/main/java/net/pterodactylus/sone/core/event/ImageInsertFinishedEvent.java [deleted file]
src/main/java/net/pterodactylus/sone/core/event/ImageInsertStartedEvent.java [deleted file]
src/main/java/net/pterodactylus/sone/core/event/MarkPostKnownEvent.java [deleted file]
src/main/java/net/pterodactylus/sone/core/event/MarkPostReplyKnownEvent.java [deleted file]
src/main/java/net/pterodactylus/sone/core/event/MarkSoneKnownEvent.java [deleted file]
src/main/java/net/pterodactylus/sone/core/event/NewSoneFoundEvent.java [deleted file]
src/main/java/net/pterodactylus/sone/core/event/PostEvent.java [deleted file]
src/main/java/net/pterodactylus/sone/core/event/PostReplyEvent.java [deleted file]
src/main/java/net/pterodactylus/sone/core/event/SoneEvent.java [deleted file]
src/main/java/net/pterodactylus/sone/core/event/SoneInsertAbortedEvent.java [deleted file]
src/main/java/net/pterodactylus/sone/core/event/SoneInsertedEvent.java [deleted file]
src/main/java/net/pterodactylus/sone/core/event/SoneInsertingEvent.java [deleted file]
src/main/java/net/pterodactylus/sone/core/event/SoneLockedEvent.java [deleted file]
src/main/java/net/pterodactylus/sone/core/event/SoneRemovedEvent.java [deleted file]
src/main/java/net/pterodactylus/sone/core/event/SoneUnlockedEvent.java [deleted file]
src/main/java/net/pterodactylus/sone/core/event/UpdateFoundEvent.java [deleted file]
src/main/java/net/pterodactylus/sone/data/Album.java
src/main/java/net/pterodactylus/sone/data/Client.java
src/main/java/net/pterodactylus/sone/data/Image.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/TemporaryImage.java
src/main/java/net/pterodactylus/sone/data/impl/AbstractAlbumBuilder.java
src/main/java/net/pterodactylus/sone/data/impl/AbstractImageBuilder.java
src/main/java/net/pterodactylus/sone/data/impl/AbstractPostBuilder.java
src/main/java/net/pterodactylus/sone/data/impl/AbstractPostReplyBuilder.java
src/main/java/net/pterodactylus/sone/data/impl/AbstractReplyBuilder.java
src/main/java/net/pterodactylus/sone/data/impl/AlbumBuilderImpl.java
src/main/java/net/pterodactylus/sone/data/impl/AlbumImpl.java
src/main/java/net/pterodactylus/sone/data/impl/IdOnlySone.java
src/main/java/net/pterodactylus/sone/data/impl/ImageBuilderImpl.java
src/main/java/net/pterodactylus/sone/data/impl/ImageImpl.java
src/main/java/net/pterodactylus/sone/data/impl/SoneImpl.java
src/main/java/net/pterodactylus/sone/database/DatabaseException.java
src/main/java/net/pterodactylus/sone/database/memory/MemoryDatabase.kt [deleted file]
src/main/java/net/pterodactylus/sone/database/memory/MemoryPost.java
src/main/java/net/pterodactylus/sone/database/memory/MemoryPostBuilder.java
src/main/java/net/pterodactylus/sone/database/memory/MemoryPostReply.java
src/main/java/net/pterodactylus/sone/database/memory/MemoryPostReplyBuilder.java
src/main/java/net/pterodactylus/sone/fcp/CreatePostCommand.java
src/main/java/net/pterodactylus/sone/fcp/CreateReplyCommand.java
src/main/java/net/pterodactylus/sone/fcp/DeletePostCommand.java
src/main/java/net/pterodactylus/sone/fcp/DeleteReplyCommand.java
src/main/java/net/pterodactylus/sone/fcp/FcpInterface.java
src/main/java/net/pterodactylus/sone/fcp/GetLocalSonesCommand.java
src/main/java/net/pterodactylus/sone/fcp/GetPostCommand.java
src/main/java/net/pterodactylus/sone/fcp/GetPostFeedCommand.java
src/main/java/net/pterodactylus/sone/fcp/GetPostsCommand.java
src/main/java/net/pterodactylus/sone/fcp/GetSoneCommand.java
src/main/java/net/pterodactylus/sone/fcp/GetSonesCommand.java
src/main/java/net/pterodactylus/sone/fcp/LikePostCommand.java
src/main/java/net/pterodactylus/sone/fcp/LikeReplyCommand.java
src/main/java/net/pterodactylus/sone/fcp/LockSoneCommand.java
src/main/java/net/pterodactylus/sone/fcp/UnlockSoneCommand.java
src/main/java/net/pterodactylus/sone/fcp/VersionCommand.java
src/main/java/net/pterodactylus/sone/freenet/Key.java [deleted file]
src/main/java/net/pterodactylus/sone/freenet/L10nFilter.java [deleted file]
src/main/java/net/pterodactylus/sone/freenet/PluginStoreConfigurationBackend.java
src/main/java/net/pterodactylus/sone/freenet/SimpleFieldSetBuilder.java
src/main/java/net/pterodactylus/sone/freenet/fcp/AbstractCommand.java
src/main/java/net/pterodactylus/sone/freenet/fcp/Command.java
src/main/java/net/pterodactylus/sone/freenet/fcp/FcpException.java
src/main/java/net/pterodactylus/sone/freenet/plugin/PluginConnector.java [deleted file]
src/main/java/net/pterodactylus/sone/freenet/plugin/PluginException.java [deleted file]
src/main/java/net/pterodactylus/sone/freenet/plugin/event/ReceivedReplyEvent.java [deleted file]
src/main/java/net/pterodactylus/sone/freenet/wot/Context.java [deleted file]
src/main/java/net/pterodactylus/sone/freenet/wot/DefaultIdentity.java [deleted file]
src/main/java/net/pterodactylus/sone/freenet/wot/DefaultOwnIdentity.java [deleted file]
src/main/java/net/pterodactylus/sone/freenet/wot/Identity.java
src/main/java/net/pterodactylus/sone/freenet/wot/IdentityChangeDetector.java [deleted file]
src/main/java/net/pterodactylus/sone/freenet/wot/IdentityChangeEventSender.java [deleted file]
src/main/java/net/pterodactylus/sone/freenet/wot/IdentityLoader.java [deleted file]
src/main/java/net/pterodactylus/sone/freenet/wot/IdentityManager.java [deleted file]
src/main/java/net/pterodactylus/sone/freenet/wot/IdentityManagerImpl.java [deleted file]
src/main/java/net/pterodactylus/sone/freenet/wot/OwnIdentity.java
src/main/java/net/pterodactylus/sone/freenet/wot/Trust.java [deleted file]
src/main/java/net/pterodactylus/sone/freenet/wot/WebOfTrustConnector.java [deleted file]
src/main/java/net/pterodactylus/sone/freenet/wot/WebOfTrustException.java [deleted file]
src/main/java/net/pterodactylus/sone/freenet/wot/event/IdentityAddedEvent.java [deleted file]
src/main/java/net/pterodactylus/sone/freenet/wot/event/IdentityEvent.java [deleted file]
src/main/java/net/pterodactylus/sone/freenet/wot/event/IdentityRemovedEvent.java [deleted file]
src/main/java/net/pterodactylus/sone/freenet/wot/event/IdentityUpdatedEvent.java [deleted file]
src/main/java/net/pterodactylus/sone/freenet/wot/event/OwnIdentityAddedEvent.java [deleted file]
src/main/java/net/pterodactylus/sone/freenet/wot/event/OwnIdentityEvent.java [deleted file]
src/main/java/net/pterodactylus/sone/freenet/wot/event/OwnIdentityRemovedEvent.java [deleted file]
src/main/java/net/pterodactylus/sone/main/DebugLoaders.java
src/main/java/net/pterodactylus/sone/main/DefaultLoaders.java
src/main/java/net/pterodactylus/sone/main/Loaders.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/notify/ListNotification.java [deleted file]
src/main/java/net/pterodactylus/sone/notify/ListNotificationFilter.java
src/main/java/net/pterodactylus/sone/template/AlbumAccessor.java
src/main/java/net/pterodactylus/sone/template/CollectionAccessor.java
src/main/java/net/pterodactylus/sone/template/CssClassNameFilter.java
src/main/java/net/pterodactylus/sone/template/GetPagePlugin.java
src/main/java/net/pterodactylus/sone/template/HttpRequestAccessor.java
src/main/java/net/pterodactylus/sone/template/IdentityAccessor.java
src/main/java/net/pterodactylus/sone/template/ImageAccessor.java
src/main/java/net/pterodactylus/sone/template/ImageLinkFilter.java
src/main/java/net/pterodactylus/sone/template/JavascriptFilter.java
src/main/java/net/pterodactylus/sone/template/PostAccessor.java [deleted file]
src/main/java/net/pterodactylus/sone/template/ProfileAccessor.java
src/main/java/net/pterodactylus/sone/template/ReplyAccessor.java
src/main/java/net/pterodactylus/sone/template/ReplyGroupFilter.java
src/main/java/net/pterodactylus/sone/template/RequestChangeFilter.java
src/main/java/net/pterodactylus/sone/template/SoneAccessor.java
src/main/java/net/pterodactylus/sone/template/SubstringFilter.java
src/main/java/net/pterodactylus/sone/template/TrustAccessor.java
src/main/java/net/pterodactylus/sone/template/UniqueElementFilter.java
src/main/java/net/pterodactylus/sone/template/UnknownDateFilter.java
src/main/java/net/pterodactylus/sone/text/Parser.java
src/main/java/net/pterodactylus/sone/text/ParserContext.java
src/main/java/net/pterodactylus/sone/text/PostPart.java
src/main/java/net/pterodactylus/sone/text/SoneTextParserContext.java
src/main/java/net/pterodactylus/sone/text/TextFilter.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/DefaultElementLoader.kt
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/UpdateChecker.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/core/UpdatedSoneProcessor.kt
src/main/kotlin/net/pterodactylus/sone/core/event/ConfigNotRead.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/core/event/DebugActivatedEvent.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/core/event/FirstStart.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/core/event/ImageEvent.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/core/event/ImageInsertAbortedEvent.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/core/event/ImageInsertFailedEvent.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/core/event/ImageInsertFinishedEvent.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/core/event/ImageInsertStartedEvent.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/core/event/MarkPostKnownEvent.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/core/event/MarkPostReplyKnownEvent.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/core/event/MarkSoneKnownEvent.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/core/event/MentionOfLocalSoneFoundEvent.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/core/event/MentionOfLocalSoneRemovedEvent.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/core/event/NewPostFoundEvent.kt
src/main/kotlin/net/pterodactylus/sone/core/event/NewPostReplyFoundEvent.kt
src/main/kotlin/net/pterodactylus/sone/core/event/NewSoneFoundEvent.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/core/event/PostEvent.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/core/event/PostRemovedEvent.kt
src/main/kotlin/net/pterodactylus/sone/core/event/PostReplyEvent.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/core/event/PostReplyRemovedEvent.kt
src/main/kotlin/net/pterodactylus/sone/core/event/Shutdown.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/core/event/SoneEvent.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/core/event/SoneInsertAbortedEvent.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/core/event/SoneInsertedEvent.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/core/event/SoneInsertingEvent.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/core/event/SoneLockedEvent.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/core/event/SoneLockedOnStartup.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/core/event/SoneRemovedEvent.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/core/event/SoneUnlockedEvent.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/core/event/Startup.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/core/event/StrictFilteringEvents.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/core/event/UpdateFoundEvent.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/core/event/WebOfTrustAppeared.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/core/event/WebOfTrustDisappeared.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/Fingerprintable.kt
src/main/kotlin/net/pterodactylus/sone/data/Identified.kt
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/AlbumBuilder.kt
src/main/kotlin/net/pterodactylus/sone/database/AlbumBuilderFactory.kt
src/main/kotlin/net/pterodactylus/sone/database/AlbumDatabase.kt
src/main/kotlin/net/pterodactylus/sone/database/AlbumProvider.kt
src/main/kotlin/net/pterodactylus/sone/database/AlbumStore.kt
src/main/kotlin/net/pterodactylus/sone/database/Database.kt
src/main/kotlin/net/pterodactylus/sone/database/ImageBuilder.kt
src/main/kotlin/net/pterodactylus/sone/database/ImageBuilderFactory.kt
src/main/kotlin/net/pterodactylus/sone/database/ImageDatabase.kt
src/main/kotlin/net/pterodactylus/sone/database/ImageProvider.kt
src/main/kotlin/net/pterodactylus/sone/database/ImageStore.kt
src/main/kotlin/net/pterodactylus/sone/database/PostBuilder.kt
src/main/kotlin/net/pterodactylus/sone/database/PostBuilderFactory.kt
src/main/kotlin/net/pterodactylus/sone/database/PostDatabase.kt
src/main/kotlin/net/pterodactylus/sone/database/PostProvider.kt
src/main/kotlin/net/pterodactylus/sone/database/PostReplyBuilder.kt
src/main/kotlin/net/pterodactylus/sone/database/PostReplyBuilderFactory.kt
src/main/kotlin/net/pterodactylus/sone/database/PostReplyDatabase.kt
src/main/kotlin/net/pterodactylus/sone/database/PostReplyProvider.kt
src/main/kotlin/net/pterodactylus/sone/database/PostReplyStore.kt
src/main/kotlin/net/pterodactylus/sone/database/PostStore.kt
src/main/kotlin/net/pterodactylus/sone/database/ReplyBuilder.kt
src/main/kotlin/net/pterodactylus/sone/database/SoneProvider.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/AsyncFreenetInterface.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/freenet/BaseL10nTranslation.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/freenet/FreenetClient.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/freenet/FreenetURIs.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/freenet/L10nFilter.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/freenet/Translation.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/freenet/plugin/FredPluginConnector.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/freenet/plugin/PluginConnector.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/freenet/plugin/PluginException.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/freenet/plugin/PluginRespiratorFacade.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/freenet/wot/Context.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/freenet/wot/DefaultIdentity.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/freenet/wot/DefaultOwnIdentity.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/freenet/wot/IdentityChangeDetector.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/freenet/wot/IdentityChangeEventSender.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/freenet/wot/IdentityLoader.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/freenet/wot/IdentityManager.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/freenet/wot/IdentityManagerImpl.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/freenet/wot/PluginWebOfTrustConnector.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/freenet/wot/Trust.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/freenet/wot/WebOfTrustConnector.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/freenet/wot/WebOfTrustException.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/freenet/wot/WebOfTrustPinger.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/freenet/wot/event/IdentityAddedEvent.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/freenet/wot/event/IdentityRemovedEvent.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/freenet/wot/event/IdentityUpdatedEvent.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/freenet/wot/event/OwnIdentityAddedEvent.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/freenet/wot/event/OwnIdentityRemovedEvent.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/main/FreenetModule.kt
src/main/kotlin/net/pterodactylus/sone/main/SoneModule.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/main/SoneModuleCreator.kt [deleted file]
src/main/kotlin/net/pterodactylus/sone/main/SonePlugin.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/main/TickerShutdown.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/notify/ListNotification.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/notify/Notifications.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/template/DurationFormatFilter.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/template/HistogramRenderer.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/template/PostAccessor.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/text/SoneMentionDetector.kt [new file with mode: 0644]
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/Booleans.kt
src/main/kotlin/net/pterodactylus/sone/utils/DefaultOption.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/utils/Freenet.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/utils/Functions.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/utils/Objects.kt
src/main/kotlin/net/pterodactylus/sone/utils/Renderables.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/PageToadletRegistry.kt
src/main/kotlin/net/pterodactylus/sone/web/SessionProvider.kt
src/main/kotlin/net/pterodactylus/sone/web/WebInterfaceModule.kt
src/main/kotlin/net/pterodactylus/sone/web/ajax/CreatePostAjaxPage.kt
src/main/kotlin/net/pterodactylus/sone/web/ajax/DistrustAjaxPage.kt [deleted file]
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/GetTranslationAjaxPage.kt
src/main/kotlin/net/pterodactylus/sone/web/ajax/JsonPage.kt
src/main/kotlin/net/pterodactylus/sone/web/ajax/TrustAjaxPage.kt [deleted file]
src/main/kotlin/net/pterodactylus/sone/web/ajax/UntrustAjaxPage.kt [deleted file]
src/main/kotlin/net/pterodactylus/sone/web/notification/ConfigNotReadHandler.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/notification/FirstStartHandler.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/notification/ImageInsertHandler.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/notification/LocalPostHandler.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/notification/LocalReplyHandler.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/notification/MarkPostKnownDuringFirstStartHandler.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/notification/MarkPostReplyKnownDuringFirstStartHandler.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/notification/NewRemotePostHandler.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/notification/NewSoneHandler.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/notification/NewVersionHandler.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/notification/NotificationHandler.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/notification/NotificationHandlerModule.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/notification/RemotePostReplyHandler.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/notification/SoneInsertHandler.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/notification/SoneLockedHandler.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/notification/SoneLockedOnStartupHandler.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/notification/SoneMentionedHandler.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/notification/StartupHandler.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/notification/WebOfTrustHandler.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/page/FreenetPage.kt
src/main/kotlin/net/pterodactylus/sone/web/page/FreenetRequest.kt
src/main/kotlin/net/pterodactylus/sone/web/page/FreenetTemplatePage.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/BookmarkPage.kt
src/main/kotlin/net/pterodactylus/sone/web/pages/CreateAlbumPage.kt
src/main/kotlin/net/pterodactylus/sone/web/pages/CreatePostPage.kt
src/main/kotlin/net/pterodactylus/sone/web/pages/CreateReplyPage.kt
src/main/kotlin/net/pterodactylus/sone/web/pages/CreateSonePage.kt
src/main/kotlin/net/pterodactylus/sone/web/pages/DebugPage.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/pages/DeleteAlbumPage.kt
src/main/kotlin/net/pterodactylus/sone/web/pages/DeleteImagePage.kt
src/main/kotlin/net/pterodactylus/sone/web/pages/DeletePostPage.kt
src/main/kotlin/net/pterodactylus/sone/web/pages/DeleteProfileFieldPage.kt
src/main/kotlin/net/pterodactylus/sone/web/pages/DeleteReplyPage.kt
src/main/kotlin/net/pterodactylus/sone/web/pages/DeleteSonePage.kt
src/main/kotlin/net/pterodactylus/sone/web/pages/DismissNotificationPage.kt
src/main/kotlin/net/pterodactylus/sone/web/pages/DistrustPage.kt [deleted file]
src/main/kotlin/net/pterodactylus/sone/web/pages/EditAlbumPage.kt
src/main/kotlin/net/pterodactylus/sone/web/pages/EditImagePage.kt
src/main/kotlin/net/pterodactylus/sone/web/pages/EditProfileFieldPage.kt
src/main/kotlin/net/pterodactylus/sone/web/pages/EditProfilePage.kt
src/main/kotlin/net/pterodactylus/sone/web/pages/FollowSonePage.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/LikePage.kt
src/main/kotlin/net/pterodactylus/sone/web/pages/LockSonePage.kt
src/main/kotlin/net/pterodactylus/sone/web/pages/LoginPage.kt
src/main/kotlin/net/pterodactylus/sone/web/pages/LogoutPage.kt
src/main/kotlin/net/pterodactylus/sone/web/pages/MarkAsKnownPage.kt
src/main/kotlin/net/pterodactylus/sone/web/pages/MetricsPage.kt [new file with mode: 0644]
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/kotlin/net/pterodactylus/sone/web/pages/TrustPage.kt [deleted file]
src/main/kotlin/net/pterodactylus/sone/web/pages/UnbookmarkPage.kt
src/main/kotlin/net/pterodactylus/sone/web/pages/UnfollowSonePage.kt
src/main/kotlin/net/pterodactylus/sone/web/pages/UnlikePage.kt
src/main/kotlin/net/pterodactylus/sone/web/pages/UnlockSonePage.kt
src/main/kotlin/net/pterodactylus/sone/web/pages/UntrustPage.kt [deleted file]
src/main/kotlin/net/pterodactylus/sone/web/pages/UploadImagePage.kt
src/main/kotlin/net/pterodactylus/sone/web/pages/ViewSonePage.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/static/css/sone.css
src/main/resources/static/javascript/jquery-1.4.2.js [deleted file]
src/main/resources/static/javascript/jquery-3.4.1.js [new file with mode: 0644]
src/main/resources/static/javascript/sone.js
src/main/resources/templates/editProfile.html
src/main/resources/templates/imageBrowser.html
src/main/resources/templates/include/head.html
src/main/resources/templates/include/soneMenu.html
src/main/resources/templates/include/viewPost.html
src/main/resources/templates/include/viewReply.html
src/main/resources/templates/knownSones.html
src/main/resources/templates/metrics.html [new file with mode: 0644]
src/main/resources/templates/notify/soneLockedOnStartupNotification.html [new file with mode: 0644]
src/main/resources/templates/options.html
src/test/java/net/pterodactylus/sone/core/ConfigurationSoneParserTest.java [deleted file]
src/test/java/net/pterodactylus/sone/core/CoreTest.java [deleted file]
src/test/java/net/pterodactylus/sone/core/FreenetInterfaceTest.java [deleted file]
src/test/java/net/pterodactylus/sone/core/PreferencesLoaderTest.java [deleted file]
src/test/java/net/pterodactylus/sone/core/SoneInserterTest.java [deleted file]
src/test/java/net/pterodactylus/sone/core/SoneParserTest.java [deleted file]
src/test/java/net/pterodactylus/sone/core/SoneUriTest.java [deleted file]
src/test/java/net/pterodactylus/sone/core/UpdateCheckerTest.java [deleted file]
src/test/java/net/pterodactylus/sone/core/WebOfTrustUpdaterTest.java
src/test/java/net/pterodactylus/sone/freenet/KeyTest.java [deleted file]
src/test/java/net/pterodactylus/sone/freenet/wot/DefaultIdentityTest.java [deleted file]
src/test/java/net/pterodactylus/sone/freenet/wot/DefaultOwnIdentityTest.java [deleted file]
src/test/java/net/pterodactylus/sone/freenet/wot/Identities.java [deleted file]
src/test/java/net/pterodactylus/sone/freenet/wot/IdentityChangeDetectorTest.java [deleted file]
src/test/java/net/pterodactylus/sone/freenet/wot/IdentityChangeEventSenderTest.java [deleted file]
src/test/java/net/pterodactylus/sone/freenet/wot/IdentityLoaderTest.java [deleted file]
src/test/java/net/pterodactylus/sone/freenet/wot/IdentityManagerTest.java [deleted file]
src/test/java/net/pterodactylus/sone/freenet/wot/event/IdentityEventTest.java [deleted file]
src/test/java/net/pterodactylus/sone/freenet/wot/event/OwnIdentityEventTest.java [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/notify/ListNotificationTest.java [deleted file]
src/test/java/net/pterodactylus/sone/template/FilesystemTemplateTest.java [deleted file]
src/test/java/net/pterodactylus/sone/template/PostAccessorTest.java [deleted file]
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 [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/core/CoreTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/core/DefaultElementLoaderTest.kt
src/test/kotlin/net/pterodactylus/sone/core/FreenetInterfaceTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/core/ImageInserterTest.kt
src/test/kotlin/net/pterodactylus/sone/core/PreferencesLoaderTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/core/PreferencesTest.kt
src/test/kotlin/net/pterodactylus/sone/core/SoneInserterTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/core/SoneParserTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/core/SoneUriCreatorTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/core/UpdateCheckerTest.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/ClientTest.kt [new file with mode: 0644]
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/AsyncFreenetInterfaceTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/freenet/BaseL10nTranslationTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/freenet/FreenetClientTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/freenet/FreenetURIsTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/freenet/L10nFilterTest.kt
src/test/kotlin/net/pterodactylus/sone/freenet/plugin/FredPluginConnectorTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/freenet/plugin/PluginRespiratorFacadeTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/freenet/wot/DefaultIdentityTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/freenet/wot/DefaultOwnIdentityTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/freenet/wot/Identities.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/freenet/wot/IdentityChangeDetectorTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/freenet/wot/IdentityChangeEventSenderTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/freenet/wot/IdentityLoaderTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/freenet/wot/IdentityManagerTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/freenet/wot/PluginWebOfTrustConnectorTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/freenet/wot/WebOfTrustPingerTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/main/FreenetModuleTest.kt
src/test/kotlin/net/pterodactylus/sone/main/SoneModuleCreatorTest.kt [deleted file]
src/test/kotlin/net/pterodactylus/sone/main/SoneModuleTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/main/SonePluginTest.kt
src/test/kotlin/net/pterodactylus/sone/main/TickerShutdownTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/notify/ListNotificationTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/template/DurationFormatFilterTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/template/FilesystemTemplateTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/template/HistogramRendererTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/template/ImageAccessorTest.kt
src/test/kotlin/net/pterodactylus/sone/template/LinkedElementsFilterTest.kt
src/test/kotlin/net/pterodactylus/sone/template/ParserFilterTest.kt
src/test/kotlin/net/pterodactylus/sone/template/PostAccessorTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/template/ProfileAccessorTest.kt
src/test/kotlin/net/pterodactylus/sone/template/ShortenFilterTest.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/Guice.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/Mockotlin.kt
src/test/kotlin/net/pterodactylus/sone/test/Mocks.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/test/NotParallel.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/test/TestLoaders.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/test/TestPage.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/test/TestUtils.kt
src/test/kotlin/net/pterodactylus/sone/text/FreenetLinkPartTest.kt
src/test/kotlin/net/pterodactylus/sone/text/LinkPartTest.kt
src/test/kotlin/net/pterodactylus/sone/text/SoneMentionDetectorTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/text/SonePartTest.kt
src/test/kotlin/net/pterodactylus/sone/text/SoneTextParserTest.kt
src/test/kotlin/net/pterodactylus/sone/utils/AutoCloseableBucketTest.kt [deleted file]
src/test/kotlin/net/pterodactylus/sone/utils/BooleansTest.kt
src/test/kotlin/net/pterodactylus/sone/utils/DefaultOptionTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/utils/ObjectsTest.kt
src/test/kotlin/net/pterodactylus/sone/utils/OptionalsTest.kt
src/test/kotlin/net/pterodactylus/sone/utils/RenderablesTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/AllPagesTest.kt
src/test/kotlin/net/pterodactylus/sone/web/FreenetSessionProviderTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/PageToadletRegistryTest.kt
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/DistrustAjaxPageTest.kt [deleted file]
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/ajax/TrustAjaxPageTest.kt [deleted file]
src/test/kotlin/net/pterodactylus/sone/web/ajax/UntrustAjaxPageTest.kt [deleted file]
src/test/kotlin/net/pterodactylus/sone/web/notification/ConfigNotReadHandlerTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/notification/FirstStartHandlerTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/notification/ImageInsertHandlerTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/notification/LocalPostHandlerTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/notification/LocalReplyHandlerTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/notification/MarkPostKnownDuringFirstStartHandlerTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/notification/MarkPostReplyKnownDuringFirstStartHandlerTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/notification/NewRemotePostHandlerTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/notification/NewSoneHandlerTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/notification/NewVersionHandlerTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/notification/NotificationHandlerModuleTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/notification/NotificationHandlerTester.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/notification/RemotePostReplyHandlerTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/notification/SoneInsertHandlerTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/notification/SoneLockedHandlerTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/notification/SoneLockedOnStartupHandlerTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/notification/SoneMentionedHandlerTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/notification/StartupHandlerTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/notification/Testing.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/notification/WebOfTrustHandlerTest.kt [new file with mode: 0644]
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/DebugPageTest.kt [new file with mode: 0644]
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/DeleteSonePageTest.kt
src/test/kotlin/net/pterodactylus/sone/web/pages/DismissNotificationPageTest.kt
src/test/kotlin/net/pterodactylus/sone/web/pages/DistrustPageTest.kt [deleted file]
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/EditProfileFieldPageTest.kt
src/test/kotlin/net/pterodactylus/sone/web/pages/EditProfilePageTest.kt
src/test/kotlin/net/pterodactylus/sone/web/pages/FollowSonePageTest.kt
src/test/kotlin/net/pterodactylus/sone/web/pages/ImageBrowserPageTest.kt
src/test/kotlin/net/pterodactylus/sone/web/pages/IndexPageTest.kt
src/test/kotlin/net/pterodactylus/sone/web/pages/KnownSonesPageTest.kt
src/test/kotlin/net/pterodactylus/sone/web/pages/MetricsPageTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/pages/OptionsPageTest.kt
src/test/kotlin/net/pterodactylus/sone/web/pages/SoneTemplatePageTest.kt
src/test/kotlin/net/pterodactylus/sone/web/pages/TrustPageTest.kt [deleted file]
src/test/kotlin/net/pterodactylus/sone/web/pages/UntrustPageTest.kt [deleted file]
src/test/kotlin/net/pterodactylus/sone/web/pages/UploadImagePageTest.kt
src/test/kotlin/net/pterodactylus/sone/web/pages/WebPageTest.kt
src/test/resources/net/pterodactylus/sone/core/sone-parser-with-invalid-recipient.xml
src/test/resources/net/pterodactylus/sone/core/sone-parser-with-recipient.xml
src/test/resources/net/pterodactylus/sone/core/sone-parser-with-valid-post-reply-time.xml
src/test/resources/net/pterodactylus/sone/core/sone-parser-with-valid-post-time.xml

index 5edd26d..2c43d38 100644 (file)
@@ -9,7 +9,7 @@ tasks:
       ./gradlew clean
   - build: |
       cd sone
-      ./gradlew build
+      ./gradlew -x findbugsTest build
   - test: |
       cd sone
       ./gradlew test
index fce4db9..abaaab1 100644 (file)
@@ -3,3 +3,5 @@
 
 /.gradle/
 /build/
+
+sone.properties
index 0beb42a..db5af2b 100644 (file)
@@ -1,18 +1,14 @@
-group = 'net.pterodactylus'
-version = '80'
+import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
 
-buildscript {
-    ext.kotlinVersion = '1.2.71'
-    repositories {
-        mavenCentral()
-    }
-    dependencies {
-        classpath group: 'info.solidsoft.gradle.pitest', name: 'gradle-pitest-plugin', version: '1.4.0'
-        classpath group: 'org.jetbrains.kotlin', name: 'kotlin-gradle-plugin', version: kotlinVersion
-        classpath group: 'org.jetbrains.kotlin', name: 'kotlin-noarg', version: kotlinVersion
-    }
+plugins {
+    id 'org.jetbrains.kotlin.jvm' version '1.3.70'
+    id 'org.jetbrains.kotlin.plugin.noarg' version '1.3.70'
+    id 'info.solidsoft.pitest' version '1.4.5'
 }
 
+group = 'net.pterodactylus'
+version = '82'
+
 repositories {
      mavenCentral()
      maven { url "https://maven.pterodactylus.net/" }
@@ -20,14 +16,18 @@ repositories {
 
 apply plugin: 'java'
 
-sourceCompatibility = 1.7
-targetCompatibility = 1.7
+sourceCompatibility = 1.8
+targetCompatibility = 1.8
 
 tasks.withType(JavaCompile) {
        options.encoding = 'UTF-8'
 }
 
-apply plugin: 'kotlin'
+tasks.withType(KotlinCompile) {
+    kotlinOptions {
+        jvmTarget = "1.8"
+    }
+}
 
 configurations {
     provided {
@@ -37,35 +37,55 @@ 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'
     provided group: 'org.bouncycastle', name: 'bcprov-jdk15on', version: '1.54'
 
-    compile group: 'org.jetbrains.kotlin', name: 'kotlin-stdlib'
-    compile group: 'net.pterodactylus', name: 'utils', version: '0.12.4'
+    compile group: 'org.jetbrains.kotlin', name: 'kotlin-stdlib-jdk8'
+    compile group: 'org.jetbrains.kotlinx', name: 'kotlinx-coroutines-core', version: '1.3.0-RC'
+
+    compile group: 'net.pterodactylus', name: 'utils', version: '0.13.1'
     compile group: 'com.google.inject', name: 'guice', version: '4.2.2'
-    compile group: 'com.google.guava', name: 'guava', version: '27.0.1-android'
+    compile group: 'com.google.guava', name: 'guava', version: '27.0.1-jre'
     compile group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.9.1'
     compile group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-yaml', version: '2.9.1'
     compile group: 'com.google.code.findbugs', name: 'jsr305', version: '3.0.2'
     compile group: 'org.jsoup', name: 'jsoup', version: '1.10.2'
+    compile group: 'io.dropwizard.metrics', name: 'metrics-core', version: '4.1.0'
+    compile group: 'javax.activation', name: 'javax.activation-api', version: '1.2.0'
 
     testCompile group: 'org.jetbrains.kotlin', name: 'kotlin-test-junit'
     testCompile group: 'junit', name: 'junit', version: '4.11'
-    testCompile group: 'org.mockito', name: 'mockito-core', version: '2.10.0'
+    testCompile group: 'org.mockito', name: 'mockito-core', version: '2.28.2'
     testCompile group: 'org.hamcrest', name: 'hamcrest-all', version: '1.3'
 }
 
 apply from: 'version.gradle'
 
-test {
+task parallelTest(type: Test) {
     maxParallelForks = Runtime.runtime.availableProcessors().intdiv(2) ?: 1
+    useJUnit {
+        excludeCategories 'net.pterodactylus.sone.test.NotParallel'
+    }
+}
+
+task notParallelTest(type: Test) {
+    maxParallelForks = 1
+    useJUnit {
+        includeCategories 'net.pterodactylus.sone.test.NotParallel'
+    }
+    dependsOn parallelTest
+}
+
+test {
+    exclude '**'
+    dependsOn parallelTest, notParallelTest
 }
 
 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')
@@ -86,14 +106,13 @@ javadoc {
 apply plugin: 'jacoco'
 
 jacoco {
-    toolVersion = '0.7.9'
+    toolVersion = '0.8.4'
 }
 
 jacocoTestReport.dependsOn test
 
-apply plugin: 'info.solidsoft.pitest'
-
 pitest {
+    pitestVersion = '1.4.10'
     outputFormats = ['HTML', 'XML']
     timestampedReports = false
     timeoutFactor = 3.0
@@ -125,8 +144,6 @@ task countLines {
     dependsOn tasks.countLinesTest
 }
 
-apply plugin: 'kotlin-noarg'
-
 noArg {
     annotation('net.pterodactylus.sone.main.NoArg')
 }
index c19a936..1cdded7 100644 (file)
@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
 distributionPath=wrapper/dists
 zipStoreBase=GRADLE_USER_HOME
 zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-4.5-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.1-all.zip
index b0df7a9..da1b2f8 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - Core.java - Copyright © 2010–2019 David Roden
+ * Sone - Core.java - Copyright © 2010–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -24,6 +24,7 @@ import static com.google.common.primitives.Longs.tryParse;
 import static java.lang.String.format;
 import static java.util.logging.Level.WARNING;
 import static java.util.logging.Logger.getLogger;
+import static net.pterodactylus.sone.data.AlbumKt.getAllImages;
 
 import java.util.ArrayList;
 import java.util.Collection;
@@ -37,30 +38,20 @@ import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.*;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 
+import com.codahale.metrics.*;
 import net.pterodactylus.sone.core.ConfigurationSoneParser.InvalidAlbumFound;
 import net.pterodactylus.sone.core.ConfigurationSoneParser.InvalidImageFound;
 import net.pterodactylus.sone.core.ConfigurationSoneParser.InvalidParentAlbumFound;
 import net.pterodactylus.sone.core.ConfigurationSoneParser.InvalidPostFound;
 import net.pterodactylus.sone.core.ConfigurationSoneParser.InvalidPostReplyFound;
-import net.pterodactylus.sone.core.event.ImageInsertFinishedEvent;
-import net.pterodactylus.sone.core.event.InsertionDelayChangedEvent;
-import net.pterodactylus.sone.core.event.MarkPostKnownEvent;
-import net.pterodactylus.sone.core.event.MarkPostReplyKnownEvent;
-import net.pterodactylus.sone.core.event.MarkSoneKnownEvent;
-import net.pterodactylus.sone.core.event.NewPostFoundEvent;
-import net.pterodactylus.sone.core.event.NewPostReplyFoundEvent;
-import net.pterodactylus.sone.core.event.NewSoneFoundEvent;
-import net.pterodactylus.sone.core.event.PostRemovedEvent;
-import net.pterodactylus.sone.core.event.PostReplyRemovedEvent;
-import net.pterodactylus.sone.core.event.SoneLockedEvent;
-import net.pterodactylus.sone.core.event.SoneRemovedEvent;
-import net.pterodactylus.sone.core.event.SoneUnlockedEvent;
+import net.pterodactylus.sone.core.event.*;
 import net.pterodactylus.sone.data.Album;
 import net.pterodactylus.sone.data.Client;
 import net.pterodactylus.sone.data.Image;
@@ -71,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;
@@ -98,8 +90,7 @@ import net.pterodactylus.util.service.AbstractService;
 import net.pterodactylus.util.thread.NamedThreadFactory;
 
 import com.google.common.annotations.VisibleForTesting;
-import com.google.common.base.Optional;
-import com.google.common.collect.FluentIterable;
+import com.google.common.base.Stopwatch;
 import com.google.common.collect.HashMultimap;
 import com.google.common.collect.Multimap;
 import com.google.common.collect.Multimaps;
@@ -121,6 +112,8 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
        /** The start time. */
        private final long startupTime = System.currentTimeMillis();
 
+       private final AtomicBoolean debug = new AtomicBoolean(false);
+
        /** The preferences. */
        private final Preferences preferences;
 
@@ -184,24 +177,13 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
        /** The time the configuration was last touched. */
        private volatile long lastConfigurationUpdate;
 
-       /**
-        * 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 MetricRegistry metricRegistry;
+       private final Histogram configurationSaveTimeHistogram;
+
+       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) {
+       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;
@@ -212,7 +194,10 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
                this.webOfTrustUpdater = webOfTrustUpdater;
                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)));
        }
 
        //
@@ -228,6 +213,16 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
                return startupTime;
        }
 
+       @Nonnull
+       public boolean getDebug() {
+               return debug.get();
+       }
+
+       public void setDebug() {
+               debug.set(true);
+               eventBus.post(new DebugActivatedEvent());
+       }
+
        /**
         * Returns the options used by the core.
         *
@@ -618,7 +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, ownIdentity.getId());
+               SoneInserter soneInserter = new SoneInserter(this, eventBus, freenetInterface, metricRegistry, soneUriCreator, ownIdentity.getId());
                soneInserter.insertionDelayChanged(new InsertionDelayChangedEvent(preferences.getInsertionDelay()));
                eventBus.register(soneInserter);
                synchronized (soneInserters) {
@@ -627,6 +622,11 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
                loadSone(sone);
                database.storeSone(sone);
                sone.setStatus(SoneStatus.idle);
+               if (sone.getPosts().isEmpty() && sone.getReplies().isEmpty() && getAllImages(sone.getRootAlbum()).isEmpty()) {
+                       // dirty hack
+                       lockSone(sone);
+                       eventBus.post(new SoneLockedOnStartup(sone));
+               }
                soneInserter.start();
                return sone;
        }
@@ -738,75 +738,6 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
        }
 
        /**
-        * Sets the trust value of the given origin Sone for the target Sone.
-        *
-        * @param origin
-        *            The origin Sone
-        * @param target
-        *            The target Sone
-        * @param trustValue
-        *            The trust value (from {@code -100} to {@code 100})
-        */
-       public void setTrust(Sone origin, Sone target, int trustValue) {
-               checkNotNull(origin, "origin must not be null");
-               checkArgument(origin.getIdentity() instanceof OwnIdentity, "origin must be a local Sone");
-               checkNotNull(target, "target must not be null");
-               checkArgument((trustValue >= -100) && (trustValue <= 100), "trustValue must be within [-100, 100]");
-               webOfTrustUpdater.setTrust((OwnIdentity) origin.getIdentity(), target.getIdentity(), trustValue, preferences.getTrustComment());
-       }
-
-       /**
-        * Removes any trust assignment for the given target Sone.
-        *
-        * @param origin
-        *            The trust origin
-        * @param target
-        *            The trust target
-        */
-       public void removeTrust(Sone origin, Sone target) {
-               checkNotNull(origin, "origin must not be null");
-               checkNotNull(target, "target must not be null");
-               checkArgument(origin.getIdentity() instanceof OwnIdentity, "origin must be a local Sone");
-               webOfTrustUpdater.setTrust((OwnIdentity) origin.getIdentity(), target.getIdentity(), null, null);
-       }
-
-       /**
-        * Assigns the configured positive trust value for the given target.
-        *
-        * @param origin
-        *            The trust origin
-        * @param target
-        *            The trust target
-        */
-       public void trustSone(Sone origin, Sone target) {
-               setTrust(origin, target, preferences.getPositiveTrust());
-       }
-
-       /**
-        * Assigns the configured negative trust value for the given target.
-        *
-        * @param origin
-        *            The trust origin
-        * @param target
-        *            The trust target
-        */
-       public void distrustSone(Sone origin, Sone target) {
-               setTrust(origin, target, preferences.getNegativeTrust());
-       }
-
-       /**
-        * Removes the trust assignment for the given target.
-        *
-        * @param origin
-        *            The trust origin
-        * @param target
-        *            The trust target
-        */
-       public void untrustSone(Sone origin, Sone target) {
-               removeTrust(origin, target);
-       }
-
-       /**
         * Updates the stored Sone with the given Sone.
         *
         * @param sone
@@ -866,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));
                        }
@@ -1045,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));
@@ -1063,7 +994,7 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
         *            The text of the post
         * @return The created post
         */
-       public Post createPost(Sone sone, Optional<Sone> recipient, String text) {
+       public Post createPost(Sone sone, @Nullable Sone recipient, String text) {
                checkNotNull(text, "text must not be null");
                checkArgument(text.trim().length() > 0, "text must not be empty");
                if (!sone.isLocal()) {
@@ -1072,8 +1003,8 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
                }
                PostBuilder postBuilder = database.newPostBuilder();
                postBuilder.from(sone.getId()).randomId().currentTime().withText(text.trim());
-               if (recipient.isPresent()) {
-                       postBuilder.to(recipient.get().getId());
+               if (recipient != null) {
+                       postBuilder.to(recipient.getId());
                }
                final Post post = postBuilder.build();
                database.storePost(post);
@@ -1187,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();
@@ -1359,7 +1290,8 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
                synchronized (soneInserters) {
                        for (Entry<Sone, SoneInserter> soneInserter : soneInserters.entrySet()) {
                                soneInserter.getValue().stop();
-                               saveSone(soneInserter.getKey());
+                               Sone latestSone = getLocalSone(soneInserter.getKey().getId());
+                               saveSone(latestSone);
                        }
                }
                synchronized (soneRescuers) {
@@ -1460,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) {
@@ -1501,8 +1433,6 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
                        configuration.getStringValue(sonePrefix + "/Options/ShowCustomAvatars").setValue(sone.getOptions().getShowCustomAvatars().name());
                        configuration.getStringValue(sonePrefix + "/Options/LoadLinkedImages").setValue(sone.getOptions().getLoadLinkedImages().name());
 
-                       configuration.save();
-
                        webOfTrustUpdater.setProperty((OwnIdentity) sone.getIdentity(), "Sone.LatestEdition", String.valueOf(sone.getLatestEdition()));
 
                        logger.log(Level.INFO, String.format("Sone %s saved.", sone));
@@ -1540,7 +1470,9 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
                        database.save();
 
                        /* now save it. */
+                       Stopwatch stopwatch = Stopwatch.createStarted();
                        configuration.save();
+                       configurationSaveTimeHistogram.update(stopwatch.elapsed(TimeUnit.MICROSECONDS));
 
                } catch (ConfigurationException ce1) {
                        logger.log(Level.SEVERE, "Could not store configuration!", ce1);
@@ -1580,7 +1512,7 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
         */
        @Subscribe
        public void ownIdentityAdded(OwnIdentityAddedEvent ownIdentityAddedEvent) {
-               OwnIdentity ownIdentity = ownIdentityAddedEvent.ownIdentity();
+               OwnIdentity ownIdentity = ownIdentityAddedEvent.getOwnIdentity();
                logger.log(Level.FINEST, String.format("Adding OwnIdentity: %s", ownIdentity));
                if (ownIdentity.hasContext("Sone")) {
                        addLocalSone(ownIdentity);
@@ -1595,7 +1527,7 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
         */
        @Subscribe
        public void ownIdentityRemoved(OwnIdentityRemovedEvent ownIdentityRemovedEvent) {
-               OwnIdentity ownIdentity = ownIdentityRemovedEvent.ownIdentity();
+               OwnIdentity ownIdentity = ownIdentityRemovedEvent.getOwnIdentity();
                logger.log(Level.FINEST, String.format("Removing OwnIdentity: %s", ownIdentity));
                trustedIdentities.removeAll(ownIdentity);
        }
@@ -1608,9 +1540,9 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
         */
        @Subscribe
        public void identityAdded(IdentityAddedEvent identityAddedEvent) {
-               Identity identity = identityAddedEvent.identity();
+               Identity identity = identityAddedEvent.getIdentity();
                logger.log(Level.FINEST, String.format("Adding Identity: %s", identity));
-               trustedIdentities.put(identityAddedEvent.ownIdentity(), identity);
+               trustedIdentities.put(identityAddedEvent.getOwnIdentity(), identity);
                addRemoteSone(identity);
        }
 
@@ -1622,7 +1554,7 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
         */
        @Subscribe
        public void identityUpdated(IdentityUpdatedEvent identityUpdatedEvent) {
-               Identity identity = identityUpdatedEvent.identity();
+               Identity identity = identityUpdatedEvent.getIdentity();
                final Sone sone = getRemoteSone(identity.getId());
                if (sone.isLocal()) {
                        return;
@@ -1646,8 +1578,8 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
         */
        @Subscribe
        public void identityRemoved(IdentityRemovedEvent identityRemovedEvent) {
-               OwnIdentity ownIdentity = identityRemovedEvent.ownIdentity();
-               Identity identity = identityRemovedEvent.identity();
+               OwnIdentity ownIdentity = identityRemovedEvent.getOwnIdentity();
+               Identity identity = identityRemovedEvent.getIdentity();
                trustedIdentities.remove(ownIdentity, identity);
                for (Entry<OwnIdentity, Collection<Identity>> trustedIdentity : trustedIdentities.asMap().entrySet()) {
                        if (trustedIdentity.getKey().equals(ownIdentity)) {
@@ -1680,9 +1612,9 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
         */
        @Subscribe
        public void imageInsertFinished(ImageInsertFinishedEvent imageInsertFinishedEvent) {
-               logger.log(Level.WARNING, String.format("Image insert finished for %s: %s", imageInsertFinishedEvent.image(), imageInsertFinishedEvent.resultingUri()));
-               imageInsertFinishedEvent.image().modify().setKey(imageInsertFinishedEvent.resultingUri().toString()).update();
-               deleteTemporaryImage(imageInsertFinishedEvent.image().getId());
+               logger.log(Level.WARNING, String.format("Image insert finished for %s: %s", imageInsertFinishedEvent.getImage(), imageInsertFinishedEvent.getResultingUri()));
+               imageInsertFinishedEvent.getImage().modify().setKey(imageInsertFinishedEvent.getResultingUri().toString()).update();
+               deleteTemporaryImage(imageInsertFinishedEvent.getImage().getId());
                touchConfiguration();
        }
 
index 1ed2024..35ac6a1 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - FreenetInterface.java - Copyright © 2010–2019 David Roden
+ * Sone - FreenetInterface.java - Copyright © 2010–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -21,7 +21,6 @@ import static freenet.keys.USK.create;
 import static java.lang.String.format;
 import static java.util.logging.Level.WARNING;
 import static java.util.logging.Logger.getLogger;
-import static net.pterodactylus.sone.freenet.Key.routingKey;
 
 import java.io.IOException;
 import java.net.MalformedURLException;
@@ -75,6 +74,7 @@ import freenet.support.api.Bucket;
 import freenet.support.api.RandomAccessBucket;
 import freenet.support.io.ArrayBucket;
 import freenet.support.io.ResumeFailedException;
+import net.pterodactylus.sone.freenet.*;
 
 /**
  * Contains all necessary functionality for interacting with the Freenet node.
@@ -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);
        }
 
@@ -255,7 +250,7 @@ public class FreenetInterface {
        public void registerActiveUsk(FreenetURI requestUri,
                        USKCallback uskCallback) {
                try {
-                       soneUskCallbacks.put(routingKey(requestUri), uskCallback);
+                       soneUskCallbacks.put(FreenetURIsKt.getRoutingKeyString(requestUri), uskCallback);
                        node.clientCore.uskManager.subscribe(create(requestUri),
                                        uskCallback, true, requestClient);
                } catch (MalformedURLException mue1) {
@@ -267,7 +262,7 @@ public class FreenetInterface {
        public void registerPassiveUsk(FreenetURI requestUri,
                        USKCallback uskCallback) {
                try {
-                       soneUskCallbacks.put(routingKey(requestUri), uskCallback);
+                       soneUskCallbacks.put(FreenetURIsKt.getRoutingKeyString(requestUri), uskCallback);
                        node.clientCore
                                        .uskManager
                                        .subscribe(create(requestUri), uskCallback, false, requestClient);
@@ -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);
                }
        }
 
@@ -327,7 +322,7 @@ public class FreenetInterface {
                };
                try {
                        node.clientCore.uskManager.subscribe(USK.create(uri), uskCallback, true, requestClient);
-                       uriUskCallbacks.put(uri, uskCallback);
+                       uriUskCallbacks.put(USK.create(uri).clearCopy().getURI(), uskCallback);
                } catch (MalformedURLException mue1) {
                        logger.log(Level.WARNING, String.format("Could not subscribe to USK: %s", uri), mue1);
                }
@@ -340,12 +335,12 @@ public class FreenetInterface {
         *            The URI to unregister the USK watcher for
         */
        public void unregisterUsk(FreenetURI uri) {
-               USKCallback uskCallback = uriUskCallbacks.remove(uri);
-               if (uskCallback == null) {
-                       logger.log(Level.INFO, String.format("Could not unregister unknown USK: %s", uri));
-                       return;
-               }
                try {
+                       USKCallback uskCallback = uriUskCallbacks.remove(USK.create(uri).clearCopy().getURI());
+                       if (uskCallback == null) {
+                               logger.log(Level.INFO, String.format("Could not unregister unknown USK: %s", uri));
+                               return;
+                       }
                        node.clientCore.uskManager.unsubscribe(USK.create(uri), uskCallback);
                } catch (MalformedURLException mue1) {
                        logger.log(Level.INFO, String.format("Could not unregister invalid USK: %s", uri), mue1);
index fc76ca9..76519f4 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - ImageInserter.java - Copyright © 2011–2019 David Roden
+ * Sone - ImageInserter.java - Copyright © 2011–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
index af88dd8..398374a 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - Options.java - Copyright © 2010–2019 David Roden
+ * Sone - Options.java - Copyright © 2010–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -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.java b/src/main/java/net/pterodactylus/sone/core/PreferencesLoader.java
deleted file mode 100644 (file)
index a730983..0000000
+++ /dev/null
@@ -1,103 +0,0 @@
-package net.pterodactylus.sone.core;
-
-import net.pterodactylus.sone.fcp.FcpInterface.FullAccessRequired;
-import net.pterodactylus.util.config.Configuration;
-import net.pterodactylus.util.config.ConfigurationException;
-
-/**
- * Loads preferences stored in a {@link Configuration} into a {@link
- * Preferences} object.
- */
-public class PreferencesLoader {
-
-       private final Preferences preferences;
-
-       public PreferencesLoader(Preferences preferences) {
-               this.preferences = preferences;
-       }
-
-       public void loadFrom(Configuration configuration) {
-               loadInsertionDelay(configuration);
-               loadPostsPerPage(configuration);
-               loadImagesPerPage(configuration);
-               loadCharactersPerPost(configuration);
-               loadPostCutOffLength(configuration);
-               loadRequireFullAccess(configuration);
-               loadPositiveTrust(configuration);
-               loadNegativeTrust(configuration);
-               loadTrustComment(configuration);
-               loadFcpInterfaceActive(configuration);
-               loadFcpFullAccessRequired(configuration);
-       }
-
-       private void loadInsertionDelay(Configuration configuration) {
-               preferences.setNewInsertionDelay(configuration.getIntValue(
-                               "Option/InsertionDelay").getValue(null));
-       }
-
-       private void loadPostsPerPage(Configuration configuration) {
-               preferences.setNewPostsPerPage(
-                               configuration.getIntValue("Option/PostsPerPage")
-                                               .getValue(null));
-       }
-
-       private void loadImagesPerPage(Configuration configuration) {
-               preferences.setNewImagesPerPage(
-                               configuration.getIntValue("Option/ImagesPerPage")
-                                               .getValue(null));
-       }
-
-       private void loadCharactersPerPost(Configuration configuration) {
-               preferences.setNewCharactersPerPost(
-                               configuration.getIntValue("Option/CharactersPerPost")
-                                               .getValue(null));
-       }
-
-       private void loadPostCutOffLength(Configuration configuration) {
-               try {
-                       preferences.setNewPostCutOffLength(
-                                       configuration.getIntValue("Option/PostCutOffLength")
-                                                       .getValue(null));
-               } catch (IllegalArgumentException iae1) {
-                       /* previous versions allowed -1, ignore and use default. */
-               }
-       }
-
-       private void loadRequireFullAccess(Configuration configuration) {
-               preferences.setNewRequireFullAccess(
-                               configuration.getBooleanValue("Option/RequireFullAccess")
-                                               .getValue(null));
-       }
-
-       private void loadPositiveTrust(Configuration configuration) {
-               preferences.setNewPositiveTrust(
-                               configuration.getIntValue("Option/PositiveTrust")
-                                               .getValue(null));
-       }
-
-       private void loadNegativeTrust(Configuration configuration) {
-               preferences.setNewNegativeTrust(
-                               configuration.getIntValue("Option/NegativeTrust")
-                                               .getValue(null));
-       }
-
-       private void loadTrustComment(Configuration configuration) {
-               preferences.setNewTrustComment(
-                               configuration.getStringValue("Option/TrustComment")
-                                               .getValue(null));
-       }
-
-       private void loadFcpInterfaceActive(Configuration configuration) {
-               preferences.setNewFcpInterfaceActive(configuration.getBooleanValue(
-                               "Option/ActivateFcpInterface").getValue(null));
-       }
-
-       private void loadFcpFullAccessRequired(Configuration configuration) {
-               Integer fullAccessRequiredInteger = configuration
-                               .getIntValue("Option/FcpFullAccessRequired").getValue(null);
-               preferences.setNewFcpFullAccessRequired(
-                               (fullAccessRequiredInteger == null) ? null :
-                                               FullAccessRequired.values()[fullAccessRequiredInteger]);
-       }
-
-}
index ba1c632..43a87fb 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - SoneDownloaderImpl.java - Copyright © 2010–2019 David Roden
+ * Sone - SoneDownloaderImpl.java - Copyright © 2010–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -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;
@@ -183,23 +182,19 @@ public class SoneDownloaderImpl extends AbstractService implements SoneDownloade
         * @return The parsed Sone, or {@code null} if the Sone could not be parsed
         */
        private Sone parseSone(Sone originalSone, FetchResult fetchResult, FreenetURI requestUri) {
-               logger.log(Level.FINEST, String.format("Parsing FetchResult (%d bytes, %s) for %s…", fetchResult.size(), fetchResult.getMimeType(), originalSone));
+               logger.finest(() -> format("Parsing FetchResult (%d bytes, %s) for %s…", fetchResult.size(), fetchResult.getMimeType(), originalSone));
                Bucket soneBucket = fetchResult.asBucket();
-               InputStream soneInputStream = null;
-               try {
-                       soneInputStream = soneBucket.getInputStream();
-                       Sone parsedSone = soneParser.parseSone(originalSone,
-                                       soneInputStream);
+               try (InputStream soneInputStream = soneBucket.getInputStream()) {
+                       Sone parsedSone = soneParser.parseSone(originalSone, soneInputStream);
                        if (parsedSone != null) {
-                               logger.log(Level.FINER, "Sone %s was successfully parsed.", parsedSone);
+                               logger.finer(() -> format("Sone %s was successfully parsed.", parsedSone));
                                parsedSone.setLatestEdition(requestUri.getEdition());
                        }
                        return parsedSone;
                } catch (Exception e1) {
-                       logger.log(Level.WARNING, String.format("Could not parse Sone from %s!", requestUri), e1);
+                       logger.log(Level.WARNING, e1, () -> format("Could not parse Sone from %s!", requestUri));
                } finally {
-                       close(soneInputStream);
-                       close(soneBucket);
+                       soneBucket.free();
                }
                return null;
        }
index e460ee7..152f629 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - SoneException.java - Copyright © 2010–2019 David Roden
+ * Sone - SoneException.java - Copyright © 2010–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
index 8a0ff53..4aa71c1 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - SoneInsertException.java - Copyright © 2011–2019 David Roden
+ * Sone - SoneInsertException.java - Copyright © 2011–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
index 79a6259..fcbe1b5 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - SoneInserter.java - Copyright © 2010–2019 David Roden
+ * Sone - SoneInserter.java - Copyright © 2010–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -19,13 +19,13 @@ package net.pterodactylus.sone.core;
 
 import static java.lang.String.format;
 import static java.lang.System.currentTimeMillis;
+import static java.util.concurrent.TimeUnit.*;
 import static java.util.logging.Logger.getLogger;
-import static net.pterodactylus.sone.data.Album.NOT_EMPTY;
+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;
@@ -35,18 +35,18 @@ import java.util.concurrent.atomic.AtomicInteger;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 
+import com.codahale.metrics.*;
+import com.google.common.base.*;
 import net.pterodactylus.sone.core.SoneModificationDetector.LockableFingerprintProvider;
 import net.pterodactylus.sone.core.event.InsertionDelayChangedEvent;
 import net.pterodactylus.sone.core.event.SoneInsertAbortedEvent;
 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;
@@ -58,8 +58,6 @@ import net.pterodactylus.util.template.TemplateParser;
 import net.pterodactylus.util.template.XmlFilter;
 
 import com.google.common.annotations.VisibleForTesting;
-import com.google.common.base.Charsets;
-import com.google.common.collect.FluentIterable;
 import com.google.common.collect.Ordering;
 import com.google.common.eventbus.EventBus;
 import com.google.common.eventbus.Subscribe;
@@ -103,8 +101,11 @@ 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;
+       private final Meter soneInsertErrorMeter;
 
        /**
         * Creates a new Sone inserter.
@@ -118,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, final String soneId) {
-               this(core, eventBus, freenetInterface, 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);
@@ -141,11 +142,14 @@ public class SoneInserter extends AbstractService {
        }
 
        @VisibleForTesting
-       SoneInserter(Core core, EventBus eventBus, FreenetInterface freenetInterface, String soneId, SoneModificationDetector soneModificationDetector, long delay) {
+       SoneInserter(Core core, EventBus eventBus, FreenetInterface freenetInterface, MetricRegistry metricRegistry, 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;
@@ -229,8 +233,11 @@ public class SoneInserter extends AbstractService {
                                                sone.setStatus(SoneStatus.inserting);
                                                long insertTime = currentTimeMillis();
                                                eventBus.post(new SoneInsertingEvent(sone));
-                                               FreenetURI finalUri = freenetInterface.insertDirectory(sone.getInsertUri(), insertInformation.generateManifestEntries(), "index.html");
-                                               eventBus.post(new SoneInsertedEvent(sone, currentTimeMillis() - insertTime, insertInformation.getFingerprint()));
+                                               Stopwatch stopwatch = Stopwatch.createStarted();
+                                               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()));
                                                /* at this point we might already be stopped. */
                                                if (shouldStop()) {
                                                        /* if so, bail out, don’t change anything. */
@@ -242,6 +249,7 @@ public class SoneInserter extends AbstractService {
                                                success = true;
                                                logger.log(Level.INFO, String.format("Inserted Sone “%s” at %s.", sone.getName(), finalUri));
                                        } catch (SoneException se1) {
+                                               soneInsertErrorMeter.mark();
                                                eventBus.post(new SoneInsertAbortedEvent(sone, se1));
                                                logger.log(Level.WARNING, String.format("Could not insert Sone “%s”!", sone.getName()), se1);
                                        } finally {
@@ -299,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);
                }
 
@@ -366,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();
@@ -386,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 92fa3dc..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;
 
 /**
@@ -24,7 +21,7 @@ class SoneModificationDetector {
        private final Ticker ticker;
        private final LockableFingerprintProvider lockableFingerprintProvider;
        private final AtomicInteger insertionDelay;
-       private Optional<Long> lastModificationTime;
+       private Long lastModificationTime;
        private String lastInsertFingerprint;
        private String lastCheckFingerprint;
 
@@ -42,18 +39,18 @@ class SoneModificationDetector {
 
        public boolean isEligibleForInsert() {
                if (lockableFingerprintProvider.isLocked()) {
-                       lastModificationTime = absent();
+                       lastModificationTime = null;
                        lastCheckFingerprint = "";
                        return false;
                }
                String fingerprint = lockableFingerprintProvider.getFingerprint();
                if (fingerprint.equals(lastInsertFingerprint)) {
-                       lastModificationTime = absent();
+                       lastModificationTime = null;
                        lastCheckFingerprint = fingerprint;
                        return false;
                }
                if (!Objects.equal(lastCheckFingerprint, fingerprint)) {
-                       lastModificationTime = of(ticker.read());
+                       lastModificationTime = ticker.read();
                        lastCheckFingerprint = fingerprint;
                        return false;
                }
@@ -67,11 +64,11 @@ class SoneModificationDetector {
        public void setFingerprint(String fingerprint) {
                lastInsertFingerprint = fingerprint;
                lastCheckFingerprint = lastInsertFingerprint;
-               lastModificationTime = absent();
+               lastModificationTime = null;
        }
 
        private boolean insertionDelayHasPassed() {
-               return NANOSECONDS.toSeconds(ticker.read() - lastModificationTime.get()) >= insertionDelay.get();
+               return NANOSECONDS.toSeconds(ticker.read() - lastModificationTime) >= insertionDelay.get();
        }
 
        public boolean isModified() {
index 1f30565..e1fd9ff 100644 (file)
@@ -1,38 +1,24 @@
 package net.pterodactylus.sone.core;
 
-import static java.util.logging.Logger.getLogger;
-import static net.pterodactylus.sone.utils.NumberParsers.parseInt;
-import static net.pterodactylus.sone.utils.NumberParsers.parseLong;
+import static java.util.concurrent.TimeUnit.*;
+import static java.util.logging.Logger.*;
+import static net.pterodactylus.sone.utils.NumberParsers.*;
 
-import java.io.InputStream;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.logging.Level;
-import java.util.logging.Logger;
+import java.io.*;
+import java.util.*;
+import java.util.logging.*;
 
-import javax.inject.Inject;
+import javax.annotation.*;
+import javax.inject.*;
 
-import net.pterodactylus.sone.data.Album;
-import net.pterodactylus.sone.data.Client;
-import net.pterodactylus.sone.data.Image;
-import net.pterodactylus.sone.data.Post;
-import net.pterodactylus.sone.data.PostReply;
-import net.pterodactylus.sone.data.Profile;
-import net.pterodactylus.sone.data.Profile.DuplicateField;
-import net.pterodactylus.sone.data.Profile.EmptyFieldName;
-import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.sone.database.Database;
-import net.pterodactylus.sone.database.PostBuilder;
-import net.pterodactylus.sone.database.PostReplyBuilder;
-import net.pterodactylus.sone.database.SoneBuilder;
-import net.pterodactylus.util.xml.SimpleXML;
-import net.pterodactylus.util.xml.XML;
+import net.pterodactylus.sone.data.*;
+import net.pterodactylus.sone.data.Profile.*;
+import net.pterodactylus.sone.database.*;
+import net.pterodactylus.util.xml.*;
 
-import org.w3c.dom.Document;
+import com.codahale.metrics.*;
+import com.google.common.base.*;
+import org.w3c.dom.*;
 
 /**
  * Parses a {@link Sone} from an XML {@link InputStream}.
@@ -42,15 +28,19 @@ public class SoneParser {
        private static final Logger logger = getLogger(SoneParser.class.getName());
        private static final int MAX_PROTOCOL_VERSION = 0;
        private final Database database;
+       private final Histogram soneParsingDurationHistogram;
 
        @Inject
-       public SoneParser(Database database) {
+       public SoneParser(Database database, MetricRegistry metricRegistry) {
                this.database = database;
+               this.soneParsingDurationHistogram = metricRegistry.histogram("sone.parse.duration", () -> new Histogram(new ExponentiallyDecayingReservoir(3000, 0)));
        }
 
+       @Nullable
        public Sone parseSone(Sone originalSone, InputStream soneInputStream) throws SoneException {
                /* TODO - impose a size limit? */
 
+               Stopwatch stopwatch = Stopwatch.createStarted();
                Document document;
                /* XML parsing is not thread-safe. */
                synchronized (this) {
@@ -257,6 +247,7 @@ public class SoneParser {
                SimpleXML albumsXml = soneXml.getNode("albums");
                Map<String, Image> allImages = new HashMap<>();
                List<Album> topLevelAlbums = new ArrayList<>();
+               Map<String, Album> allAlbums = new HashMap<>();
                if (albumsXml != null) {
                        for (SimpleXML albumXml : albumsXml.getNodes("album")) {
                                String id = albumXml.getValue("id", null);
@@ -269,7 +260,7 @@ public class SoneParser {
                                }
                                Album parent = null;
                                if (parentId != null) {
-                                       parent = database.getAlbum(parentId);
+                                       parent = allAlbums.get(parentId);
                                        if (parent == null) {
                                                logger.log(Level.WARNING, String.format("Downloaded Sone %s has album with invalid parent!", sone));
                                                return null;
@@ -288,6 +279,7 @@ public class SoneParser {
                                } else {
                                        topLevelAlbums.add(album);
                                }
+                               allAlbums.put(album.getId(), album);
                                SimpleXML imagesXml = albumXml.getNode("images");
                                if (imagesXml != null) {
                                        for (SimpleXML imageXml : imagesXml.getNodes("image")) {
@@ -334,6 +326,11 @@ public class SoneParser {
                        sone.getRootAlbum().addAlbum(album);
                }
 
+               // record the duration
+               stopwatch.stop();
+               soneParsingDurationHistogram.update(stopwatch.elapsed(MICROSECONDS));
+               logger.fine(() -> "Parsed " + originalSone.getIdentity().getId() + "@" + originalSone.getLatestEdition() + " in " + stopwatch.elapsed(MICROSECONDS) + "μs.");
+
                return sone;
 
        }
index 2246ac9..2a1d14d 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - SoneRescuer.java - Copyright © 2011–2019 David Roden
+ * Sone - SoneRescuer.java - Copyright © 2011–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
diff --git a/src/main/java/net/pterodactylus/sone/core/SoneUri.java b/src/main/java/net/pterodactylus/sone/core/SoneUri.java
deleted file mode 100644 (file)
index 373077f..0000000
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * Sone - SoneUri.java - Copyright © 2013–2019 David Roden
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <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;
-               }
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/core/UpdateChecker.java b/src/main/java/net/pterodactylus/sone/core/UpdateChecker.java
deleted file mode 100644 (file)
index 9f6963f..0000000
+++ /dev/null
@@ -1,246 +0,0 @@
-/*
- * Sone - UpdateChecker.java - Copyright © 2011–2019 David Roden
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.sone.core;
-
-import static java.util.logging.Logger.getLogger;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.net.MalformedURLException;
-import java.util.Date;
-import java.util.Properties;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-
-import javax.inject.Singleton;
-
-import net.pterodactylus.sone.core.event.UpdateFoundEvent;
-import net.pterodactylus.sone.main.PluginHomepage;
-import net.pterodactylus.sone.main.SonePlugin;
-import net.pterodactylus.util.io.Closer;
-import net.pterodactylus.util.version.Version;
-
-import com.google.common.eventbus.EventBus;
-import com.google.inject.Inject;
-
-import freenet.keys.FreenetURI;
-import freenet.support.api.Bucket;
-
-/**
- * Watches the official Sone homepage for new releases.
- */
-@Singleton
-public class UpdateChecker {
-
-       /** The logger. */
-       private static final Logger logger = getLogger(UpdateChecker.class.getName());
-
-       /** The event bus. */
-       private final EventBus eventBus;
-
-       /** The Freenet interface. */
-       private final FreenetInterface freenetInterface;
-
-       /** The current URI of the homepage. */
-       private FreenetURI currentUri;
-
-       /** The latest known edition. */
-       private long latestEdition = SonePlugin.getLatestEdition();
-
-       /** The current latest known version. */
-       private Version currentLatestVersion;
-       private final Version currentRunningVersion;
-
-       /** The release date of the latest version. */
-       private long latestVersionDate;
-
-       private final PluginHomepage pluginHomepage;
-
-       /**
-        * Creates a new update checker.
-        *
-        * @param eventBus
-        *            The event bus
-        * @param freenetInterface
-        *            The freenet interface to use
-        */
-       @Inject
-       public UpdateChecker(EventBus eventBus, FreenetInterface freenetInterface, Version currentVersion, PluginHomepage pluginHomepage) {
-               this.eventBus = eventBus;
-               this.freenetInterface = freenetInterface;
-               this.currentRunningVersion = currentVersion;
-               this.currentLatestVersion = currentVersion;
-               this.pluginHomepage = pluginHomepage;
-       }
-
-       //
-       // ACCESSORS
-       //
-
-       /**
-        * Returns whether a version that is later than the currently running
-        * version has been found.
-        *
-        * @return {@code true} if a new version was found
-        */
-       public boolean hasLatestVersion() {
-               return currentLatestVersion.compareTo(currentRunningVersion) > 0;
-       }
-
-       /**
-        * Returns the latest version. If no new latest version has been found, the
-        * current version is returned.
-        *
-        * @return The latest known version
-        */
-       public Version getLatestVersion() {
-               return currentLatestVersion;
-       }
-
-       /**
-        * Returns the release time of the latest version. If no new latest version
-        * has been found, the returned value is undefined.
-        *
-        * @return The release time of the latest version, if a new version was
-        *         found
-        */
-       public long getLatestVersionDate() {
-               return latestVersionDate;
-       }
-
-       /**
-        * Returns the latest known edition of the Sone homepage.
-        *
-        * @return The latest edition of the Sone homepage
-        */
-       public long getLatestEdition() {
-               return latestEdition;
-       }
-
-       //
-       // ACTIONS
-       //
-
-       /**
-        * Starts the update checker.
-        */
-       public void start() {
-               try {
-                       currentUri = new FreenetURI(pluginHomepage.getHomepage());
-               } catch (MalformedURLException mue1) {
-                       /* this can not really happen unless I screw up. */
-                       logger.log(Level.SEVERE, "Sone Homepage URI invalid!", mue1);
-               }
-               freenetInterface.registerUsk(currentUri, new FreenetInterface.Callback() {
-
-                       @Override
-                       @SuppressWarnings("synthetic-access")
-                       public void editionFound(FreenetURI uri, long edition, boolean newKnownGood, boolean newSlot) {
-                               logger.log(Level.FINEST, String.format("Found update for %s: %d, %s, %s", uri, edition, newKnownGood, newSlot));
-                               if (newKnownGood || newSlot) {
-                                       Fetched uriResult = freenetInterface.fetchUri(uri.setMetaString(new String[] { "sone.properties" }));
-                                       if (uriResult == null) {
-                                               logger.log(Level.WARNING, String.format("Could not fetch properties of latest homepage: %s", uri));
-                                               return;
-                                       }
-                                       Bucket resultBucket = uriResult.getFetchResult().asBucket();
-                                       try {
-                                               parseProperties(resultBucket.getInputStream(), edition);
-                                               latestEdition = edition;
-                                       } catch (IOException ioe1) {
-                                               logger.log(Level.WARNING, String.format("Could not parse sone.properties of %s!", uri), ioe1);
-                                       } finally {
-                                               resultBucket.free();
-                                       }
-                               }
-                       }
-               });
-       }
-
-       /**
-        * Stops the update checker.
-        */
-       public void stop() {
-               freenetInterface.unregisterUsk(currentUri);
-       }
-
-       //
-       // PRIVATE ACTIONS
-       //
-
-       /**
-        * Parses the properties of the latest version and fires events, if
-        * necessary.
-        *
-        * @see UpdateFoundEvent
-        * @param propertiesInputStream
-        *            The input stream to parse
-        * @param edition
-        *            The latest edition of the Sone homepage
-        * @throws IOException
-        *             if an I/O error occured
-        */
-       private void parseProperties(InputStream propertiesInputStream, long edition) throws IOException {
-               Properties properties = new Properties();
-               InputStreamReader inputStreamReader = null;
-               try {
-                       inputStreamReader = new InputStreamReader(propertiesInputStream, "UTF-8");
-                       properties.load(inputStreamReader);
-               } finally {
-                       Closer.close(inputStreamReader);
-               }
-               String versionString = properties.getProperty("CurrentVersion/Version");
-               String releaseTimeString = properties.getProperty("CurrentVersion/ReleaseTime");
-               if ((versionString == null) || (releaseTimeString == null)) {
-                       logger.log(Level.INFO, "Invalid data parsed from properties.");
-                       return;
-               }
-               Version version = Version.parse(versionString);
-               long releaseTime = 0;
-               try {
-                       releaseTime = Long.parseLong(releaseTimeString);
-               } catch (NumberFormatException nfe1) {
-                       /* ignore. */
-               }
-               if ((version == null) || (releaseTime == 0)) {
-                       logger.log(Level.INFO, "Could not parse data from properties.");
-                       return;
-               }
-               if (version.compareTo(currentLatestVersion) > 0) {
-                       currentLatestVersion = version;
-                       latestVersionDate = releaseTime;
-                       boolean disruptive = disruptiveVersionBetweenCurrentAndFound(properties);
-                       logger.log(Level.INFO, String.format("Found new version: %s (%tc%s)", version, new Date(releaseTime), disruptive ? ", disruptive" : ""));
-                       eventBus.post(new UpdateFoundEvent(version, releaseTime, edition, disruptive));
-               }
-       }
-
-       private boolean disruptiveVersionBetweenCurrentAndFound(Properties properties) {
-               for (String key : properties.stringPropertyNames()) {
-                       if (key.startsWith("DisruptiveVersion/")) {
-                               Version disruptiveVersion = Version.parse(key.substring("DisruptiveVersion/".length()));
-                               if (disruptiveVersion.compareTo(currentRunningVersion) > 0) {
-                                       return true;
-                               }
-                       }
-               }
-               return false;
-       }
-
-}
index 35c3203..95d066d 100644 (file)
@@ -1,6 +1,5 @@
 package net.pterodactylus.sone.core;
 
-import net.pterodactylus.sone.freenet.wot.Identity;
 import net.pterodactylus.sone.freenet.wot.OwnIdentity;
 import net.pterodactylus.util.service.Service;
 
@@ -12,7 +11,6 @@ import com.google.inject.ImplementedBy;
 @ImplementedBy(WebOfTrustUpdaterImpl.class)
 public interface WebOfTrustUpdater extends Service {
 
-       void setTrust(OwnIdentity truster, Identity trustee, Integer score, String comment);
        boolean addContextWait(OwnIdentity ownIdentity, String context);
        void removeContext(OwnIdentity ownIdentity, String context);
        void setProperty(OwnIdentity ownIdentity, String propertyName, String propertyValue);
index 809ca20..da27152 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - WebOfTrustUpdaterImpl.java - Copyright © 2013–2019 David Roden
+ * Sone - WebOfTrustUpdaterImpl.java - Copyright © 2013–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -26,11 +26,8 @@ import java.util.logging.Level;
 import java.util.logging.Logger;
 
 import net.pterodactylus.sone.freenet.plugin.PluginException;
-import net.pterodactylus.sone.freenet.wot.Identity;
 import net.pterodactylus.sone.freenet.wot.OwnIdentity;
-import net.pterodactylus.sone.freenet.wot.Trust;
 import net.pterodactylus.sone.freenet.wot.WebOfTrustConnector;
-import net.pterodactylus.sone.freenet.wot.WebOfTrustException;
 import net.pterodactylus.util.service.AbstractService;
 
 import com.google.common.annotations.VisibleForTesting;
@@ -74,34 +71,6 @@ public class WebOfTrustUpdaterImpl extends AbstractService implements WebOfTrust
        //
 
        /**
-        * Updates the trust relation between the truster and the trustee. This method
-        * will return immediately and perform a trust update in the background.
-        *
-        * @param truster
-        *              The identity giving the trust
-        * @param trustee
-        *              The identity receiving the trust
-        * @param score
-        *              The new level of trust (from -100 to 100, may be {@code null} to remove
-        *              the trust completely)
-        * @param comment
-        *              The comment of the trust relation
-        */
-       @Override
-       public void setTrust(OwnIdentity truster, Identity trustee, Integer score, String comment) {
-               SetTrustJob setTrustJob = new SetTrustJob(truster, trustee, score, comment);
-               if (updateJobs.contains(setTrustJob)) {
-                       updateJobs.remove(setTrustJob);
-               }
-               logger.log(Level.FINER, "Adding Trust Update Job: " + setTrustJob);
-               try {
-                       updateJobs.put(setTrustJob);
-               } catch (InterruptedException e) {
-                       /* the queue is unbounded so it should never block. */
-               }
-       }
-
-       /**
         * Adds the given context to the given own identity, waiting for completion of
         * the operation.
         *
@@ -297,91 +266,6 @@ public class WebOfTrustUpdaterImpl extends AbstractService implements WebOfTrust
        }
 
        /**
-        * Update job that sets the trust relation between two identities.
-        */
-       @VisibleForTesting
-       class SetTrustJob extends WebOfTrustUpdateJob {
-
-               /** The identity giving the trust. */
-               private final OwnIdentity truster;
-
-               /** The identity receiving the trust. */
-               private final Identity trustee;
-
-               /** The score of the relation. */
-               private final Integer score;
-
-               /** The comment of the relation. */
-               private final String comment;
-
-               /**
-                * Creates a new set trust job.
-                *
-                * @param truster
-                *              The identity giving the trust
-                * @param trustee
-                *              The identity receiving the trust
-                * @param score
-                *              The score of the trust (from -100 to 100, may be {@code null} to remote
-                *              the trust relation completely)
-                * @param comment
-                *              The comment of the trust relation
-                */
-               public SetTrustJob(OwnIdentity truster, Identity trustee, Integer score, String comment) {
-                       this.truster = checkNotNull(truster, "truster must not be null");
-                       this.trustee = checkNotNull(trustee, "trustee must not be null");
-                       this.score = score;
-                       this.comment = comment;
-               }
-
-               /** {@inheritDoc} */
-               @Override
-               @SuppressWarnings("synthetic-access")
-               public void run() {
-                       try {
-                               if (score != null) {
-                                       webOfTrustConnector.setTrust(truster, trustee, score, comment);
-                                       trustee.setTrust(truster, new Trust(score, null, 0));
-                               } else {
-                                       webOfTrustConnector.removeTrust(truster, trustee);
-                                       trustee.removeTrust(truster);
-                               }
-                               finish(true);
-                       } catch (WebOfTrustException wote1) {
-                               logger.log(Level.WARNING, "Could not set Trust value for " + truster + " -> " + trustee + " to " + score + " (" + comment + ")!", wote1);
-                               finish(false);
-                       }
-               }
-
-               //
-               // OBJECT METHODS
-               //
-
-               /** {@inheritDoc} */
-               @Override
-               public boolean equals(Object object) {
-                       if ((object == null) || !object.getClass().equals(getClass())) {
-                               return false;
-                       }
-                       SetTrustJob updateJob = (SetTrustJob) object;
-                       return updateJob.truster.equals(truster) && updateJob.trustee.equals(trustee);
-               }
-
-               /** {@inheritDoc} */
-               @Override
-               public int hashCode() {
-                       return getClass().hashCode() ^ truster.hashCode() ^ trustee.hashCode();
-               }
-
-               /** {@inheritDoc} */
-               @Override
-               public String toString() {
-                       return String.format("%s[truster=%s,trustee=%s]", getClass().getSimpleName(), truster.getId(), trustee.getId());
-               }
-
-       }
-
-       /**
         * Base class for context updates of an {@link OwnIdentity}.
         */
        @VisibleForTesting
diff --git a/src/main/java/net/pterodactylus/sone/core/event/ImageEvent.java b/src/main/java/net/pterodactylus/sone/core/event/ImageEvent.java
deleted file mode 100644 (file)
index 6daef1a..0000000
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * Sone - ImageEvent.java - Copyright © 2013–2019 David Roden
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.sone.core.event;
-
-import net.pterodactylus.sone.data.Image;
-
-/**
- * Base class for {@link Image} events.
- */
-public abstract class ImageEvent {
-
-       /** The image this event is about. */
-       private final Image image;
-
-       /**
-        * Creates a new image event.
-        *
-        * @param image
-        *            The image this event is about
-        */
-       protected ImageEvent(Image image) {
-               this.image = image;
-       }
-
-       //
-       // ACCESSORS
-       //
-
-       /**
-        * Returns the image this event is about.
-        *
-        * @return The image this event is about
-        */
-       public Image image() {
-               return image;
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/core/event/ImageInsertAbortedEvent.java b/src/main/java/net/pterodactylus/sone/core/event/ImageInsertAbortedEvent.java
deleted file mode 100644 (file)
index 2bfa3a2..0000000
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * Sone - ImageInsertAbortedEvent.java - Copyright © 2013–2019 David Roden
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.sone.core.event;
-
-import net.pterodactylus.sone.data.Image;
-
-/**
- * Event that signals that an {@link Image} insert is aborted.
- */
-public class ImageInsertAbortedEvent extends ImageEvent {
-
-       /**
-        * Creates a new “image insert aborted” event.
-        *
-        * @param image
-        *            The image whose insert aborted
-        */
-       public ImageInsertAbortedEvent(Image image) {
-               super(image);
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/core/event/ImageInsertFailedEvent.java b/src/main/java/net/pterodactylus/sone/core/event/ImageInsertFailedEvent.java
deleted file mode 100644 (file)
index 430c64b..0000000
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- * Sone - ImageInsertFailedEvent.java - Copyright © 2013–2019 David Roden
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.sone.core.event;
-
-import net.pterodactylus.sone.data.Image;
-
-/**
- * Event that signals that an {@link Image} insert has failed.
- */
-public class ImageInsertFailedEvent extends ImageEvent {
-
-       /** The cause of the insert failure. */
-       private final Throwable cause;
-
-       /**
-        * Creates a new “image insert failed” event.
-        *
-        * @param image
-        *            The image whose insert failed
-        * @param cause
-        *            The cause of the insert failure
-        */
-       public ImageInsertFailedEvent(Image image, Throwable cause) {
-               super(image);
-               this.cause = cause;
-       }
-
-       //
-       // ACCESSORS
-       //
-
-       /**
-        * Returns the cause of the insert failure.
-        *
-        * @return The cause of the insert failure
-        */
-       public Throwable cause() {
-               return cause;
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/core/event/ImageInsertFinishedEvent.java b/src/main/java/net/pterodactylus/sone/core/event/ImageInsertFinishedEvent.java
deleted file mode 100644 (file)
index 4419311..0000000
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * Sone - ImageInsertFinishedEvent.java - Copyright © 2013–2019 David Roden
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.sone.core.event;
-
-import net.pterodactylus.sone.data.Image;
-import freenet.keys.FreenetURI;
-
-/**
- * Event that signals that an {@link Image} insert is finished.
- */
-public class ImageInsertFinishedEvent extends ImageEvent {
-
-       /** The URI of the image. */
-       private final FreenetURI resultingUri;
-
-       /**
-        * Creates a new “image insert finished” event.
-        *
-        * @param image
-        *            The image whose insert finished
-        * @param resultingUri
-        *            The resulting URI of the image
-        */
-       public ImageInsertFinishedEvent(Image image, FreenetURI resultingUri) {
-               super(image);
-               this.resultingUri = resultingUri;
-       }
-
-       //
-       // ACCESSORS
-       //
-
-       /**
-        * Returns the URI of the image.
-        *
-        * @return The URI of the image
-        */
-       public FreenetURI resultingUri() {
-               return resultingUri;
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/core/event/ImageInsertStartedEvent.java b/src/main/java/net/pterodactylus/sone/core/event/ImageInsertStartedEvent.java
deleted file mode 100644 (file)
index ac9e3e3..0000000
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * Sone - ImageInsertStartedEvent.java - Copyright © 2013–2019 David Roden
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.sone.core.event;
-
-import net.pterodactylus.sone.data.Image;
-
-/**
- * Event that signals that an {@link Image} is not being inserted.
- */
-public class ImageInsertStartedEvent extends ImageEvent {
-
-       /**
-        * Creates a new “image is inserted” event.
-        *
-        * @param image
-        *            The image that is being inserted
-        */
-       public ImageInsertStartedEvent(Image image) {
-               super(image);
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/core/event/MarkPostKnownEvent.java b/src/main/java/net/pterodactylus/sone/core/event/MarkPostKnownEvent.java
deleted file mode 100644 (file)
index 7c8a6c3..0000000
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- * Sone - MarkPostKnownEvent.java - Copyright © 2013–2019 David Roden
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.sone.core.event;
-
-import net.pterodactylus.sone.data.Post;
-
-/**
- * Event that signals that a {@link Post} has been marked as
- * {@link Post#isKnown() known}.
- */
-public class MarkPostKnownEvent extends PostEvent {
-
-       /**
-        * Creates a new “post marked known” event.
-        *
-        * @param post
-        *            The post that was marked as known
-        */
-       public MarkPostKnownEvent(Post post) {
-               super(post);
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/core/event/MarkPostReplyKnownEvent.java b/src/main/java/net/pterodactylus/sone/core/event/MarkPostReplyKnownEvent.java
deleted file mode 100644 (file)
index 4c0e2fc..0000000
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- * Sone - MarkPostReplyKnownEvent.java - Copyright © 2013–2019 David Roden
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.sone.core.event;
-
-import net.pterodactylus.sone.data.PostReply;
-
-/**
- * Event that signals that a {@link PostReply} has been marked as
- * {@link PostReply#isKnown() known}.
- */
-public class MarkPostReplyKnownEvent extends PostReplyEvent {
-
-       /**
-        * Creates a new “post reply marked known” event.
-        *
-        * @param postReply
-        *            The post reply that was marked as known
-        */
-       public MarkPostReplyKnownEvent(PostReply postReply) {
-               super(postReply);
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/core/event/MarkSoneKnownEvent.java b/src/main/java/net/pterodactylus/sone/core/event/MarkSoneKnownEvent.java
deleted file mode 100644 (file)
index 5bbdf5e..0000000
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- * Sone - MarkSoneKnownEvent.java - Copyright © 2013–2019 David Roden
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.sone.core.event;
-
-import net.pterodactylus.sone.data.Sone;
-
-/**
- * Event that signals that a {@link Sone} has been marked as
- * {@link Sone#isKnown() known}.
- */
-public class MarkSoneKnownEvent extends SoneEvent {
-
-       /**
-        * Creates a new “Sone marked known” event.
-        *
-        * @param sone
-        *            The Sone that was marked as known
-        */
-       public MarkSoneKnownEvent(Sone sone) {
-               super(sone);
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/core/event/NewSoneFoundEvent.java b/src/main/java/net/pterodactylus/sone/core/event/NewSoneFoundEvent.java
deleted file mode 100644 (file)
index 8fb17d2..0000000
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * Sone - NewSoneFoundEvent.java - Copyright © 2013–2019 David Roden
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.sone.core.event;
-
-import net.pterodactylus.sone.data.Sone;
-
-/**
- * Event that signals that a new remote Sone was found.
- */
-public class NewSoneFoundEvent extends SoneEvent {
-
-       /**
-        * Creates a new “new Sone found” event.
-        *
-        * @param sone
-        *            The Sone that was found
-        */
-       public NewSoneFoundEvent(Sone sone) {
-               super(sone);
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/core/event/PostEvent.java b/src/main/java/net/pterodactylus/sone/core/event/PostEvent.java
deleted file mode 100644 (file)
index 93a61c6..0000000
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * Sone - PostEvent.java - Copyright © 2013–2019 David Roden
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.sone.core.event;
-
-import net.pterodactylus.sone.data.Post;
-
-/**
- * Base class for post events.
- */
-public class PostEvent {
-
-       /** The post the event is about. */
-       private final Post post;
-
-       /**
-        * Creates a new post event.
-        *
-        * @param post
-        *            The post the event is about
-        */
-       protected PostEvent(Post post) {
-               this.post = post;
-       }
-
-       //
-       // ACCESSORS
-       //
-
-       /**
-        * Returns the post the event is about.
-        *
-        * @return The post the event is about
-        */
-       public Post post() {
-               return post;
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/core/event/PostReplyEvent.java b/src/main/java/net/pterodactylus/sone/core/event/PostReplyEvent.java
deleted file mode 100644 (file)
index e31870b..0000000
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * Sone - PostReplyEvent.java - Copyright © 2013–2019 David Roden
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.sone.core.event;
-
-import net.pterodactylus.sone.data.PostReply;
-
-/**
- * Base class for {@link PostReply} events.
- */
-public class PostReplyEvent {
-
-       /** The post reply the event is about. */
-       private final PostReply postReply;
-
-       /**
-        * Creates a new post reply event.
-        *
-        * @param postReply
-        *            The post reply the event is about
-        */
-       protected PostReplyEvent(PostReply postReply) {
-               this.postReply = postReply;
-       }
-
-       //
-       // ACCESSORS
-       //
-
-       /**
-        * Returns the post reply the event is about.
-        *
-        * @return The post reply the event is about
-        */
-       public PostReply postReply() {
-               return postReply;
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/core/event/SoneEvent.java b/src/main/java/net/pterodactylus/sone/core/event/SoneEvent.java
deleted file mode 100644 (file)
index b7497ff..0000000
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * Sone - SoneEvent.java - Copyright © 2013–2019 David Roden
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.sone.core.event;
-
-import net.pterodactylus.sone.data.Sone;
-
-/**
- * Base class for Sone events.
- */
-public abstract class SoneEvent {
-
-       /** The Sone this event is about. */
-       private final Sone sone;
-
-       /**
-        * Creates a new Sone event.
-        *
-        * @param sone
-        *            The Sone this event is about
-        */
-       protected SoneEvent(Sone sone) {
-               this.sone = sone;
-       }
-
-       //
-       // ACCESSORS
-       //
-
-       /**
-        * Returns the Sone this event is about.
-        *
-        * @return The Sone this event is about
-        */
-       public Sone sone() {
-               return sone;
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/core/event/SoneInsertAbortedEvent.java b/src/main/java/net/pterodactylus/sone/core/event/SoneInsertAbortedEvent.java
deleted file mode 100644 (file)
index a8c9bb6..0000000
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- * Sone - SoneInsertAbortedEvent.java - Copyright © 2013–2019 David Roden
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.sone.core.event;
-
-import net.pterodactylus.sone.data.Sone;
-
-/**
- * Event that signals that a {@link Sone} insert was aborted.
- */
-public class SoneInsertAbortedEvent extends SoneEvent {
-
-       /** The cause of the abortion. */
-       private final Throwable cause;
-
-       /**
-        * Creates a new “Sone was inserted” event.
-        *
-        * @param sone
-        *            The Sone that was inserted
-        * @param cause
-        *            The cause of the abortion
-        */
-       public SoneInsertAbortedEvent(Sone sone, Throwable cause) {
-               super(sone);
-               this.cause = cause;
-       }
-
-       //
-       // ACCESSORS
-       //
-
-       /**
-        * Returns the cause of the abortion.
-        *
-        * @return The cause of the abortion (may be {@code null})
-        */
-       public Throwable cause() {
-               return cause;
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/core/event/SoneInsertedEvent.java b/src/main/java/net/pterodactylus/sone/core/event/SoneInsertedEvent.java
deleted file mode 100644 (file)
index 53fa935..0000000
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
- * Sone - SoneInsertedEvent.java - Copyright © 2013–2019 David Roden
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.sone.core.event;
-
-import net.pterodactylus.sone.data.Sone;
-
-/**
- * Event that signals that a {@link Sone} was inserted.
- */
-public class SoneInsertedEvent extends SoneEvent {
-
-       private final long insertDuration;
-       private final String insertFingerprint;
-
-       public SoneInsertedEvent(Sone sone, long insertDuration, String insertFingerprint) {
-               super(sone);
-               this.insertDuration = insertDuration;
-               this.insertFingerprint = insertFingerprint;
-       }
-
-       public long insertDuration() {
-               return insertDuration;
-       }
-
-       public String insertFingerprint() {
-               return insertFingerprint;
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/core/event/SoneInsertingEvent.java b/src/main/java/net/pterodactylus/sone/core/event/SoneInsertingEvent.java
deleted file mode 100644 (file)
index a9ef6fb..0000000
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * Sone - SoneInsertingEvent.java - Copyright © 2013–2019 David Roden
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.sone.core.event;
-
-import net.pterodactylus.sone.data.Sone;
-
-/**
- * Event that signals that a {@link Sone} is now being inserted.
- */
-public class SoneInsertingEvent extends SoneEvent {
-
-       /**
-        * Creates a new “Sone is being inserted” event.
-        *
-        * @param sone
-        *            The Sone that is being inserted
-        */
-       public SoneInsertingEvent(Sone sone) {
-               super(sone);
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/core/event/SoneLockedEvent.java b/src/main/java/net/pterodactylus/sone/core/event/SoneLockedEvent.java
deleted file mode 100644 (file)
index cdc9e3a..0000000
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- * Sone - SoneLockedEvent.java - Copyright © 2013–2019 David Roden
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.sone.core.event;
-
-import net.pterodactylus.sone.data.Sone;
-
-/**
- * Event that signals that a {@link Sone} was locked. Only
- * {@link Sone#isLocal() local Sones} can be locked.
- */
-public class SoneLockedEvent extends SoneEvent {
-
-       /**
-        * Creates a new “Sone locked” event.
-        *
-        * @param sone
-        *            The Sone that was locked
-        */
-       public SoneLockedEvent(Sone sone) {
-               super(sone);
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/core/event/SoneRemovedEvent.java b/src/main/java/net/pterodactylus/sone/core/event/SoneRemovedEvent.java
deleted file mode 100644 (file)
index e46272d..0000000
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * Sone - SoneRemovedEvent.java - Copyright © 2013–2019 David Roden
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.sone.core.event;
-
-import net.pterodactylus.sone.data.Sone;
-
-/**
- * Event that signals that a {@link Sone} was removed.
- */
-public class SoneRemovedEvent extends SoneEvent {
-
-       /**
-        * Creates a new “Sone removed” event.
-        *
-        * @param sone
-        *            The Sone that was removed
-        */
-       public SoneRemovedEvent(Sone sone) {
-               super(sone);
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/core/event/SoneUnlockedEvent.java b/src/main/java/net/pterodactylus/sone/core/event/SoneUnlockedEvent.java
deleted file mode 100644 (file)
index 106d32d..0000000
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- * Sone - SoneUnlockedEvent.java - Copyright © 2013–2019 David Roden
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.sone.core.event;
-
-import net.pterodactylus.sone.data.Sone;
-
-/**
- * Event that signals that a {@link Sone} was unlocked. Only
- * {@link Sone#isLocal() local Sones} can be locked.
- */
-public class SoneUnlockedEvent extends SoneEvent {
-
-       /**
-        * Creates a new “Sone unlocked” event.
-        *
-        * @param sone
-        *            The Sone that was unlocked
-        */
-       public SoneUnlockedEvent(Sone sone) {
-               super(sone);
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/core/event/UpdateFoundEvent.java b/src/main/java/net/pterodactylus/sone/core/event/UpdateFoundEvent.java
deleted file mode 100644 (file)
index 2884090..0000000
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * Sone - UpdateFoundEvent.java - Copyright © 2013–2019 David Roden
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.sone.core.event;
-
-import net.pterodactylus.util.version.Version;
-
-/**
- * Event that signals that an update for Sone was found.
- */
-public class UpdateFoundEvent {
-
-       private final Version version;
-       private final long releaseTime;
-       private final long latestEdition;
-       private final boolean disruptive;
-
-       public UpdateFoundEvent(Version version, long releaseTime, long latestEdition, boolean disruptive) {
-               this.version = version;
-               this.releaseTime = releaseTime;
-               this.latestEdition = latestEdition;
-               this.disruptive = disruptive;
-       }
-
-       public Version version() {
-               return version;
-       }
-
-       public long releaseTime() {
-               return releaseTime;
-       }
-
-       public long latestEdition() {
-               return latestEdition;
-       }
-
-       public boolean disruptive() {
-               return disruptive;
-       }
-
-}
index c4af294..bf8f3ec 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - Album.java - Copyright © 2011–2019 David Roden
+ * Sone - Album.java - Copyright © 2011–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
 
 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 f23decf..7642957 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - Client.java - Copyright © 2010–2019 David Roden
+ * Sone - Client.java - Copyright © 2010–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -81,4 +81,9 @@ public class Client {
                return Objects.hashCode(name, version);
        }
 
+       @Override
+       public String toString() {
+               return name + " " + version;
+       }
+
 }
index 93d0702..8414f3a 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - Image.java - Copyright © 2011–2019 David Roden
+ * Sone - Image.java - Copyright © 2011–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
index b6648e2..d4d34e6 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - Post.java - Copyright © 2010–2019 David Roden
+ * Sone - Post.java - Copyright © 2010–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -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 dc4a903..6db3876 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - PostReply.java - Copyright © 2010–2019 David Roden
+ * Sone - PostReply.java - Copyright © 2010–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -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 8ea09f7..34246cb 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - Profile.java - Copyright © 2010–2019 David Roden
+ * Sone - Profile.java - Copyright © 2010–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -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 c596605..0cf2a52 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - Reply.java - Copyright © 2010–2019 David Roden
+ * Sone - Reply.java - Copyright © 2010–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
 
 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 273b73e..2b0a2eb 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - Sone.java - Copyright © 2010–2019 David Roden
+ * Sone - Sone.java - Copyright © 2010–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
 
 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 378a348..b76c3cd 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - TemporaryImage.java - Copyright © 2011–2019 David Roden
+ * Sone - TemporaryImage.java - Copyright © 2011–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
index 460be4e..04b0eac 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - AbstractAlbumBuilder.java - Copyright © 2013–2019 David Roden
+ * Sone - AbstractAlbumBuilder.java - Copyright © 2013–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
index f545ae9..56ceb6d 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - AbstractImageBuilder.java - Copyright © 2013–2019 David Roden
+ * Sone - AbstractImageBuilder.java - Copyright © 2013–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
index cabe8d5..ca927b2 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - AbstractPostBuilder.java - Copyright © 2013–2019 David Roden
+ * Sone - AbstractPostBuilder.java - Copyright © 2013–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
index 0fe124e..8dffdcd 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - AbstractPostReplyBuilder.java - Copyright © 2013–2019 David Roden
+ * Sone - AbstractPostReplyBuilder.java - Copyright © 2013–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
index 052ca2d..7e95f38 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - AbstractReplyBuilder.java - Copyright © 2013–2019 David Roden
+ * Sone - AbstractReplyBuilder.java - Copyright © 2013–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
index 601daa8..696d08d 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - AlbumBuilderImpl.java - Copyright © 2013–2019 David Roden
+ * Sone - AlbumBuilderImpl.java - Copyright © 2013–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
index 3ec362f..04ab8e9 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - AlbumImpl.java - Copyright © 2011–2019 David Roden
+ * Sone - AlbumImpl.java - Copyright © 2011–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
 
 package net.pterodactylus.sone.data.impl;
 
-import static com.google.common.base.Optional.absent;
-import static com.google.common.base.Optional.fromNullable;
-import static com.google.common.base.Preconditions.checkArgument;
-import static com.google.common.base.Preconditions.checkNotNull;
-import static java.nio.charset.StandardCharsets.UTF_8;
-
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.UUID;
-
-import net.pterodactylus.sone.data.Album;
-import net.pterodactylus.sone.data.Image;
-import net.pterodactylus.sone.data.Sone;
-
-import com.google.common.base.Function;
-import com.google.common.base.Optional;
-import com.google.common.base.Predicates;
-import com.google.common.collect.Collections2;
-import com.google.common.hash.Hasher;
+import java.util.*;
+import javax.annotation.*;
+
+import com.google.common.base.*;
+import com.google.common.collect.*;
 import com.google.common.hash.Hashing;
+import com.google.common.hash.*;
+import net.pterodactylus.sone.data.*;
+
+import static com.google.common.base.Preconditions.*;
+import static java.nio.charset.StandardCharsets.*;
 
 /**
  * Container for images that can also contain nested {@link AlbumImpl}s.
@@ -258,32 +247,33 @@ public class AlbumImpl implements Album {
        public Modifier modify() throws IllegalStateException {
                // TODO: reenable check for local Sones
                return new Modifier() {
-                       private Optional<String> title = absent();
-
-                       private Optional<String> description = absent();
+                       @Nullable
+                       private String title;
+                       @Nullable
+                       private String description;
 
                        @Override
                        public Modifier setTitle(String title) {
-                               this.title = fromNullable(title);
+                               this.title = title;
                                return this;
                        }
 
                        @Override
                        public Modifier setDescription(String description) {
-                               this.description = fromNullable(description);
+                               this.description = description;
                                return this;
                        }
 
                        @Override
                        public Album update() throws IllegalStateException {
-                               if (title.isPresent() && title.get().trim().isEmpty()) {
+                               if (title != null && title.trim().isEmpty()) {
                                        throw new AlbumTitleMustNotBeEmpty();
                                }
-                               if (title.isPresent()) {
-                                       AlbumImpl.this.title = title.get();
+                               if (title != null) {
+                                       AlbumImpl.this.title = title;
                                }
-                               if (description.isPresent()) {
-                                       AlbumImpl.this.description = description.get();
+                               if (description != null) {
+                                       AlbumImpl.this.description = description;
                                }
                                return AlbumImpl.this;
                        }
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 b62cc40..e74b71a 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - ImageBuilderImpl.java - Copyright © 2013–2019 David Roden
+ * Sone - ImageBuilderImpl.java - Copyright © 2013–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
index a54a8de..0dd84fe 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - ImageImpl.java - Copyright © 2011–2019 David Roden
+ * Sone - ImageImpl.java - Copyright © 2011–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
  */
 package net.pterodactylus.sone.data.impl;
 
-import static com.google.common.base.Optional.absent;
-import static com.google.common.base.Optional.fromNullable;
-import static com.google.common.base.Optional.of;
-import static com.google.common.base.Preconditions.checkNotNull;
-import static com.google.common.base.Preconditions.checkState;
-import static java.nio.charset.StandardCharsets.UTF_8;
+import java.util.*;
+import javax.annotation.*;
 
-import java.util.UUID;
+import com.google.common.hash.*;
+import net.pterodactylus.sone.data.*;
 
-import net.pterodactylus.sone.data.Album;
-import net.pterodactylus.sone.data.Image;
-import net.pterodactylus.sone.data.Sone;
-
-import com.google.common.base.Optional;
-import com.google.common.hash.Hasher;
-import com.google.common.hash.Hashing;
+import static com.google.common.base.Preconditions.*;
+import static java.nio.charset.StandardCharsets.*;
 
 /**
  * Container for image metadata.
@@ -146,93 +138,94 @@ public class ImageImpl implements Image {
        public Modifier modify() throws IllegalStateException {
                // TODO: reenable check for local images
                return new Modifier() {
-                       private Optional<Sone> sone = absent();
-
-                       private Optional<Long> creationTime = absent();
-
-                       private Optional<String> key = absent();
-
-                       private Optional<String> title = absent();
-
-                       private Optional<String> description = absent();
-
-                       private Optional<Integer> width = absent();
-
-                       private Optional<Integer> height = absent();
+                       @Nullable
+                       private Sone sone;
+                       @Nullable
+                       private Long creationTime;
+                       @Nullable
+                       private String key;
+                       @Nullable
+                       private String title;
+                       @Nullable
+                       private String description;
+                       @Nullable
+                       private Integer width;
+                       @Nullable
+                       private Integer height;
 
                        @Override
                        public Modifier setSone(Sone sone) {
-                               this.sone = fromNullable(sone);
+                               this.sone = sone;
                                return this;
                        }
 
                        @Override
                        public Modifier setCreationTime(long creationTime) {
-                               this.creationTime = of(creationTime);
+                               this.creationTime = creationTime;
                                return this;
                        }
 
                        @Override
                        public Modifier setKey(String key) {
-                               this.key = fromNullable(key);
+                               this.key = key;
                                return this;
                        }
 
                        @Override
                        public Modifier setTitle(String title) {
-                               this.title = fromNullable(title);
+                               this.title = title;
                                return this;
                        }
 
                        @Override
                        public Modifier setDescription(String description) {
-                               this.description = fromNullable(description);
+                               this.description = description;
                                return this;
                        }
 
                        @Override
                        public Modifier setWidth(int width) {
-                               this.width = of(width);
+                               this.width = width;
                                return this;
                        }
 
                        @Override
                        public Modifier setHeight(int height) {
-                               this.height = of(height);
+                               this.height = height;
                                return this;
                        }
 
                        @Override
                        public Image update() throws IllegalStateException {
-                               checkState(!sone.isPresent() || (ImageImpl.this.sone == null) || sone.get().equals(ImageImpl.this.sone), "can not change Sone once set");
-                               checkState(!creationTime.isPresent() || ((ImageImpl.this.creationTime == 0) || (ImageImpl.this.creationTime == creationTime.get())), "can not change creation time once set");
-                               checkState(!key.isPresent() || (ImageImpl.this.key == null) || key.get().equals(ImageImpl.this.key), "can not change key once set");
-                               if (title.isPresent() && title.get().trim().isEmpty()) {
+                               checkState(sone == null || (ImageImpl.this.sone == null) || sone.equals(ImageImpl.this.sone), "can not change Sone once set");
+                               checkState(creationTime == null || ((ImageImpl.this.creationTime == 0) || (ImageImpl.this.creationTime == creationTime)), "can not change creation time once set");
+                               checkState(key == null || (ImageImpl.this.key == null) || key.equals(ImageImpl.this.key), "can not change key once set");
+                               if (title != null && title.trim().isEmpty()) {
                                        throw new ImageTitleMustNotBeEmpty();
                                }
-                               checkState(!width.isPresent() || (ImageImpl.this.width == 0) || width.get().equals(ImageImpl.this.width), "can not change width once set");
-                               checkState(!height.isPresent() || (ImageImpl.this.height == 0) || height.get().equals(ImageImpl.this.height), "can not change height once set");
+                               checkState(width == null || (ImageImpl.this.width == 0) || width.equals(ImageImpl.this.width), "can not change width once set");
+                               checkState(height == null || (ImageImpl.this.height == 0) || height.equals(ImageImpl.this.height), "can not change height once set");
 
-                               if (sone.isPresent()) {
-                                       ImageImpl.this.sone = sone.get();
+                               if (sone != null) {
+                                       ImageImpl.this.sone = sone;
                                }
-                               if (creationTime.isPresent()) {
-                                       ImageImpl.this.creationTime = creationTime.get();
+                               if (creationTime != null) {
+                                       ImageImpl.this.creationTime = creationTime;
                                }
-                               if (key.isPresent()) {
-                                       ImageImpl.this.key = key.get();
+                               if (key != null) {
+                                       ImageImpl.this.key = key;
                                }
-                               if (title.isPresent()) {
-                                       ImageImpl.this.title = title.get();
+                               if (title != null) {
+                                       ImageImpl.this.title = title;
                                }
-                               if (description.isPresent()) {
-                                       ImageImpl.this.description = description.get();
+                               if (description != null) {
+                                       ImageImpl.this.description = description;
                                }
-                               if (width.isPresent()) {
-                                       ImageImpl.this.width = width.get();
+                               if (width != null) {
+                                       ImageImpl.this.width = width;
                                }
-                               if (height.isPresent()) {
-                                       ImageImpl.this.height = height.get();
+                               if (height != null) {
+                                       ImageImpl.this.height = height;
                                }
 
                                return ImageImpl.this;
index 3366448..f2f2ea6 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - SoneImpl.java - Copyright © 2010–2019 David Roden
+ * Sone - SoneImpl.java - Copyright © 2010–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -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);
        }
 
        //
index 9763ad6..6a24756 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - DatabaseException.java - Copyright © 2013–2019 David Roden
+ * Sone - DatabaseException.java - Copyright © 2013–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
diff --git a/src/main/java/net/pterodactylus/sone/database/memory/MemoryDatabase.kt b/src/main/java/net/pterodactylus/sone/database/memory/MemoryDatabase.kt
deleted file mode 100644 (file)
index 6fce7e0..0000000
+++ /dev/null
@@ -1,345 +0,0 @@
-/*
- * Sone - MemoryDatabase.kt - Copyright © 2013–2019 David Roden
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <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.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.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) }, 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)
-
-       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() {
-               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() =
-                       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() =
-                       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 30fc2b0..451b6b5 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - MemoryPost.java - Copyright © 2010–2019 David Roden
+ * Sone - MemoryPost.java - Copyright © 2010–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
index dea3449..b9e66fe 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - MemoryPostBuilder.java - Copyright © 2013–2019 David Roden
+ * Sone - MemoryPostBuilder.java - Copyright © 2013–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
 
 package net.pterodactylus.sone.database.memory;
 
-import java.util.UUID;
+import java.util.*;
+import javax.annotation.*;
 
-import net.pterodactylus.sone.data.Post;
-import net.pterodactylus.sone.data.impl.AbstractPostBuilder;
-import net.pterodactylus.sone.database.PostBuilder;
-import net.pterodactylus.sone.database.SoneProvider;
+import net.pterodactylus.sone.data.*;
+import net.pterodactylus.sone.data.impl.*;
+import net.pterodactylus.sone.database.*;
 
 /**
  * {@link PostBuilder} implementation that creates a {@link MemoryPost}.
  */
 class MemoryPostBuilder extends AbstractPostBuilder {
 
-       /** The database. */
        private final MemoryDatabase database;
 
-       /**
-        * Creates a new memory post builder.
-        *
-        * @param memoryDatabase
-        *            The database
-        * @param soneProvider
-        *            The Sone provider
-        */
        public MemoryPostBuilder(MemoryDatabase memoryDatabase, SoneProvider soneProvider) {
                super(soneProvider);
                database = memoryDatabase;
        }
 
-       /**
-        * {@inheritDocs}
-        */
+       @Nonnull
        @Override
        public Post build() throws IllegalStateException {
                validate();
-               Post post = new MemoryPost(database, soneProvider, randomId ? UUID.randomUUID().toString() : id, senderId, recipientId, currentTime ? System.currentTimeMillis() : time, text);
-               post.setKnown(database.isPostKnown(post));
-               return post;
+               return new MemoryPost(database, soneProvider, randomId ? UUID.randomUUID().toString() : id, senderId, recipientId, currentTime ? System.currentTimeMillis() : time, text);
        }
 
 }
index 40f0775..5764622 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - MemoryPostReply.java - Copyright © 2013–2019 David Roden
+ * Sone - MemoryPostReply.java - Copyright © 2013–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -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 3bcfe33..b6e2e24 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - MemoryPostReplyBuilder.java - Copyright © 2013–2019 David Roden
+ * Sone - MemoryPostReplyBuilder.java - Copyright © 2013–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
 
 package net.pterodactylus.sone.database.memory;
 
-import java.util.UUID;
+import java.util.*;
+import javax.annotation.*;
 
-import net.pterodactylus.sone.data.PostReply;
-import net.pterodactylus.sone.data.impl.AbstractPostReplyBuilder;
-import net.pterodactylus.sone.database.PostReplyBuilder;
-import net.pterodactylus.sone.database.SoneProvider;
+import net.pterodactylus.sone.data.*;
+import net.pterodactylus.sone.data.impl.*;
+import net.pterodactylus.sone.database.*;
 
 /**
  * {@link PostReplyBuilder} implementation that creates {@link MemoryPostReply}
@@ -30,35 +30,20 @@ import net.pterodactylus.sone.database.SoneProvider;
  */
 class MemoryPostReplyBuilder extends AbstractPostReplyBuilder {
 
-       /** The database. */
        private final MemoryDatabase database;
-
-       /** The Sone provider. */
        private final SoneProvider soneProvider;
 
-       /**
-        * Creates a new {@link MemoryPostReply} builder.
-        *
-        * @param database
-        *            The database
-        * @param soneProvider
-        *            The Sone provider
-        */
        public MemoryPostReplyBuilder(MemoryDatabase database, SoneProvider soneProvider) {
                this.database = database;
                this.soneProvider = soneProvider;
        }
 
-       /**
-        * {@inheritDocs}
-        */
+       @Nonnull
        @Override
        public PostReply build() throws IllegalStateException {
                validate();
 
-               PostReply postReply = new MemoryPostReply(database, soneProvider, randomId ? UUID.randomUUID().toString() : id, senderId, currentTime ? System.currentTimeMillis() : time, text, postId);
-               postReply.setKnown(database.isPostReplyKnown(postReply));
-               return postReply;
+               return new MemoryPostReply(database, soneProvider, randomId ? UUID.randomUUID().toString() : id, senderId, currentTime ? System.currentTimeMillis() : time, text, postId);
        }
 
 }
index 2c8456d..3cb1d6b 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - CreatePostCommand.java - Copyright © 2011–2019 David Roden
+ * Sone - CreatePostCommand.java - Copyright © 2011–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -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;
@@ -29,7 +27,7 @@ import freenet.support.SimpleFieldSet;
 /**
  * FCP command that creates a new {@link Post}.
  *
- * @see Core#createPost(Sone, Optional, String)
+ * @see Core#createPost(Sone, Sone, String)
  */
 public class CreatePostCommand extends AbstractSoneCommand {
 
@@ -57,7 +55,7 @@ public class CreatePostCommand extends AbstractSoneCommand {
                if (sone.equals(recipient)) {
                        return new ErrorResponse("Sone and Recipient must not be the same.");
                }
-               Post post = getCore().createPost(sone, Optional.fromNullable(recipient), text);
+               Post post = getCore().createPost(sone, recipient, text);
                return new Response("PostCreated", new SimpleFieldSetBuilder().put("Post", post.getId()).get());
        }
 
index 9f17940..de0f309 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - CreateReplyCommand.java - Copyright © 2011–2019 David Roden
+ * Sone - CreateReplyCommand.java - Copyright © 2011–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
index c93029f..5d599e1 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - DeletePostCommand.java - Copyright © 2011–2019 David Roden
+ * Sone - DeletePostCommand.java - Copyright © 2011–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
index 4531f93..59f744a 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - DeleteReplyCommand.java - Copyright © 2011–2019 David Roden
+ * Sone - DeleteReplyCommand.java - Copyright © 2011–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
index 0b321f4..3bb8777 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - FcpInterface.java - Copyright © 2011–2019 David Roden
+ * Sone - FcpInterface.java - Copyright © 2011–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
index 1427fe7..3ef919e 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - GetLocalSonesCommand.java - Copyright © 2011–2019 David Roden
+ * Sone - GetLocalSonesCommand.java - Copyright © 2011–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
index d1dc643..8033bcb 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - GetPostCommand.java - Copyright © 2011–2019 David Roden
+ * Sone - GetPostCommand.java - Copyright © 2011–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
index bf80dc2..05c2349 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - GetPostFeedCommand.java - Copyright © 2011–2019 David Roden
+ * Sone - GetPostFeedCommand.java - Copyright © 2011–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -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 9cc131b..2f250a3 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - GetPostsCommand.java - Copyright © 2011–2019 David Roden
+ * Sone - GetPostsCommand.java - Copyright © 2011–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
index a4b936b..64d24fe 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - GetSoneCommand.java - Copyright © 2011–2019 David Roden
+ * Sone - GetSoneCommand.java - Copyright © 2011–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
index ec43143..eedf120 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - GetSonesCommand.java - Copyright © 2011–2019 David Roden
+ * Sone - GetSonesCommand.java - Copyright © 2011–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -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 edd5a43..058045a 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - LikePostCommand.java - Copyright © 2011–2019 David Roden
+ * Sone - LikePostCommand.java - Copyright © 2011–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
index 800e43c..acdfb98 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - LikeReplyCommand.java - Copyright © 2011–2019 David Roden
+ * Sone - LikeReplyCommand.java - Copyright © 2011–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
index 691bc4b..fbceb9a 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - LockSoneCommand.java - Copyright © 2013–2019 David Roden
+ * Sone - LockSoneCommand.java - Copyright © 2013–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
index ca5a59e..d489603 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - UnlockSoneCommand.java - Copyright © 2013–2019 David Roden
+ * Sone - UnlockSoneCommand.java - Copyright © 2013–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
index 7acea2d..a7d1cd7 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - VersionCommand.java - Copyright © 2011–2019 David Roden
+ * Sone - VersionCommand.java - Copyright © 2011–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
diff --git a/src/main/java/net/pterodactylus/sone/freenet/Key.java b/src/main/java/net/pterodactylus/sone/freenet/Key.java
deleted file mode 100644 (file)
index 6811642..0000000
+++ /dev/null
@@ -1,65 +0,0 @@
-package net.pterodactylus.sone.freenet;
-
-import static freenet.support.Base64.encode;
-import static java.lang.String.format;
-
-import freenet.keys.FreenetURI;
-
-import com.google.common.annotations.VisibleForTesting;
-
-/**
- * Encapsulates the parts of a {@link FreenetURI} that do not change while
- * being converted from SSK to USK and/or back.
- */
-public class Key {
-
-       private final byte[] routingKey;
-       private final byte[] cryptoKey;
-       private final byte[] extra;
-
-       private Key(byte[] routingKey, byte[] cryptoKey, byte[] extra) {
-               this.routingKey = routingKey;
-               this.cryptoKey = cryptoKey;
-               this.extra = extra;
-       }
-
-       @VisibleForTesting
-       public String getRoutingKey() {
-               return encode(routingKey);
-       }
-
-       @VisibleForTesting
-       public String getCryptoKey() {
-               return encode(cryptoKey);
-       }
-
-       @VisibleForTesting
-       public String getExtra() {
-               return encode(extra);
-       }
-
-       public FreenetURI toUsk(String docName, long edition, String... paths) {
-               return new FreenetURI("USK", docName, paths, routingKey, cryptoKey,
-                               extra, edition);
-       }
-
-       public FreenetURI toSsk(String docName, String... paths) {
-               return new FreenetURI("SSK", docName, paths, routingKey, cryptoKey,
-                               extra);
-       }
-
-       public FreenetURI toSsk(String docName, long edition, String... paths) {
-               return new FreenetURI("SSK", format("%s-%d", docName, edition), paths,
-                               routingKey, cryptoKey, extra, edition);
-       }
-
-       public static Key from(FreenetURI freenetURI) {
-               return new Key(freenetURI.getRoutingKey(), freenetURI.getCryptoKey(),
-                               freenetURI.getExtra());
-       }
-
-       public static String routingKey(FreenetURI freenetURI) {
-               return from(freenetURI).getRoutingKey();
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/freenet/L10nFilter.java b/src/main/java/net/pterodactylus/sone/freenet/L10nFilter.java
deleted file mode 100644 (file)
index d667811..0000000
+++ /dev/null
@@ -1,78 +0,0 @@
-/*
- * Sone - L10nFilter.java - Copyright © 2010–2019 David Roden
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.sone.freenet;
-
-import java.text.MessageFormat;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Locale;
-import java.util.Map;
-
-import javax.annotation.Nonnull;
-
-import net.pterodactylus.util.template.Filter;
-import net.pterodactylus.util.template.TemplateContext;
-
-import freenet.l10n.BaseL10n;
-
-/**
- * {@link Filter} implementation replaces {@link String} values with their
- * translated equivalents.
- */
-public class L10nFilter implements Filter {
-
-       private final BaseL10n l10n;
-
-       public L10nFilter(BaseL10n l10n) {
-               this.l10n = l10n;
-       }
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       public String format(TemplateContext templateContext, Object data, Map<String, Object> parameters) {
-               List<Object> parameterValues = getParameters(data, parameters);
-               String text = getText(data);
-               if (parameterValues.isEmpty()) {
-                       return l10n.getString(text);
-               }
-               return new MessageFormat(l10n.getString(text), new Locale(l10n.getSelectedLanguage().shortCode)).format(parameterValues.toArray());
-       }
-
-       @Nonnull
-       private String getText(Object data) {
-               return (data instanceof L10nText) ? ((L10nText) data).getText() : String.valueOf(data);
-       }
-
-       @Nonnull
-       private List<Object> getParameters(Object data, Map<String, Object> parameters) {
-               if (data instanceof L10nText) {
-                       return ((L10nText) data).getParameters();
-               }
-               List<Object> parameterValues = new ArrayList<>();
-               int parameterIndex = 0;
-               while (parameters.containsKey(String.valueOf(parameterIndex))) {
-                       Object value = parameters.get(String.valueOf(parameterIndex));
-                       parameterValues.add(value);
-                       ++parameterIndex;
-               }
-               return parameterValues;
-       }
-
-}
index cce544f..a8a2697 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - PluginStoreConfigurationBackend.java - Copyright © 2010–2019 David Roden
+ * Sone - PluginStoreConfigurationBackend.java - Copyright © 2010–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
index 48eb4bf..a974a0c 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - SimpleFieldSetBuilder.java - Copyright © 2011–2019 David Roden
+ * Sone - SimpleFieldSetBuilder.java - Copyright © 2011–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
index 3ea4804..890e3ff 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - AbstractCommand.java - Copyright © 2011–2019 David Roden
+ * Sone - AbstractCommand.java - Copyright © 2011–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
index 63fc8eb..f3f32cf 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - Command.java - Copyright © 2011–2019 David Roden
+ * Sone - Command.java - Copyright © 2011–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
index 0e156a1..db70018 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - FcpException.java - Copyright © 2011–2019 David Roden
+ * Sone - FcpException.java - Copyright © 2011–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
diff --git a/src/main/java/net/pterodactylus/sone/freenet/plugin/PluginConnector.java b/src/main/java/net/pterodactylus/sone/freenet/plugin/PluginConnector.java
deleted file mode 100644 (file)
index fbaabb3..0000000
+++ /dev/null
@@ -1,133 +0,0 @@
-/*
- * Sone - PluginConnector.java - Copyright © 2010–2019 David Roden
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.sone.freenet.plugin;
-
-import net.pterodactylus.sone.freenet.plugin.event.ReceivedReplyEvent;
-
-import com.google.common.eventbus.EventBus;
-import com.google.inject.Inject;
-import com.google.inject.Singleton;
-
-import freenet.pluginmanager.FredPluginTalker;
-import freenet.pluginmanager.PluginNotFoundException;
-import freenet.pluginmanager.PluginRespirator;
-import freenet.pluginmanager.PluginTalker;
-import freenet.support.SimpleFieldSet;
-import freenet.support.api.Bucket;
-
-/**
- * Interface for talking to other plugins. Other plugins are identified by their
- * name and a unique connection identifier.
- */
-@Singleton
-public class PluginConnector implements FredPluginTalker {
-
-       /** The event bus. */
-       private final EventBus eventBus;
-
-       /** The plugin respirator. */
-       private final PluginRespirator pluginRespirator;
-
-       /**
-        * Creates a new plugin connector.
-        *
-        * @param eventBus
-        *            The event bus
-        * @param pluginRespirator
-        *            The plugin respirator
-        */
-       @Inject
-       public PluginConnector(EventBus eventBus, PluginRespirator pluginRespirator) {
-               this.eventBus = eventBus;
-               this.pluginRespirator = pluginRespirator;
-       }
-
-       //
-       // ACTIONS
-       //
-
-       /**
-        * Sends a request to the given plugin.
-        *
-        * @param pluginName
-        *            The name of the plugin
-        * @param identifier
-        *            The identifier of the connection
-        * @param fields
-        *            The fields of the message
-        * @throws PluginException
-        *             if the plugin can not be found
-        */
-       public void sendRequest(String pluginName, String identifier, SimpleFieldSet fields) throws PluginException {
-               sendRequest(pluginName, identifier, fields, null);
-       }
-
-       /**
-        * Sends a request to the given plugin.
-        *
-        * @param pluginName
-        *            The name of the plugin
-        * @param identifier
-        *            The identifier of the connection
-        * @param fields
-        *            The fields of the message
-        * @param data
-        *            The payload of the message (may be null)
-        * @throws PluginException
-        *             if the plugin can not be found
-        */
-       public void sendRequest(String pluginName, String identifier, SimpleFieldSet fields, Bucket data) throws PluginException {
-               getPluginTalker(pluginName, identifier).send(fields, data);
-       }
-
-       //
-       // PRIVATE METHODS
-       //
-
-       /**
-        * Returns the plugin talker for the given plugin connection.
-        *
-        * @param pluginName
-        *            The name of the plugin
-        * @param identifier
-        *            The identifier of the connection
-        * @return The plugin talker
-        * @throws PluginException
-        *             if the plugin can not be found
-        */
-       private PluginTalker getPluginTalker(String pluginName, String identifier) throws PluginException {
-               try {
-                       return pluginRespirator.getPluginTalker(this, pluginName, identifier);
-               } catch (PluginNotFoundException pnfe1) {
-                       throw new PluginException(pnfe1);
-               }
-       }
-
-       //
-       // INTERFACE FredPluginTalker
-       //
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       public void onReply(String pluginName, String identifier, SimpleFieldSet params, Bucket data) {
-               eventBus.post(new ReceivedReplyEvent(this, pluginName, identifier, params, data));
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/freenet/plugin/PluginException.java b/src/main/java/net/pterodactylus/sone/freenet/plugin/PluginException.java
deleted file mode 100644 (file)
index e263a60..0000000
+++ /dev/null
@@ -1,66 +0,0 @@
-/*
- * Sone - PluginException.java - Copyright © 2010–2019 David Roden
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.sone.freenet.plugin;
-
-import net.pterodactylus.sone.freenet.wot.WebOfTrustException;
-
-/**
- * Exception that signals an error when communicating with a plugin.
- */
-public class PluginException extends WebOfTrustException {
-
-       /**
-        * Creates a new plugin exception.
-        */
-       public PluginException() {
-               super();
-       }
-
-       /**
-        * Creates a new plugin exception.
-        *
-        * @param message
-        *            The message of the exception
-        */
-       public PluginException(String message) {
-               super(message);
-       }
-
-       /**
-        * Creates a new plugin exception.
-        *
-        * @param cause
-        *            The cause of the exception
-        */
-       public PluginException(Throwable cause) {
-               super(cause);
-       }
-
-       /**
-        * Creates a new plugin exception.
-        *
-        * @param message
-        *            The message of the exception
-        * @param cause
-        *            The cause of the exception
-        */
-       public PluginException(String message, Throwable cause) {
-               super(message, cause);
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/freenet/plugin/event/ReceivedReplyEvent.java b/src/main/java/net/pterodactylus/sone/freenet/plugin/event/ReceivedReplyEvent.java
deleted file mode 100644 (file)
index ce2ba7f..0000000
+++ /dev/null
@@ -1,115 +0,0 @@
-/*
- * Sone - ReceivedReplyEvent.java - Copyright © 2013–2019 David Roden
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.sone.freenet.plugin.event;
-
-import net.pterodactylus.sone.freenet.plugin.PluginConnector;
-import freenet.support.SimpleFieldSet;
-import freenet.support.api.Bucket;
-
-/**
- * Event that signals that a plugin reply was received.
- */
-public class ReceivedReplyEvent {
-
-       /** The connector that received the reply. */
-       private final PluginConnector pluginConnector;
-
-       /** The name of the plugin that sent the reply. */
-       private final String pluginName;
-
-       /** The identifier of the initial request. */
-       private final String identifier;
-
-       /** The fields containing the reply. */
-       private final SimpleFieldSet fieldSet;
-
-       /** The optional reply data. */
-       private final Bucket data;
-
-       /**
-        * Creates a new “reply received” event.
-        *
-        * @param pluginConnector
-        *            The connector that received the event
-        * @param pluginName
-        *            The name of the plugin that sent the reply
-        * @param identifier
-        *            The identifier of the initial request
-        * @param fieldSet
-        *            The fields containing the reply
-        * @param data
-        *            The optional data of the reply
-        */
-       public ReceivedReplyEvent(PluginConnector pluginConnector, String pluginName, String identifier, SimpleFieldSet fieldSet, Bucket data) {
-               this.pluginConnector = pluginConnector;
-               this.pluginName = pluginName;
-               this.identifier = identifier;
-               this.fieldSet = fieldSet;
-               this.data = data;
-       }
-
-       //
-       // ACCESSORS
-       //
-
-       /**
-        * Returns the plugin connector that received the reply.
-        *
-        * @return The plugin connector that received the reply
-        */
-       public PluginConnector pluginConnector() {
-               return pluginConnector;
-       }
-
-       /**
-        * Returns the name of the plugin that sent the reply.
-        *
-        * @return The name of the plugin that sent the reply
-        */
-       public String pluginName() {
-               return pluginName;
-       }
-
-       /**
-        * Returns the identifier of the initial request.
-        *
-        * @return The identifier of the initial request
-        */
-       public String identifier() {
-               return identifier;
-       }
-
-       /**
-        * Returns the fields containing the reply.
-        *
-        * @return The fields containing the reply
-        */
-       public SimpleFieldSet fieldSet() {
-               return fieldSet;
-       }
-
-       /**
-        * Returns the optional data of the reply.
-        *
-        * @return The optional data of the reply (may be {@code null})
-        */
-       public Bucket data() {
-               return data;
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/freenet/wot/Context.java b/src/main/java/net/pterodactylus/sone/freenet/wot/Context.java
deleted file mode 100644 (file)
index b386bdb..0000000
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * Sone - Context.java - Copyright © 2014–2019 David Roden
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.sone.freenet.wot;
-
-import javax.annotation.Nullable;
-
-import com.google.common.base.Function;
-
-/**
- * Custom container for the Web of Trust context. This allows easier
- * configuration of dependency injection.
- */
-public class Context {
-
-       public static final Function<Context, String> extractContext = new Function<Context, String>() {
-               @Nullable
-               @Override
-               public String apply(@Nullable Context context) {
-                       return (context == null) ? null : context.getContext();
-               }
-       };
-
-       private final String context;
-
-       public Context(String context) {
-               this.context = context;
-       }
-
-       public String getContext() {
-               return context;
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/freenet/wot/DefaultIdentity.java b/src/main/java/net/pterodactylus/sone/freenet/wot/DefaultIdentity.java
deleted file mode 100644 (file)
index 0d92d61..0000000
+++ /dev/null
@@ -1,181 +0,0 @@
-/*
- * Sone - DefaultIdentity.java - Copyright © 2010–2019 David Roden
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.sone.freenet.wot;
-
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Map;
-import java.util.Set;
-
-/**
- * A Web of Trust identity.
- */
-public class DefaultIdentity implements Identity {
-
-       /** The ID of the identity. */
-       private final String id;
-
-       /** The nickname of the identity. */
-       private final String nickname;
-
-       /** The request URI of the identity. */
-       private final String requestUri;
-
-       /** The contexts of the identity. */
-       private final Set<String> contexts = Collections.synchronizedSet(new HashSet<String>());
-
-       /** The properties of the identity. */
-       private final Map<String, String> properties = Collections.synchronizedMap(new HashMap<String, String>());
-
-       /** Cached trust. */
-       private final Map<OwnIdentity, Trust> trustCache = Collections.synchronizedMap(new HashMap<OwnIdentity, Trust>());
-
-       /**
-        * Creates a new identity.
-        *
-        * @param id
-        *            The ID of the identity
-        * @param nickname
-        *            The nickname of the identity
-        * @param requestUri
-        *            The request URI of the identity
-        */
-       public DefaultIdentity(String id, String nickname, String requestUri) {
-               this.id = id;
-               this.nickname = nickname;
-               this.requestUri = requestUri;
-       }
-
-       //
-       // ACCESSORS
-       //
-
-       @Override
-       public String getId() {
-               return id;
-       }
-
-       @Override
-       public String getNickname() {
-               return nickname;
-       }
-
-       @Override
-       public String getRequestUri() {
-               return requestUri;
-       }
-
-       @Override
-       public Set<String> getContexts() {
-               return Collections.unmodifiableSet(contexts);
-       }
-
-       @Override
-       public boolean hasContext(String context) {
-               return contexts.contains(context);
-       }
-
-       @Override
-       public void setContexts(Collection<String> contexts) {
-               this.contexts.clear();
-               this.contexts.addAll(contexts);
-       }
-
-       @Override
-       public Identity addContext(String context) {
-               contexts.add(context);
-               return this;
-       }
-
-       @Override
-       public Identity removeContext(String context) {
-               contexts.remove(context);
-               return this;
-       }
-
-       @Override
-       public Map<String, String> getProperties() {
-               return Collections.unmodifiableMap(properties);
-       }
-
-       @Override
-       public void setProperties(Map<String, String> properties) {
-               this.properties.clear();
-               this.properties.putAll(properties);
-       }
-
-       @Override
-       public String getProperty(String name) {
-               return properties.get(name);
-       }
-
-       @Override
-       public Identity setProperty(String name, String value) {
-               properties.put(name, value);
-               return this;
-       }
-
-       @Override
-       public Identity removeProperty(String name) {
-               properties.remove(name);
-               return this;
-       }
-
-       @Override
-       public Trust getTrust(OwnIdentity ownIdentity) {
-               return trustCache.get(ownIdentity);
-       }
-
-       @Override
-       public Identity setTrust(OwnIdentity ownIdentity, Trust trust) {
-               trustCache.put(ownIdentity, trust);
-               return this;
-       }
-
-       @Override
-       public Identity removeTrust(OwnIdentity ownIdentity) {
-               trustCache.remove(ownIdentity);
-               return this;
-       }
-
-       //
-       // OBJECT METHODS
-       //
-
-       @Override
-       public int hashCode() {
-               return getId().hashCode();
-       }
-
-       @Override
-       public boolean equals(Object object) {
-               if (!(object instanceof Identity)) {
-                       return false;
-               }
-               Identity identity = (Identity) object;
-               return identity.getId().equals(getId());
-       }
-
-       @Override
-       public String toString() {
-               return getClass().getSimpleName() + "[id=" + id + ",nickname=" + nickname + ",contexts=" + contexts + ",properties=" + properties + "]";
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/freenet/wot/DefaultOwnIdentity.java b/src/main/java/net/pterodactylus/sone/freenet/wot/DefaultOwnIdentity.java
deleted file mode 100644 (file)
index e2e3a74..0000000
+++ /dev/null
@@ -1,91 +0,0 @@
-/*
- * Sone - DefaultOwnIdentity.java - Copyright © 2010–2019 David Roden
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.sone.freenet.wot;
-
-import static com.google.common.base.Preconditions.checkNotNull;
-
-/**
- * An own identity is an identity that the owner of the node has full control
- * over.
- */
-public class DefaultOwnIdentity extends DefaultIdentity implements OwnIdentity {
-
-       /** The insert URI of the identity. */
-       private final String insertUri;
-
-       /**
-        * Creates a new own identity.
-        *
-        * @param id
-        *            The ID of the identity
-        * @param nickname
-        *            The nickname of the identity
-        * @param requestUri
-        *            The request URI of the identity
-        * @param insertUri
-        *            The insert URI of the identity
-        */
-       public DefaultOwnIdentity(String id, String nickname, String requestUri, String insertUri) {
-               super(id, nickname, requestUri);
-               this.insertUri = checkNotNull(insertUri);
-       }
-
-       //
-       // ACCESSORS
-       //
-
-       @Override
-       public String getInsertUri() {
-               return insertUri;
-       }
-
-       @Override
-       public OwnIdentity addContext(String context) {
-               return (OwnIdentity) super.addContext(context);
-       }
-
-       @Override
-       public OwnIdentity removeContext(String context) {
-               return (OwnIdentity) super.removeContext(context);
-       }
-
-       @Override
-       public OwnIdentity setProperty(String name, String value) {
-               return (OwnIdentity) super.setProperty(name, value);
-       }
-
-       @Override
-       public OwnIdentity removeProperty(String name) {
-               return (OwnIdentity) super.removeProperty(name);
-       }
-
-       //
-       // OBJECT METHODS
-       //
-
-       @Override
-       public int hashCode() {
-               return super.hashCode();
-       }
-
-       @Override
-       public boolean equals(Object object) {
-               return super.equals(object);
-       }
-
-}
index a99aac0..e6f4f62 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - Identity.java - Copyright © 2010–2019 David Roden
+ * Sone - Identity.java - Copyright © 2010–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
 
 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
@@ -31,20 +27,6 @@ import com.google.common.base.Function;
  */
 public interface Identity {
 
-       public static final Function<Identity, Set<String>> TO_CONTEXTS = new Function<Identity, Set<String>>() {
-               @Override
-               public Set<String> apply(Identity identity) {
-                       return (identity == null) ? Collections.<String>emptySet() : identity.getContexts();
-               }
-       };
-
-       public static final Function<Identity, Map<String, String>> TO_PROPERTIES = new Function<Identity, Map<String, String>>() {
-               @Override
-               public Map<String, String> apply(Identity input) {
-                       return (input == null) ? Collections.<String, String>emptyMap() : input.getProperties();
-               }
-       };
-
        /**
         * Returns the ID of the identity.
         *
@@ -97,7 +79,7 @@ public interface Identity {
         * @param contexts
         *            All contexts of the identity
         */
-       public void setContexts(Collection<String> contexts);
+       public void setContexts(Set<String> contexts);
 
        /**
         * Removes the given context from this identity.
@@ -149,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.java b/src/main/java/net/pterodactylus/sone/freenet/wot/IdentityChangeDetector.java
deleted file mode 100644 (file)
index 8b28011..0000000
+++ /dev/null
@@ -1,198 +0,0 @@
-/*
- * Sone - IdentityChangeDetector.java - Copyright © 2013–2019 David Roden
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.sone.freenet.wot;
-
-import static com.google.common.base.Optional.absent;
-import static com.google.common.base.Optional.fromNullable;
-import static com.google.common.base.Predicates.not;
-import static com.google.common.collect.FluentIterable.from;
-import static net.pterodactylus.sone.freenet.wot.Identity.TO_CONTEXTS;
-import static net.pterodactylus.sone.freenet.wot.Identity.TO_PROPERTIES;
-
-import java.util.Collection;
-import java.util.Map;
-import java.util.Map.Entry;
-
-import com.google.common.base.Optional;
-import com.google.common.base.Predicate;
-import com.google.common.collect.FluentIterable;
-import com.google.common.collect.ImmutableMap;
-
-/**
- * Detects changes between two lists of {@link Identity}s. The detector can find
- * added and removed identities, and for identities that exist in both list
- * their contexts and properties are checked for added, removed, or (in case of
- * properties) changed values.
- */
-public class IdentityChangeDetector {
-
-       private final Map<String, Identity> oldIdentities;
-       private Optional<IdentityProcessor> onNewIdentity = absent();
-       private Optional<IdentityProcessor> onRemovedIdentity = absent();
-       private Optional<IdentityProcessor> onChangedIdentity = absent();
-       private Optional<IdentityProcessor> onUnchangedIdentity = absent();
-
-       public IdentityChangeDetector(Collection<? extends Identity> oldIdentities) {
-               this.oldIdentities = convertToMap(oldIdentities);
-       }
-
-       public void onNewIdentity(IdentityProcessor onNewIdentity) {
-               this.onNewIdentity = fromNullable(onNewIdentity);
-       }
-
-       public void onRemovedIdentity(IdentityProcessor onRemovedIdentity) {
-               this.onRemovedIdentity = fromNullable(onRemovedIdentity);
-       }
-
-       public void onChangedIdentity(IdentityProcessor onChangedIdentity) {
-               this.onChangedIdentity = fromNullable(onChangedIdentity);
-       }
-
-       public void onUnchangedIdentity(IdentityProcessor onUnchangedIdentity) {
-               this.onUnchangedIdentity = fromNullable(onUnchangedIdentity);
-       }
-
-       public void detectChanges(final Collection<? extends Identity> newIdentities) {
-               notifyForRemovedIdentities(from(oldIdentities.values()).filter(notContainedIn(newIdentities)));
-               notifyForNewIdentities(from(newIdentities).filter(notContainedIn(oldIdentities.values())));
-               notifyForChangedIdentities(from(newIdentities).filter(containedIn(oldIdentities)).filter(hasChanged(oldIdentities)));
-               notifyForUnchangedIdentities(from(newIdentities).filter(containedIn(oldIdentities)).filter(not(hasChanged(oldIdentities))));
-       }
-
-       private void notifyForRemovedIdentities(Iterable<Identity> identities) {
-               notify(onRemovedIdentity, identities);
-       }
-
-       private void notifyForNewIdentities(FluentIterable<? extends Identity> newIdentities) {
-               notify(onNewIdentity, newIdentities);
-       }
-
-       private void notifyForChangedIdentities(FluentIterable<? extends Identity> identities) {
-               notify(onChangedIdentity, identities);
-       }
-
-       private void notifyForUnchangedIdentities(FluentIterable<? extends Identity> identities) {
-               notify(onUnchangedIdentity, identities);
-       }
-
-       private void notify(Optional<IdentityProcessor> identityProcessor, Iterable<? extends Identity> identities) {
-               if (!identityProcessor.isPresent()) {
-                       return;
-               }
-               for (Identity identity : identities) {
-                       identityProcessor.get().processIdentity(identity);
-               }
-       }
-
-       private static Predicate<Identity> hasChanged(final Map<String, Identity> oldIdentities) {
-               return new Predicate<Identity>() {
-                       @Override
-                       public boolean apply(Identity identity) {
-                               return (identity != null) && identityHasChanged(oldIdentities.get(identity.getId()), identity);
-                       }
-               };
-       }
-
-       private static boolean identityHasChanged(Identity oldIdentity, Identity newIdentity) {
-               return identityHasNewContexts(oldIdentity, newIdentity)
-                               || identityHasRemovedContexts(oldIdentity, newIdentity)
-                               || identityHasNewProperties(oldIdentity, newIdentity)
-                               || identityHasRemovedProperties(oldIdentity, newIdentity)
-                               || identityHasChangedProperties(oldIdentity, newIdentity);
-       }
-
-       private static boolean identityHasNewContexts(Identity oldIdentity, Identity newIdentity) {
-               return from(TO_CONTEXTS.apply(newIdentity)).anyMatch(notAContextOf(oldIdentity));
-       }
-
-       private static boolean identityHasRemovedContexts(Identity oldIdentity, Identity newIdentity) {
-               return from(TO_CONTEXTS.apply(oldIdentity)).anyMatch(notAContextOf(newIdentity));
-       }
-
-       private static boolean identityHasNewProperties(Identity oldIdentity, Identity newIdentity) {
-               return from(TO_PROPERTIES.apply(newIdentity).entrySet()).anyMatch(notAPropertyOf(oldIdentity));
-       }
-
-       private static boolean identityHasRemovedProperties(Identity oldIdentity, Identity newIdentity) {
-               return from(TO_PROPERTIES.apply(oldIdentity).entrySet()).anyMatch(notAPropertyOf(newIdentity));
-       }
-
-       private static boolean identityHasChangedProperties(Identity oldIdentity, Identity newIdentity) {
-               return from(TO_PROPERTIES.apply(oldIdentity).entrySet()).anyMatch(hasADifferentValueThanIn(newIdentity));
-       }
-
-       private static Predicate<Identity> containedIn(final Map<String, Identity> identities) {
-               return new Predicate<Identity>() {
-                       @Override
-                       public boolean apply(Identity identity) {
-                               return (identity != null) && identities.containsKey(identity.getId());
-                       }
-               };
-       }
-
-       private static Predicate<String> notAContextOf(final Identity identity) {
-               return new Predicate<String>() {
-                       @Override
-                       public boolean apply(String context) {
-                               return (identity != null) && !identity.getContexts().contains(context);
-                       }
-               };
-       }
-
-       private static Predicate<Identity> notContainedIn(final Collection<? extends Identity> newIdentities) {
-               return new Predicate<Identity>() {
-                       @Override
-                       public boolean apply(Identity identity) {
-                               return (identity != null) && !newIdentities.contains(identity);
-                       }
-               };
-       }
-
-       private static Predicate<Entry<String, String>> notAPropertyOf(final Identity identity) {
-               return new Predicate<Entry<String, String>>() {
-                       @Override
-                       public boolean apply(Entry<String, String> property) {
-                               return (property != null) && !identity.getProperties().containsKey(property.getKey());
-                       }
-               };
-       }
-
-       private static Predicate<Entry<String, String>> hasADifferentValueThanIn(final Identity newIdentity) {
-               return new Predicate<Entry<String, String>>() {
-                       @Override
-                       public boolean apply(Entry<String, String> property) {
-                               return (property != null) && !newIdentity.getProperty(property.getKey()).equals(property.getValue());
-                       }
-               };
-       }
-
-       private static Map<String, Identity> convertToMap(Collection<? extends Identity> identities) {
-               ImmutableMap.Builder<String, Identity> mapBuilder = ImmutableMap.builder();
-               for (Identity identity : identities) {
-                       mapBuilder.put(identity.getId(), identity);
-               }
-               return mapBuilder.build();
-       }
-
-       public interface IdentityProcessor {
-
-               void processIdentity(Identity identity);
-
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/freenet/wot/IdentityChangeEventSender.java b/src/main/java/net/pterodactylus/sone/freenet/wot/IdentityChangeEventSender.java
deleted file mode 100644 (file)
index fd57c38..0000000
+++ /dev/null
@@ -1,132 +0,0 @@
-/*
- * Sone - IdentityChangeEventSender.java - Copyright © 2013–2019 David Roden
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.sone.freenet.wot;
-
-import java.util.Collection;
-import java.util.Map;
-
-import net.pterodactylus.sone.freenet.wot.IdentityChangeDetector.IdentityProcessor;
-import net.pterodactylus.sone.freenet.wot.event.IdentityAddedEvent;
-import net.pterodactylus.sone.freenet.wot.event.IdentityRemovedEvent;
-import net.pterodactylus.sone.freenet.wot.event.IdentityUpdatedEvent;
-import net.pterodactylus.sone.freenet.wot.event.OwnIdentityAddedEvent;
-import net.pterodactylus.sone.freenet.wot.event.OwnIdentityRemovedEvent;
-
-import com.google.common.eventbus.EventBus;
-
-/**
- * Detects changes in {@link Identity}s trusted my multiple {@link
- * OwnIdentity}s.
- *
- * @see IdentityChangeDetector
- */
-public class IdentityChangeEventSender {
-
-       private final EventBus eventBus;
-       private final Map<OwnIdentity, Collection<Identity>> oldIdentities;
-
-       public IdentityChangeEventSender(EventBus eventBus, Map<OwnIdentity, Collection<Identity>> oldIdentities) {
-               this.eventBus = eventBus;
-               this.oldIdentities = oldIdentities;
-       }
-
-       public void detectChanges(Map<OwnIdentity, Collection<Identity>> identities) {
-               IdentityChangeDetector identityChangeDetector = new IdentityChangeDetector(oldIdentities.keySet());
-               identityChangeDetector.onNewIdentity(addNewOwnIdentityAndItsTrustedIdentities(identities));
-               identityChangeDetector.onRemovedIdentity(removeOwnIdentityAndItsTrustedIdentities(oldIdentities));
-               identityChangeDetector.onUnchangedIdentity(detectChangesInTrustedIdentities(identities, oldIdentities));
-               identityChangeDetector.detectChanges(identities.keySet());
-       }
-
-       private IdentityProcessor addNewOwnIdentityAndItsTrustedIdentities(final Map<OwnIdentity, Collection<Identity>> newIdentities) {
-               return new IdentityProcessor() {
-                       @Override
-                       public void processIdentity(Identity identity) {
-                               eventBus.post(new OwnIdentityAddedEvent((OwnIdentity) identity));
-                               for (Identity newIdentity : newIdentities.get((OwnIdentity) identity)) {
-                                       eventBus.post(new IdentityAddedEvent((OwnIdentity) identity, newIdentity));
-                               }
-                       }
-               };
-       }
-
-       private IdentityProcessor removeOwnIdentityAndItsTrustedIdentities(final Map<OwnIdentity, Collection<Identity>> oldIdentities) {
-               return new IdentityProcessor() {
-                       @Override
-                       public void processIdentity(Identity identity) {
-                               eventBus.post(new OwnIdentityRemovedEvent((OwnIdentity) identity));
-                               for (Identity removedIdentity : oldIdentities.get((OwnIdentity) identity)) {
-                                       eventBus.post(new IdentityRemovedEvent((OwnIdentity) identity, removedIdentity));
-                               }
-                       }
-               };
-       }
-
-       private IdentityProcessor detectChangesInTrustedIdentities(Map<OwnIdentity, Collection<Identity>> newIdentities, Map<OwnIdentity, Collection<Identity>> oldIdentities) {
-               return new DefaultIdentityProcessor(oldIdentities, newIdentities);
-       }
-
-       private class DefaultIdentityProcessor implements IdentityProcessor {
-
-               private final Map<OwnIdentity, Collection<Identity>> oldIdentities;
-               private final Map<OwnIdentity, Collection<Identity>> newIdentities;
-
-               public DefaultIdentityProcessor(Map<OwnIdentity, Collection<Identity>> oldIdentities, Map<OwnIdentity, Collection<Identity>> newIdentities) {
-                       this.oldIdentities = oldIdentities;
-                       this.newIdentities = newIdentities;
-               }
-
-               @Override
-               public void processIdentity(Identity ownIdentity) {
-                       IdentityChangeDetector identityChangeDetector = new IdentityChangeDetector(oldIdentities.get((OwnIdentity) ownIdentity));
-                       identityChangeDetector.onNewIdentity(notifyForAddedIdentities((OwnIdentity) ownIdentity));
-                       identityChangeDetector.onRemovedIdentity(notifyForRemovedIdentities((OwnIdentity) ownIdentity));
-                       identityChangeDetector.onChangedIdentity(notifyForChangedIdentities((OwnIdentity) ownIdentity));
-                       identityChangeDetector.detectChanges(newIdentities.get((OwnIdentity) ownIdentity));
-               }
-
-               private IdentityProcessor notifyForChangedIdentities(final OwnIdentity ownIdentity) {
-                       return new IdentityProcessor() {
-                               @Override
-                               public void processIdentity(Identity identity) {
-                                       eventBus.post(new IdentityUpdatedEvent(ownIdentity, identity));
-                               }
-                       };
-               }
-
-               private IdentityProcessor notifyForRemovedIdentities(final OwnIdentity ownIdentity) {
-                       return new IdentityProcessor() {
-                               @Override
-                               public void processIdentity(Identity identity) {
-                                       eventBus.post(new IdentityRemovedEvent(ownIdentity, identity));
-                               }
-                       };
-               }
-
-               private IdentityProcessor notifyForAddedIdentities(final OwnIdentity ownIdentity) {
-                       return new IdentityProcessor() {
-                               @Override
-                               public void processIdentity(Identity identity) {
-                                       eventBus.post(new IdentityAddedEvent(ownIdentity, identity));
-                               }
-                       };
-               }
-
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/freenet/wot/IdentityLoader.java b/src/main/java/net/pterodactylus/sone/freenet/wot/IdentityLoader.java
deleted file mode 100644 (file)
index f16df1c..0000000
+++ /dev/null
@@ -1,77 +0,0 @@
-/*
- * Sone - IdentityLoader.java - Copyright © 2013–2019 David Roden
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.sone.freenet.wot;
-
-import static java.util.Collections.emptySet;
-import static net.pterodactylus.sone.freenet.wot.Context.extractContext;
-
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Set;
-
-import net.pterodactylus.sone.freenet.plugin.PluginException;
-
-import com.google.common.base.Optional;
-import com.google.inject.Inject;
-
-/**
- * Loads {@link OwnIdentity}s and the {@link Identity}s they trust.
- */
-public class IdentityLoader {
-
-       private final WebOfTrustConnector webOfTrustConnector;
-       private final Optional<Context> context;
-
-       public IdentityLoader(WebOfTrustConnector webOfTrustConnector) {
-               this(webOfTrustConnector, Optional.<Context>absent());
-       }
-
-       @Inject
-       public IdentityLoader(WebOfTrustConnector webOfTrustConnector, Optional<Context> context) {
-               this.webOfTrustConnector = webOfTrustConnector;
-               this.context = context;
-       }
-
-       public Map<OwnIdentity, Collection<Identity>> loadIdentities() throws WebOfTrustException {
-               Collection<OwnIdentity> currentOwnIdentities = webOfTrustConnector.loadAllOwnIdentities();
-               return loadTrustedIdentitiesForOwnIdentities(currentOwnIdentities);
-       }
-
-       private Map<OwnIdentity, Collection<Identity>> loadTrustedIdentitiesForOwnIdentities(Collection<OwnIdentity> ownIdentities) throws PluginException {
-               Map<OwnIdentity, Collection<Identity>> currentIdentities = new HashMap<>();
-
-               for (OwnIdentity ownIdentity : ownIdentities) {
-                       if (identityDoesNotHaveTheCorrectContext(ownIdentity)) {
-                               currentIdentities.put(ownIdentity, Collections.<Identity>emptySet());
-                               continue;
-                       }
-
-                       Set<Identity> trustedIdentities = webOfTrustConnector.loadTrustedIdentities(ownIdentity, context.transform(extractContext));
-                       currentIdentities.put(ownIdentity, trustedIdentities);
-               }
-
-               return currentIdentities;
-       }
-
-       private boolean identityDoesNotHaveTheCorrectContext(OwnIdentity ownIdentity) {
-               return context.isPresent() && !ownIdentity.hasContext(context.transform(extractContext).get());
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/freenet/wot/IdentityManager.java b/src/main/java/net/pterodactylus/sone/freenet/wot/IdentityManager.java
deleted file mode 100644 (file)
index c0f6f1b..0000000
+++ /dev/null
@@ -1,20 +0,0 @@
-package net.pterodactylus.sone.freenet.wot;
-
-import java.util.Set;
-
-import net.pterodactylus.util.service.Service;
-
-import com.google.common.eventbus.EventBus;
-import com.google.inject.ImplementedBy;
-
-/**
- * Connects to a {@link WebOfTrustConnector} and sends identity events to an
- * {@link EventBus}.
- */
-@ImplementedBy(IdentityManagerImpl.class)
-public interface IdentityManager extends Service {
-
-       boolean isConnected();
-       Set<OwnIdentity> getAllOwnIdentities();
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/freenet/wot/IdentityManagerImpl.java b/src/main/java/net/pterodactylus/sone/freenet/wot/IdentityManagerImpl.java
deleted file mode 100644 (file)
index 6f46465..0000000
+++ /dev/null
@@ -1,147 +0,0 @@
-/*
- * Sone - IdentityManagerImpl.java - Copyright © 2010–2019 David Roden
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.sone.freenet.wot;
-
-import static java.util.logging.Logger.getLogger;
-
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Map;
-import java.util.Set;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-
-import net.pterodactylus.sone.freenet.plugin.PluginException;
-import net.pterodactylus.util.service.AbstractService;
-
-import com.google.common.collect.Sets;
-import com.google.common.eventbus.EventBus;
-import com.google.inject.Inject;
-import com.google.inject.Singleton;
-
-/**
- * The identity manager takes care of loading and storing identities, their
- * contexts, and properties. It does so in a way that does not expose errors via
- * exceptions but it only logs them and tries to return sensible defaults.
- * <p>
- * It is also responsible for polling identities from the Web of Trust plugin
- * and sending events to the {@link EventBus} when {@link Identity}s and
- * {@link OwnIdentity}s are discovered or disappearing.
- */
-@Singleton
-public class IdentityManagerImpl extends AbstractService implements IdentityManager {
-
-       /** The logger. */
-       private static final Logger logger = getLogger(IdentityManagerImpl.class.getName());
-
-       /** The event bus. */
-       private final EventBus eventBus;
-
-       private final IdentityLoader identityLoader;
-
-       /** The Web of Trust connector. */
-       private final WebOfTrustConnector webOfTrustConnector;
-
-       /** The currently known own identities. */
-       private final Set<OwnIdentity> currentOwnIdentities = Sets.newHashSet();
-
-       /**
-        * Creates a new identity manager.
-        *
-        * @param eventBus
-        *            The event bus
-        * @param webOfTrustConnector
-        *            The Web of Trust connector
-        */
-       @Inject
-       public IdentityManagerImpl(EventBus eventBus, WebOfTrustConnector webOfTrustConnector, IdentityLoader identityLoader) {
-               super("Sone Identity Manager", false);
-               this.eventBus = eventBus;
-               this.webOfTrustConnector = webOfTrustConnector;
-               this.identityLoader = identityLoader;
-       }
-
-       //
-       // ACCESSORS
-       //
-
-       /**
-        * Returns whether the Web of Trust plugin could be reached during the last
-        * try.
-        *
-        * @return {@code true} if the Web of Trust plugin is connected,
-        *         {@code false} otherwise
-        */
-       @Override
-       public boolean isConnected() {
-               try {
-                       webOfTrustConnector.ping();
-                       return true;
-               } catch (PluginException pe1) {
-                       /* not connected, ignore. */
-                       return false;
-               }
-       }
-
-       /**
-        * Returns all own identities.
-        *
-        * @return All own identities
-        */
-       @Override
-       public Set<OwnIdentity> getAllOwnIdentities() {
-               synchronized (currentOwnIdentities) {
-                       return new HashSet<>(currentOwnIdentities);
-               }
-       }
-
-       //
-       // SERVICE METHODS
-       //
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       protected void serviceRun() {
-               Map<OwnIdentity, Collection<Identity>> oldIdentities = new HashMap<>();
-
-               while (!shouldStop()) {
-                       try {
-                               Map<OwnIdentity, Collection<Identity>> currentIdentities = identityLoader.loadIdentities();
-
-                               IdentityChangeEventSender identityChangeEventSender = new IdentityChangeEventSender(eventBus, oldIdentities);
-                               identityChangeEventSender.detectChanges(currentIdentities);
-
-                               oldIdentities = currentIdentities;
-
-                               synchronized (currentOwnIdentities) {
-                                       currentOwnIdentities.clear();
-                                       currentOwnIdentities.addAll(currentIdentities.keySet());
-                               }
-                       } catch (WebOfTrustException wote1) {
-                               logger.log(Level.WARNING, "WoT has disappeared!", wote1);
-                       }
-
-                       /* wait a minute before checking again. */
-                       sleep(60 * 1000);
-               }
-       }
-
-}
index 500f2c7..25b77b9 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - OwnIdentity.java - Copyright © 2010–2019 David Roden
+ * Sone - OwnIdentity.java - Copyright © 2010–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
diff --git a/src/main/java/net/pterodactylus/sone/freenet/wot/Trust.java b/src/main/java/net/pterodactylus/sone/freenet/wot/Trust.java
deleted file mode 100644 (file)
index 2da00f6..0000000
+++ /dev/null
@@ -1,104 +0,0 @@
-/*
- * Sone - Trust.java - Copyright © 2010–2019 David Roden
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.sone.freenet.wot;
-
-import static com.google.common.base.Objects.equal;
-
-import com.google.common.base.Objects;
-
-/**
- * Container class for trust in the web of trust.
- */
-public class Trust {
-
-       /** Explicitely assigned trust. */
-       private final Integer explicit;
-
-       /** Implicitely calculated trust. */
-       private final Integer implicit;
-
-       /** The distance from the owner of the trust tree. */
-       private final Integer distance;
-
-       /**
-        * Creates a new trust container.
-        *
-        * @param explicit
-        *            The explicit trust
-        * @param implicit
-        *            The implicit trust
-        * @param distance
-        *            The distance
-        */
-       public Trust(Integer explicit, Integer implicit, Integer distance) {
-               this.explicit = explicit;
-               this.implicit = implicit;
-               this.distance = distance;
-       }
-
-       /**
-        * Returns the trust explicitely assigned to an identity.
-        *
-        * @return The explicitely assigned trust, or {@code null} if the identity
-        *         is not in the own identity’s trust tree
-        */
-       public Integer getExplicit() {
-               return explicit;
-       }
-
-       /**
-        * Returns the implicitely assigned trust, or the calculated trust.
-        *
-        * @return The calculated trust, or {@code null} if the identity is not in
-        *         the own identity’s trust tree
-        */
-       public Integer getImplicit() {
-               return implicit;
-       }
-
-       /**
-        * Returns the distance of the trusted identity from the trusting identity.
-        *
-        * @return The distance from the own identity, or {@code null} if the
-        *         identity is not in the own identity’s trust tree
-        */
-       public Integer getDistance() {
-               return distance;
-       }
-
-       @Override
-       public boolean equals(Object object) {
-               if (!(object instanceof Trust)) {
-                       return false;
-               }
-               Trust trust = (Trust) object;
-               return equal(getExplicit(), trust.getExplicit()) && equal(getImplicit(), trust.getImplicit()) && equal(getDistance(), trust.getDistance());
-       }
-
-       @Override
-       public int hashCode() {
-               return Objects.hashCode(explicit, implicit, distance);
-       }
-
-       /** {@inheritDoc} */
-       @Override
-       public String toString() {
-               return getClass().getName() + "[explicit=" + explicit + ",implicit=" + implicit + ",distance=" + distance + "]";
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/freenet/wot/WebOfTrustConnector.java b/src/main/java/net/pterodactylus/sone/freenet/wot/WebOfTrustConnector.java
deleted file mode 100644 (file)
index c1dbef9..0000000
+++ /dev/null
@@ -1,623 +0,0 @@
-/*
- * Sone - WebOfTrustConnector.java - Copyright © 2010–2019 David Roden
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.sone.freenet.wot;
-
-import static java.util.logging.Logger.getLogger;
-import static net.pterodactylus.sone.utils.NumberParsers.parseInt;
-
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.atomic.AtomicLong;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-
-import net.pterodactylus.sone.freenet.plugin.PluginConnector;
-import net.pterodactylus.sone.freenet.plugin.PluginException;
-import net.pterodactylus.sone.freenet.plugin.event.ReceivedReplyEvent;
-
-import com.google.common.base.Optional;
-import com.google.common.collect.MapMaker;
-import com.google.common.eventbus.Subscribe;
-import com.google.inject.Inject;
-import com.google.inject.Singleton;
-
-import freenet.support.SimpleFieldSet;
-import freenet.support.api.Bucket;
-
-/**
- * Connector for the Web of Trust plugin.
- */
-@Singleton
-public class WebOfTrustConnector {
-
-       /** The logger. */
-       private static final Logger logger = getLogger(WebOfTrustConnector.class.getName());
-
-       /** The name of the WoT plugin. */
-       private static final String WOT_PLUGIN_NAME = "plugins.WebOfTrust.WebOfTrust";
-
-       /** Counter for connection identifiers. */
-       private final AtomicLong counter = new AtomicLong();
-
-       /** The plugin connector. */
-       private final PluginConnector pluginConnector;
-
-       /** Map for replies. */
-       private final Map<PluginIdentifier, Reply> replies = new MapMaker().makeMap();
-
-       /**
-        * Creates a new Web of Trust connector that uses the given plugin
-        * connector.
-        *
-        * @param pluginConnector
-        *            The plugin connector
-        */
-       @Inject
-       public WebOfTrustConnector(PluginConnector pluginConnector) {
-               this.pluginConnector = pluginConnector;
-       }
-
-       //
-       // ACTIONS
-       //
-
-       /**
-        * Stops the web of trust connector.
-        */
-       public void stop() {
-               /* does nothing. */
-       }
-
-       /**
-        * Loads all own identities from the Web of Trust plugin.
-        *
-        * @return All own identity
-        * @throws WebOfTrustException
-        *             if the own identities can not be loaded
-        */
-       public Set<OwnIdentity> loadAllOwnIdentities() throws WebOfTrustException {
-               Reply reply = performRequest(SimpleFieldSetConstructor.create().put("Message", "GetOwnIdentities").get());
-               SimpleFieldSet fields = reply.getFields();
-               int ownIdentityCounter = -1;
-               Set<OwnIdentity> ownIdentities = new HashSet<>();
-               while (true) {
-                       String id = fields.get("Identity" + ++ownIdentityCounter);
-                       if (id == null) {
-                               break;
-                       }
-                       String requestUri = fields.get("RequestURI" + ownIdentityCounter);
-                       String insertUri = fields.get("InsertURI" + ownIdentityCounter);
-                       String nickname = fields.get("Nickname" + ownIdentityCounter);
-                       DefaultOwnIdentity ownIdentity = new DefaultOwnIdentity(id, nickname, requestUri, insertUri);
-                       ownIdentity.setContexts(parseContexts("Contexts" + ownIdentityCounter + ".", fields));
-                       ownIdentity.setProperties(parseProperties("Properties" + ownIdentityCounter + ".", fields));
-                       ownIdentities.add(ownIdentity);
-               }
-               return ownIdentities;
-       }
-
-       /**
-        * Loads all identities that the given identities trusts with a score of
-        * more than 0.
-        *
-        * @param ownIdentity
-        *            The own identity
-        * @return All trusted identities
-        * @throws PluginException
-        *             if an error occured talking to the Web of Trust plugin
-        */
-       public Set<Identity> loadTrustedIdentities(OwnIdentity ownIdentity) throws PluginException {
-               return loadTrustedIdentities(ownIdentity, Optional.<String>absent());
-       }
-
-       /**
-        * Loads all identities that the given identities trusts with a score of
-        * more than 0 and the (optional) given context.
-        *
-        * @param ownIdentity
-        *            The own identity
-        * @param context
-        *            The context to filter, or {@code null}
-        * @return All trusted identities
-        * @throws PluginException
-        *             if an error occured talking to the Web of Trust plugin
-        */
-       public Set<Identity> loadTrustedIdentities(OwnIdentity ownIdentity, Optional<String> context) throws PluginException {
-               Reply reply = performRequest(SimpleFieldSetConstructor.create().put("Message", "GetIdentitiesByScore").put("Truster", ownIdentity.getId()).put("Selection", "+").put("Context", context.or("")).put("WantTrustValues", "true").get());
-               SimpleFieldSet fields = reply.getFields();
-               Set<Identity> identities = new HashSet<>();
-               int identityCounter = -1;
-               while (true) {
-                       String id = fields.get("Identity" + ++identityCounter);
-                       if (id == null) {
-                               break;
-                       }
-                       String nickname = fields.get("Nickname" + identityCounter);
-                       String requestUri = fields.get("RequestURI" + identityCounter);
-                       DefaultIdentity identity = new DefaultIdentity(id, nickname, requestUri);
-                       identity.setContexts(parseContexts("Contexts" + identityCounter + ".", fields));
-                       identity.setProperties(parseProperties("Properties" + identityCounter + ".", fields));
-                       Integer trust = parseInt(fields.get("Trust" + identityCounter), null);
-                       int score = parseInt(fields.get("Score" + identityCounter), 0);
-                       int rank = parseInt(fields.get("Rank" + identityCounter), 0);
-                       identity.setTrust(ownIdentity, new Trust(trust, score, rank));
-                       identities.add(identity);
-               }
-               return identities;
-       }
-
-       /**
-        * Adds the given context to the given identity.
-        *
-        * @param ownIdentity
-        *            The identity to add the context to
-        * @param context
-        *            The context to add
-        * @throws PluginException
-        *             if an error occured talking to the Web of Trust plugin
-        */
-       public void addContext(OwnIdentity ownIdentity, String context) throws PluginException {
-               performRequest(SimpleFieldSetConstructor.create().put("Message", "AddContext").put("Identity", ownIdentity.getId()).put("Context", context).get());
-       }
-
-       /**
-        * Removes the given context from the given identity.
-        *
-        * @param ownIdentity
-        *            The identity to remove the context from
-        * @param context
-        *            The context to remove
-        * @throws PluginException
-        *             if an error occured talking to the Web of Trust plugin
-        */
-       public void removeContext(OwnIdentity ownIdentity, String context) throws PluginException {
-               performRequest(SimpleFieldSetConstructor.create().put("Message", "RemoveContext").put("Identity", ownIdentity.getId()).put("Context", context).get());
-       }
-
-       /**
-        * Returns the value of the property with the given name.
-        *
-        * @param identity
-        *            The identity whose properties to check
-        * @param name
-        *            The name of the property to return
-        * @return The value of the property, or {@code null} if there is no value
-        * @throws PluginException
-        *             if an error occured talking to the Web of Trust plugin
-        */
-       public String getProperty(Identity identity, String name) throws PluginException {
-               Reply reply = performRequest(SimpleFieldSetConstructor.create().put("Message", "GetProperty").put("Identity", identity.getId()).put("Property", name).get());
-               return reply.getFields().get("Property");
-       }
-
-       /**
-        * Sets the property with the given name to the given value.
-        *
-        * @param ownIdentity
-        *            The identity to set the property on
-        * @param name
-        *            The name of the property to set
-        * @param value
-        *            The value to set
-        * @throws PluginException
-        *             if an error occured talking to the Web of Trust plugin
-        */
-       public void setProperty(OwnIdentity ownIdentity, String name, String value) throws PluginException {
-               performRequest(SimpleFieldSetConstructor.create().put("Message", "SetProperty").put("Identity", ownIdentity.getId()).put("Property", name).put("Value", value).get());
-       }
-
-       /**
-        * Removes the property with the given name.
-        *
-        * @param ownIdentity
-        *            The identity to remove the property from
-        * @param name
-        *            The name of the property to remove
-        * @throws PluginException
-        *             if an error occured talking to the Web of Trust plugin
-        */
-       public void removeProperty(OwnIdentity ownIdentity, String name) throws PluginException {
-               performRequest(SimpleFieldSetConstructor.create().put("Message", "RemoveProperty").put("Identity", ownIdentity.getId()).put("Property", name).get());
-       }
-
-       /**
-        * Returns the trust for the given identity assigned to it by the given own
-        * identity.
-        *
-        * @param ownIdentity
-        *            The own identity
-        * @param identity
-        *            The identity to get the trust for
-        * @return The trust for the given identity
-        * @throws PluginException
-        *             if an error occured talking to the Web of Trust plugin
-        */
-       public Trust getTrust(OwnIdentity ownIdentity, Identity identity) throws PluginException {
-               Reply getTrustReply = performRequest(SimpleFieldSetConstructor.create().put("Message", "GetIdentity").put("Truster", ownIdentity.getId()).put("Identity", identity.getId()).get());
-               String trust = getTrustReply.getFields().get("Trust");
-               String score = getTrustReply.getFields().get("Score");
-               String rank = getTrustReply.getFields().get("Rank");
-               Integer explicit = null;
-               Integer implicit = null;
-               Integer distance = null;
-               try {
-                       explicit = Integer.valueOf(trust);
-               } catch (NumberFormatException nfe1) {
-                       /* ignore. */
-               }
-               try {
-                       implicit = Integer.valueOf(score);
-                       distance = Integer.valueOf(rank);
-               } catch (NumberFormatException nfe1) {
-                       /* ignore. */
-               }
-               return new Trust(explicit, implicit, distance);
-       }
-
-       /**
-        * Sets the trust for the given identity.
-        *
-        * @param ownIdentity
-        *            The trusting identity
-        * @param identity
-        *            The trusted identity
-        * @param trust
-        *            The amount of trust (-100 thru 100)
-        * @param comment
-        *            The comment or explanation of the trust value
-        * @throws PluginException
-        *             if an error occured talking to the Web of Trust plugin
-        */
-       public void setTrust(OwnIdentity ownIdentity, Identity identity, int trust, String comment) throws PluginException {
-               performRequest(SimpleFieldSetConstructor.create().put("Message", "SetTrust").put("Truster", ownIdentity.getId()).put("Trustee", identity.getId()).put("Value", String.valueOf(trust)).put("Comment", comment).get());
-       }
-
-       /**
-        * Removes any trust assignment of the given own identity for the given
-        * identity.
-        *
-        * @param ownIdentity
-        *            The own identity
-        * @param identity
-        *            The identity to remove all trust for
-        * @throws WebOfTrustException
-        *             if an error occurs
-        */
-       public void removeTrust(OwnIdentity ownIdentity, Identity identity) throws WebOfTrustException {
-               performRequest(SimpleFieldSetConstructor.create().put("Message", "RemoveTrust").put("Truster", ownIdentity.getId()).put("Trustee", identity.getId()).get());
-       }
-
-       /**
-        * Pings the Web of Trust plugin. If the plugin can not be reached, a
-        * {@link PluginException} is thrown.
-        *
-        * @throws PluginException
-        *             if the plugin is not loaded
-        */
-       public void ping() throws PluginException {
-               performRequest(SimpleFieldSetConstructor.create().put("Message", "Ping").get());
-       }
-
-       //
-       // PRIVATE ACTIONS
-       //
-
-       /**
-        * Parses the contexts from the given fields.
-        *
-        * @param prefix
-        *            The prefix to use to access the contexts
-        * @param fields
-        *            The fields to parse the contexts from
-        * @return The parsed contexts
-        */
-       private static Set<String> parseContexts(String prefix, SimpleFieldSet fields) {
-               Set<String> contexts = new HashSet<>();
-               int contextCounter = -1;
-               while (true) {
-                       String context = fields.get(prefix + "Context" + ++contextCounter);
-                       if (context == null) {
-                               break;
-                       }
-                       contexts.add(context);
-               }
-               return contexts;
-       }
-
-       /**
-        * Parses the properties from the given fields.
-        *
-        * @param prefix
-        *            The prefix to use to access the properties
-        * @param fields
-        *            The fields to parse the properties from
-        * @return The parsed properties
-        */
-       private static Map<String, String> parseProperties(String prefix, SimpleFieldSet fields) {
-               Map<String, String> properties = new HashMap<>();
-               int propertiesCounter = -1;
-               while (true) {
-                       String propertyName = fields.get(prefix + "Property" + ++propertiesCounter + ".Name");
-                       if (propertyName == null) {
-                               break;
-                       }
-                       String propertyValue = fields.get(prefix + "Property" + propertiesCounter + ".Value");
-                       properties.put(propertyName, propertyValue);
-               }
-               return properties;
-       }
-
-       /**
-        * Sends a request containing the given fields and waits for the target
-        * message.
-        *
-        * @param fields
-        *            The fields of the message
-        * @return The reply message
-        * @throws PluginException
-        *             if the request could not be sent
-        */
-       private Reply performRequest(SimpleFieldSet fields) throws PluginException {
-               return performRequest(fields, null);
-       }
-
-       /**
-        * Sends a request containing the given fields and waits for the target
-        * message.
-        *
-        * @param fields
-        *            The fields of the message
-        * @param data
-        *            The payload of the message
-        * @return The reply message
-        * @throws PluginException
-        *             if the request could not be sent
-        */
-       private Reply performRequest(SimpleFieldSet fields, Bucket data) throws PluginException {
-               String identifier = "FCP-Command-" + System.currentTimeMillis() + "-" + counter.getAndIncrement();
-               Reply reply = new Reply();
-               PluginIdentifier pluginIdentifier = new PluginIdentifier(WOT_PLUGIN_NAME, identifier);
-               replies.put(pluginIdentifier, reply);
-
-               logger.log(Level.FINE, String.format("Sending FCP Request: %s", fields.get("Message")));
-               synchronized (reply) {
-                       try {
-                               pluginConnector.sendRequest(WOT_PLUGIN_NAME, identifier, fields, data);
-                               while (reply.getFields() == null) {
-                                       try {
-                                               reply.wait();
-                                       } catch (InterruptedException ie1) {
-                                               logger.log(Level.WARNING, String.format("Got interrupted while waiting for reply on %s.", fields.get("Message")), ie1);
-                                       }
-                               }
-                       } finally {
-                               replies.remove(pluginIdentifier);
-                       }
-               }
-               logger.log(Level.FINEST, String.format("Received FCP Response for %s: %s", fields.get("Message"), (reply.getFields() != null) ? reply.getFields().get("Message") : null));
-               if ((reply.getFields() == null) || "Error".equals(reply.getFields().get("Message"))) {
-                       throw new PluginException("Could not perform request for " + fields.get("Message"));
-               }
-               return reply;
-       }
-
-       /**
-        * Notifies the connector that a plugin reply was received.
-        *
-        * @param receivedReplyEvent
-        *            The event
-        */
-       @Subscribe
-       public void receivedReply(ReceivedReplyEvent receivedReplyEvent) {
-               PluginIdentifier pluginIdentifier = new PluginIdentifier(receivedReplyEvent.pluginName(), receivedReplyEvent.identifier());
-               Reply reply = replies.remove(pluginIdentifier);
-               if (reply == null) {
-                       return;
-               }
-               logger.log(Level.FINEST, String.format("Received Reply from Plugin: %s", receivedReplyEvent.fieldSet().get("Message")));
-               synchronized (reply) {
-                       reply.setFields(receivedReplyEvent.fieldSet());
-                       reply.setData(receivedReplyEvent.data());
-                       reply.notify();
-               }
-       }
-
-       /**
-        * Container for the data of the reply from a plugin.
-        */
-       private static class Reply {
-
-               /** The fields of the reply. */
-               private SimpleFieldSet fields;
-
-               /** The payload of the reply. */
-               private Bucket data;
-
-               /** Empty constructor. */
-               public Reply() {
-                       /* do nothing. */
-               }
-
-               /**
-                * Returns the fields of the reply.
-                *
-                * @return The fields of the reply
-                */
-               public SimpleFieldSet getFields() {
-                       return fields;
-               }
-
-               /**
-                * Sets the fields of the reply.
-                *
-                * @param fields
-                *            The fields of the reply
-                */
-               public void setFields(SimpleFieldSet fields) {
-                       this.fields = fields;
-               }
-
-               /**
-                * Returns the payload of the reply.
-                *
-                * @return The payload of the reply (may be {@code null})
-                */
-               @SuppressWarnings("unused")
-               public Bucket getData() {
-                       return data;
-               }
-
-               /**
-                * Sets the payload of the reply.
-                *
-                * @param data
-                *            The payload of the reply (may be {@code null})
-                */
-               public void setData(Bucket data) {
-                       this.data = data;
-               }
-
-       }
-
-       /**
-        * Helper method to create {@link SimpleFieldSet}s with terser code.
-        */
-       private static class SimpleFieldSetConstructor {
-
-               /** The field set being created. */
-               private SimpleFieldSet simpleFieldSet;
-
-               /**
-                * Creates a new simple field set constructor.
-                *
-                * @param shortLived
-                *            {@code true} if the resulting simple field set should be
-                *            short-lived, {@code false} otherwise
-                */
-               private SimpleFieldSetConstructor(boolean shortLived) {
-                       simpleFieldSet = new SimpleFieldSet(shortLived);
-               }
-
-               //
-               // ACCESSORS
-               //
-
-               /**
-                * Returns the created simple field set.
-                *
-                * @return The created simple field set
-                */
-               public SimpleFieldSet get() {
-                       return simpleFieldSet;
-               }
-
-               /**
-                * Sets the field with the given name to the given value.
-                *
-                * @param name
-                *            The name of the fleld
-                * @param value
-                *            The value of the field
-                * @return This constructor (for method chaining)
-                */
-               public SimpleFieldSetConstructor put(String name, String value) {
-                       simpleFieldSet.putOverwrite(name, value);
-                       return this;
-               }
-
-               //
-               // ACTIONS
-               //
-
-               /**
-                * Creates a new simple field set constructor.
-                *
-                * @return The created simple field set constructor
-                */
-               public static SimpleFieldSetConstructor create() {
-                       return create(true);
-               }
-
-               /**
-                * Creates a new simple field set constructor.
-                *
-                * @param shortLived
-                *            {@code true} if the resulting simple field set should be
-                *            short-lived, {@code false} otherwise
-                * @return The created simple field set constructor
-                */
-               public static SimpleFieldSetConstructor create(boolean shortLived) {
-                       return new SimpleFieldSetConstructor(shortLived);
-               }
-
-       }
-
-       /**
-        * Container for identifying plugins. Plugins are identified by their plugin
-        * name and their unique identifier.
-        */
-       private static class PluginIdentifier {
-
-               /** The plugin name. */
-               private final String pluginName;
-
-               /** The plugin identifier. */
-               private final String identifier;
-
-               /**
-                * Creates a new plugin identifier.
-                *
-                * @param pluginName
-                *            The name of the plugin
-                * @param identifier
-                *            The identifier of the plugin
-                */
-               public PluginIdentifier(String pluginName, String identifier) {
-                       this.pluginName = pluginName;
-                       this.identifier = identifier;
-               }
-
-               //
-               // OBJECT METHODS
-               //
-
-               /**
-                * {@inheritDoc}
-                */
-               @Override
-               public int hashCode() {
-                       return pluginName.hashCode() ^ identifier.hashCode();
-               }
-
-               /**
-                * {@inheritDoc}
-                */
-               @Override
-               public boolean equals(Object object) {
-                       if (!(object instanceof PluginIdentifier)) {
-                               return false;
-                       }
-                       PluginIdentifier pluginIdentifier = (PluginIdentifier) object;
-                       return pluginName.equals(pluginIdentifier.pluginName) && identifier.equals(pluginIdentifier.identifier);
-               }
-
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/freenet/wot/WebOfTrustException.java b/src/main/java/net/pterodactylus/sone/freenet/wot/WebOfTrustException.java
deleted file mode 100644 (file)
index 954fe04..0000000
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- * Sone - WebOfTrustException.java - Copyright © 2010–2019 David Roden
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.sone.freenet.wot;
-
-/**
- * Exception that signals an error processing web of trust identities, mostly
- * when communicating with the web of trust plugin.
- */
-public class WebOfTrustException extends Exception {
-
-       /**
-        * Creates a new web of trust exception.
-        */
-       public WebOfTrustException() {
-               super();
-       }
-
-       /**
-        * Creates a new web of trust exception.
-        *
-        * @param message
-        *            The message of the exception
-        */
-       public WebOfTrustException(String message) {
-               super(message);
-       }
-
-       /**
-        * Creates a new web of trust exception.
-        *
-        * @param cause
-        *            The cause of the exception
-        */
-       public WebOfTrustException(Throwable cause) {
-               super(cause);
-       }
-
-       /**
-        * Creates a new web of trust exception.
-        *
-        * @param message
-        *            The message of the exception
-        * @param cause
-        *            The cause of the exception
-        */
-       public WebOfTrustException(String message, Throwable cause) {
-               super(message, cause);
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/freenet/wot/event/IdentityAddedEvent.java b/src/main/java/net/pterodactylus/sone/freenet/wot/event/IdentityAddedEvent.java
deleted file mode 100644 (file)
index 2d7ab32..0000000
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * Sone - IdentityAddedEvent.java - Copyright © 2013–2019 David Roden
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.sone.freenet.wot.event;
-
-import net.pterodactylus.sone.freenet.wot.Identity;
-import net.pterodactylus.sone.freenet.wot.OwnIdentity;
-
-/**
- * Event that signals that an {@link Identity} was added.
- */
-public class IdentityAddedEvent extends IdentityEvent {
-
-       /**
-        * Creates a new “identity added” event.
-        *
-        * @param ownIdentity
-        *            The own identity that added the identity
-        * @param identity
-        *            The identity that was added
-        */
-       public IdentityAddedEvent(OwnIdentity ownIdentity, Identity identity) {
-               super(ownIdentity, identity);
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/freenet/wot/event/IdentityEvent.java b/src/main/java/net/pterodactylus/sone/freenet/wot/event/IdentityEvent.java
deleted file mode 100644 (file)
index 8a23ae1..0000000
+++ /dev/null
@@ -1,83 +0,0 @@
-/*
- * Sone - IdentityEvent.java - Copyright © 2013–2019 David Roden
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.sone.freenet.wot.event;
-
-import net.pterodactylus.sone.freenet.wot.Identity;
-import net.pterodactylus.sone.freenet.wot.OwnIdentity;
-
-/**
- * Base class for {@link Identity} events.
- */
-public abstract class IdentityEvent {
-
-       /** The own identity this event relates to. */
-       private final OwnIdentity ownIdentity;
-
-       /** The identity this event is about. */
-       private final Identity identity;
-
-       /**
-        * Creates a new identity-based event.
-        *
-        * @param ownIdentity
-        *            The own identity that relates to the identity
-        * @param identity
-        *            The identity this event is about
-        */
-       protected IdentityEvent(OwnIdentity ownIdentity, Identity identity) {
-               this.ownIdentity = ownIdentity;
-               this.identity = identity;
-       }
-
-       //
-       // ACCESSORS
-       //
-
-       /**
-        * Returns the own identity this event relates to.
-        *
-        * @return The own identity this event relates to
-        */
-       public OwnIdentity ownIdentity() {
-               return ownIdentity;
-       }
-
-       /**
-        * Returns the identity this event is about.
-        *
-        * @return The identity this event is about
-        */
-       public Identity identity() {
-               return identity;
-       }
-
-       @Override
-       public int hashCode() {
-               return ownIdentity().hashCode() ^ identity().hashCode();
-       }
-
-       @Override
-       public boolean equals(Object object) {
-               if ((object == null) || !object.getClass().equals(getClass())) {
-                       return false;
-               }
-               IdentityEvent identityEvent = (IdentityEvent) object;
-               return ownIdentity().equals(identityEvent.ownIdentity()) && identity().equals(identityEvent.identity());
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/freenet/wot/event/IdentityRemovedEvent.java b/src/main/java/net/pterodactylus/sone/freenet/wot/event/IdentityRemovedEvent.java
deleted file mode 100644 (file)
index 655015e..0000000
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * Sone - IdentityRemovedEvent.java - Copyright © 2013–2019 David Roden
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.sone.freenet.wot.event;
-
-import net.pterodactylus.sone.freenet.wot.Identity;
-import net.pterodactylus.sone.freenet.wot.OwnIdentity;
-
-/**
- * Event that signals that an {@link Identity} was removed.
- */
-public class IdentityRemovedEvent extends IdentityEvent {
-
-       /**
-        * Creates a new “identity removed” event.
-        *
-        * @param ownIdentity
-        *            The own identity that removed the identity
-        * @param identity
-        *            The identity that was removed
-        */
-       public IdentityRemovedEvent(OwnIdentity ownIdentity, Identity identity) {
-               super(ownIdentity, identity);
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/freenet/wot/event/IdentityUpdatedEvent.java b/src/main/java/net/pterodactylus/sone/freenet/wot/event/IdentityUpdatedEvent.java
deleted file mode 100644 (file)
index a71ad5b..0000000
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * Sone - IdentityUpdatedEvent.java - Copyright © 2013–2019 David Roden
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.sone.freenet.wot.event;
-
-import net.pterodactylus.sone.freenet.wot.Identity;
-import net.pterodactylus.sone.freenet.wot.OwnIdentity;
-
-/**
- * Event that signals that an {@link Identity} was updated.
- */
-public class IdentityUpdatedEvent extends IdentityEvent {
-
-       /**
-        * Creates a new “identity updated” event.
-        *
-        * @param ownIdentity
-        *            The own identity that tracks the identity
-        * @param identity
-        *            The identity that was updated
-        */
-       public IdentityUpdatedEvent(OwnIdentity ownIdentity, Identity identity) {
-               super(ownIdentity, identity);
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/freenet/wot/event/OwnIdentityAddedEvent.java b/src/main/java/net/pterodactylus/sone/freenet/wot/event/OwnIdentityAddedEvent.java
deleted file mode 100644 (file)
index fc34848..0000000
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * Sone - OwnIdentityAddedEvent.java - Copyright © 2013–2019 David Roden
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.sone.freenet.wot.event;
-
-import net.pterodactylus.sone.freenet.wot.OwnIdentity;
-
-/**
- * Event that signals that an {@link OwnIdentity} was added.
- */
-public class OwnIdentityAddedEvent extends OwnIdentityEvent {
-
-       /**
-        * Creates new “own identity added” event.
-        *
-        * @param ownIdentity
-        *            The own identity that was added
-        */
-       public OwnIdentityAddedEvent(OwnIdentity ownIdentity) {
-               super(ownIdentity);
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/freenet/wot/event/OwnIdentityEvent.java b/src/main/java/net/pterodactylus/sone/freenet/wot/event/OwnIdentityEvent.java
deleted file mode 100644 (file)
index 9e88d20..0000000
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- * Sone - OwnIdentityEvent.java - Copyright © 2013–2019 David Roden
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.sone.freenet.wot.event;
-
-import net.pterodactylus.sone.freenet.wot.OwnIdentity;
-
-/**
- * Base class for {@link OwnIdentity} events.
- */
-public abstract class OwnIdentityEvent {
-
-       /** The own identity this event is about. */
-       private final OwnIdentity ownIdentity;
-
-       /**
-        * Creates a new own identity-based event.
-        *
-        * @param ownIdentity
-        *            The own identity this event is about
-        */
-       protected OwnIdentityEvent(OwnIdentity ownIdentity) {
-               this.ownIdentity = ownIdentity;
-       }
-
-       //
-       // ACCESSORS
-       //
-
-       /**
-        * Returns the own identity this event is about.
-        *
-        * @return The own identity this event is about
-        */
-       public OwnIdentity ownIdentity() {
-               return ownIdentity;
-       }
-
-       @Override
-       public int hashCode() {
-               return ownIdentity().hashCode();
-       }
-
-       @Override
-       public boolean equals(Object object) {
-               if ((object == null) || !object.getClass().equals(getClass())) {
-                       return false;
-               }
-               OwnIdentityEvent ownIdentityEvent = (OwnIdentityEvent) object;
-               return ownIdentity().equals(ownIdentityEvent.ownIdentity());
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/freenet/wot/event/OwnIdentityRemovedEvent.java b/src/main/java/net/pterodactylus/sone/freenet/wot/event/OwnIdentityRemovedEvent.java
deleted file mode 100644 (file)
index 73761b0..0000000
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * Sone - OwnIdentityRemovedEvent.java - Copyright © 2013–2019 David Roden
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.sone.freenet.wot.event;
-
-import net.pterodactylus.sone.freenet.wot.OwnIdentity;
-
-/**
- * Event that signals that an {@link OwnIdentity} was removed.
- */
-public class OwnIdentityRemovedEvent extends OwnIdentityEvent {
-
-       /**
-        * Creates a new “own identity removed” event.
-        *
-        * @param ownIdentity
-        *            The own identity that was removed
-        */
-       public OwnIdentityRemovedEvent(OwnIdentity ownIdentity) {
-               super(ownIdentity);
-       }
-
-}
index c42b056..2e18347 100644 (file)
@@ -1,6 +1,7 @@
 package net.pterodactylus.sone.main;
 
 import java.io.File;
+import javax.annotation.Nonnull;
 
 import net.pterodactylus.sone.template.FilesystemTemplate;
 import net.pterodactylus.sone.web.pages.ReloadingPage;
@@ -21,16 +22,19 @@ public class DebugLoaders implements Loaders {
                this.filesystemPath = filesystemPath;
        }
 
+       @Nonnull
        @Override
-       public Template loadTemplate(String path) {
+       public Template loadTemplate(@Nonnull String path) {
                return new FilesystemTemplate(new File(filesystemPath, path).getAbsolutePath());
        }
 
+       @Nonnull
        @Override
-       public <REQ extends Request> Page<REQ> loadStaticPage(String basePath, String prefix, String mimeType) {
+       public <REQ extends Request> Page<REQ> loadStaticPage(@Nonnull String basePath, @Nonnull String prefix, @Nonnull String mimeType) {
                return new ReloadingPage<>(basePath, new File(filesystemPath, prefix).getAbsolutePath(), mimeType);
        }
 
+       @Nonnull
        @Override
        public TemplateProvider getTemplateProvider() {
                return new FilesystemTemplateProvider(new File(filesystemPath, "/templates/").getAbsolutePath());
index 72d8d19..5072970 100644 (file)
@@ -1,14 +1,10 @@
 package net.pterodactylus.sone.main;
 
-import static net.pterodactylus.util.template.TemplateParser.parse;
+import java.io.*;
 
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.Reader;
-import java.io.UnsupportedEncodingException;
+import javax.annotation.Nonnull;
 
 import net.pterodactylus.sone.web.WebInterface;
-import net.pterodactylus.util.io.Closer;
 import net.pterodactylus.util.template.ClassPathTemplateProvider;
 import net.pterodactylus.util.template.Template;
 import net.pterodactylus.util.template.TemplateProvider;
@@ -16,33 +12,32 @@ import net.pterodactylus.util.web.Page;
 import net.pterodactylus.util.web.Request;
 import net.pterodactylus.util.web.StaticPage;
 
+import static net.pterodactylus.util.template.TemplateParser.parse;
+
 /**
  * Default {@link Loaders} implementation that loads resources from the classpath.
  */
 public class DefaultLoaders implements Loaders {
 
+       @Nonnull
        @Override
-       public Template loadTemplate(String path) {
-               InputStream templateInputStream = null;
-               Reader reader = null;
-               try {
-                       templateInputStream = getClass().getResourceAsStream(path);
-                       reader = new InputStreamReader(templateInputStream, "UTF-8");
+       public Template loadTemplate(@Nonnull String path) {
+               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);
                }
        }
 
+       @Nonnull
        @Override
-       public <REQ extends Request> Page<REQ> loadStaticPage(String pathPrefix, String basePath, String mimeType) {
+       public <REQ extends Request> Page<REQ> loadStaticPage(@Nonnull String pathPrefix, @Nonnull String basePath, @Nonnull String mimeType) {
                return new StaticPage<REQ>(pathPrefix, basePath, mimeType) {
                };
        }
 
+       @Nonnull
        @Override
        public TemplateProvider getTemplateProvider() {
                return new ClassPathTemplateProvider(WebInterface.class, "/templates/");
index 8ee5132..b07118b 100644 (file)
@@ -1,5 +1,7 @@
 package net.pterodactylus.sone.main;
 
+import javax.annotation.Nonnull;
+
 import net.pterodactylus.util.template.Template;
 import net.pterodactylus.util.template.TemplateProvider;
 import net.pterodactylus.util.web.Page;
@@ -13,8 +15,8 @@ import com.google.inject.ImplementedBy;
 @ImplementedBy(DefaultLoaders.class)
 public interface Loaders {
 
-       Template loadTemplate(String path);
-       <REQ extends Request> Page<REQ> loadStaticPage(String basePath, String prefix, String mimeType);
-       TemplateProvider getTemplateProvider();
+       @Nonnull Template loadTemplate(@Nonnull String path);
+       @Nonnull <REQ extends Request> Page<REQ> loadStaticPage(@Nonnull String basePath, @Nonnull String prefix, @Nonnull String mimeType);
+       @Nonnull TemplateProvider getTemplateProvider();
 
 }
index ceb336b..82ab065 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - SonePlugin.java - Copyright © 2010–2019 David Roden
+ * Sone - SonePlugin.java - Copyright © 2010–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
 
 package net.pterodactylus.sone.main;
 
-import static com.google.common.base.Optional.of;
-import static java.util.logging.Logger.getLogger;
+import static java.util.logging.Logger.*;
 
-import java.io.File;
-import java.util.logging.Handler;
-import java.util.logging.Level;
-import java.util.logging.LogRecord;
 import java.util.logging.Logger;
-
-import javax.inject.Singleton;
-
-import net.pterodactylus.sone.core.Core;
-import net.pterodactylus.sone.database.Database;
-import net.pterodactylus.sone.database.PostProvider;
-import net.pterodactylus.sone.database.SoneProvider;
-import net.pterodactylus.sone.database.memory.MemoryDatabase;
-import net.pterodactylus.sone.fcp.FcpInterface;
-import net.pterodactylus.sone.freenet.PluginStoreConfigurationBackend;
-import net.pterodactylus.sone.freenet.wot.Context;
-import net.pterodactylus.sone.freenet.wot.WebOfTrustConnector;
-import net.pterodactylus.sone.web.WebInterface;
-import net.pterodactylus.sone.web.WebInterfaceModule;
-import net.pterodactylus.util.config.Configuration;
-import net.pterodactylus.util.config.ConfigurationException;
-import net.pterodactylus.util.config.MapConfigurationBackend;
-import net.pterodactylus.util.version.Version;
-
-import com.google.common.base.Optional;
-import com.google.common.cache.CacheBuilder;
-import com.google.common.cache.CacheLoader;
-import com.google.common.cache.LoadingCache;
-import com.google.common.eventbus.EventBus;
-import com.google.inject.AbstractModule;
-import com.google.inject.Guice;
-import com.google.inject.Injector;
-import com.google.inject.Module;
-import com.google.inject.TypeLiteral;
-import com.google.inject.matcher.Matchers;
-import com.google.inject.spi.InjectionListener;
-import com.google.inject.spi.TypeEncounter;
-import com.google.inject.spi.TypeListener;
-
-import freenet.client.async.PersistenceDisabledException;
-import freenet.l10n.BaseL10n;
-import freenet.l10n.BaseL10n.LANGUAGE;
-import freenet.l10n.PluginL10n;
-import freenet.pluginmanager.FredPlugin;
-import freenet.pluginmanager.FredPluginBaseL10n;
-import freenet.pluginmanager.FredPluginFCP;
-import freenet.pluginmanager.FredPluginL10n;
-import freenet.pluginmanager.FredPluginThreadless;
-import freenet.pluginmanager.FredPluginVersioned;
-import freenet.pluginmanager.PluginReplySender;
-import freenet.pluginmanager.PluginRespirator;
-import freenet.support.SimpleFieldSet;
-import freenet.support.api.Bucket;
+import java.util.logging.*;
+
+import javax.annotation.Nonnull;
+
+import net.pterodactylus.sone.core.*;
+import net.pterodactylus.sone.core.event.*;
+import net.pterodactylus.sone.fcp.*;
+import net.pterodactylus.sone.freenet.wot.*;
+import net.pterodactylus.sone.web.*;
+import net.pterodactylus.sone.web.notification.NotificationHandler;
+import net.pterodactylus.sone.web.notification.NotificationHandlerModule;
+
+import freenet.l10n.BaseL10n.*;
+import freenet.l10n.*;
+import freenet.pluginmanager.*;
+import freenet.support.*;
+import freenet.support.api.*;
+
+import com.google.common.annotations.*;
+import com.google.common.eventbus.*;
+import com.google.common.cache.*;
+import com.google.inject.*;
+import com.google.inject.name.*;
+import kotlin.jvm.functions.*;
 
 /**
  * This class interfaces with Freenet. It is the class that is loaded by the
@@ -85,12 +56,13 @@ public class SonePlugin implements FredPlugin, FredPluginFCP, FredPluginL10n, Fr
        static {
                /* initialize logging. */
                soneLogger.setUseParentHandlers(false);
+               soneLogger.setLevel(Level.ALL);
                soneLogger.addHandler(new Handler() {
                        private final LoadingCache<String, Class<?>> classCache = CacheBuilder.newBuilder()
                                        .build(new CacheLoader<String, Class<?>>() {
                                                @Override
-                                               public Class<?> load(String key) throws Exception {
-                                                       return Class.forName(key);
+                                               public Class<?> load(@Nonnull String key) throws Exception {
+                                                       return SonePlugin.class.getClassLoader().loadClass(key);
                                                }
                                        });
 
@@ -122,19 +94,24 @@ public class SonePlugin implements FredPlugin, FredPluginFCP, FredPluginL10n, Fr
        }
 
        /** The current year at time of release. */
-       private static final int YEAR = 2019;
+       private static final int YEAR = 2020;
        private static final String SONE_HOMEPAGE = "USK@nwa8lHa271k2QvJ8aa0Ov7IHAV-DFOCFgmDt3X6BpCI,DuQSUZiI~agF8c-6tjsFFGuZ8eICrzWCILB60nT8KKo,AQACAAE/sone/";
-       private static final int LATEST_EDITION = 79;
+       private static final int LATEST_EDITION = 81;
 
        /** The logger. */
        private static final Logger logger = getLogger(SonePlugin.class.getName());
 
+       private final Function1<Module[], Injector> injectorCreator;
+
        /** The plugin respirator. */
        private PluginRespirator pluginRespirator;
 
        /** The core. */
        private Core core;
 
+       /** The event bus. */
+       private EventBus eventBus;
+
        /** The web interface. */
        private WebInterface webInterface;
 
@@ -147,6 +124,15 @@ public class SonePlugin implements FredPlugin, FredPluginFCP, FredPluginL10n, Fr
        /** The web of trust connector. */
        private WebOfTrustConnector webOfTrustConnector;
 
+       public SonePlugin() {
+               this(Guice::createInjector);
+       }
+
+       @VisibleForTesting
+       public SonePlugin(Function1<Module[], Injector> injectorCreator) {
+               this.injectorCreator = injectorCreator;
+       }
+
        //
        // ACCESSORS
        //
@@ -206,92 +192,7 @@ public class SonePlugin implements FredPlugin, FredPluginFCP, FredPluginL10n, Fr
        public void runPlugin(PluginRespirator pluginRespirator) {
                this.pluginRespirator = pluginRespirator;
 
-               /* create a configuration. */
-               Configuration oldConfiguration;
-               Configuration newConfiguration = null;
-               boolean firstStart = !new File("sone.properties").exists();
-               boolean newConfig = false;
-               try {
-                       oldConfiguration = new Configuration(new MapConfigurationBackend(new File("sone.properties"), false));
-                       newConfiguration = oldConfiguration;
-               } catch (ConfigurationException ce1) {
-                       newConfig = true;
-                       logger.log(Level.INFO, "Could not load configuration file, trying plugin store…", ce1);
-                       try {
-                               newConfiguration = new Configuration(new MapConfigurationBackend(new File("sone.properties"), true));
-                               logger.log(Level.INFO, "Created new configuration file.");
-                       } catch (ConfigurationException ce2) {
-                               logger.log(Level.SEVERE, "Could not create configuration file, using Plugin Store!", ce2);
-                       }
-                       try {
-                               oldConfiguration = new Configuration(new PluginStoreConfigurationBackend(pluginRespirator));
-                               logger.log(Level.INFO, "Plugin store loaded.");
-                       } catch (PersistenceDisabledException pde1) {
-                               logger.log(Level.SEVERE, "Could not load any configuration, using empty configuration!");
-                               oldConfiguration = new Configuration(new MapConfigurationBackend());
-                       }
-               }
-
-               final Configuration startConfiguration;
-               if ((newConfiguration != null) && (oldConfiguration != newConfiguration)) {
-                       logger.log(Level.INFO, "Setting configuration to file-based configuration.");
-                       startConfiguration = newConfiguration;
-               } else {
-                       startConfiguration = oldConfiguration;
-               }
-               final EventBus eventBus = new EventBus();
-
-               /* Freenet injector configuration. */
-               FreenetModule freenetModule =  new FreenetModule(pluginRespirator);
-
-               /* Sone injector configuration. */
-               AbstractModule soneModule = new AbstractModule() {
-
-                       @Override
-                       protected void configure() {
-                               bind(EventBus.class).toInstance(eventBus);
-                               bind(Configuration.class).toInstance(startConfiguration);
-                               Context context = new Context("Sone");
-                               bind(Context.class).toInstance(context);
-                               bind(getOptionalContextTypeLiteral()).toInstance(of(context));
-                               bind(SonePlugin.class).toInstance(SonePlugin.this);
-                               bind(Version.class).toInstance(Version.parse(getVersion().substring(1)));
-                               bind(PluginVersion.class).toInstance(new PluginVersion(getVersion()));
-                               bind(PluginYear.class).toInstance(new PluginYear(getYear()));
-                               bind(PluginHomepage.class).toInstance(new PluginHomepage(getHomepage()));
-                               bind(Database.class).to(MemoryDatabase.class).in(Singleton.class);
-                               bind(BaseL10n.class).toInstance(l10n.getBase());
-                               bind(SoneProvider.class).to(Core.class).in(Singleton.class);
-                               bind(PostProvider.class).to(Core.class).in(Singleton.class);
-                               if (startConfiguration.getBooleanValue("Developer.LoadFromFilesystem").getValue(false)) {
-                                       String path = startConfiguration.getStringValue("Developer.FilesystemPath").getValue(null);
-                                       if (path != null) {
-                                               bind(Loaders.class).toInstance(new DebugLoaders(path));
-                                       }
-                               }
-                               bindListener(Matchers.any(), new TypeListener() {
-
-                                       @Override
-                                       public <I> void hear(TypeLiteral<I> typeLiteral, TypeEncounter<I> typeEncounter) {
-                                               typeEncounter.register(new InjectionListener<I>() {
-
-                                                       @Override
-                                                       public void afterInjection(I injectee) {
-                                                               eventBus.register(injectee);
-                                                       }
-                                               });
-                                       }
-                               });
-                       }
-
-                       private TypeLiteral<Optional<Context>> getOptionalContextTypeLiteral() {
-                               return new TypeLiteral<Optional<Context>>() {
-                               };
-                       }
-
-               };
-               Module webInterfaceModule = new WebInterfaceModule();
-               Injector injector = Guice.createInjector(freenetModule, soneModule, webInterfaceModule);
+               Injector injector = createInjector();
                core = injector.getInstance(Core.class);
 
                /* create web of trust connector. */
@@ -303,11 +204,47 @@ public class SonePlugin implements FredPlugin, FredPluginFCP, FredPluginL10n, Fr
                /* create the web interface. */
                webInterface = injector.getInstance(WebInterface.class);
 
+               /* we need to request this to install all notification handlers. */
+               injector.getInstance(NotificationHandler.class);
+
+               /* and this is required to shutdown all tickers. */
+               injector.getInstance(TickerShutdown.class);
+
                /* start core! */
                core.start();
+
+               /* start the web interface! */
                webInterface.start();
-               webInterface.setFirstStart(firstStart);
-               webInterface.setNewConfig(newConfig);
+
+               /* send some events on startup */
+               eventBus = injector.getInstance(EventBus.class);
+
+               /* first start? */
+               if (injector.getInstance(Key.get(Boolean.class, Names.named("FirstStart")))) {
+                       eventBus.post(new FirstStart());
+               } else {
+                       /* new config? */
+                       if (injector.getInstance(Key.get(Boolean.class, Names.named("NewConfig")))) {
+                               eventBus.post(new ConfigNotRead());
+                       }
+               }
+
+               eventBus.post(new Startup());
+       }
+
+       @VisibleForTesting
+       protected Injector createInjector() {
+               FreenetModule freenetModule = new FreenetModule(pluginRespirator);
+               AbstractModule soneModule = new SoneModule(this, new EventBus());
+               Module webInterfaceModule = new WebInterfaceModule();
+               Module notificationHandlerModule = new NotificationHandlerModule();
+
+               return createInjector(freenetModule, soneModule, webInterfaceModule, notificationHandlerModule);
+       }
+
+       @VisibleForTesting
+       protected Injector createInjector(Module... modules) {
+               return injectorCreator.invoke(modules);
        }
 
        /**
@@ -315,6 +252,9 @@ public class SonePlugin implements FredPlugin, FredPluginFCP, FredPluginL10n, Fr
         */
        @Override
        public void terminate() {
+               /* send shutdown event. */
+               eventBus.post(new Shutdown());
+
                try {
                        /* stop the web interface. */
                        webInterface.stop();
diff --git a/src/main/java/net/pterodactylus/sone/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)
diff --git a/src/main/java/net/pterodactylus/sone/notify/ListNotification.java b/src/main/java/net/pterodactylus/sone/notify/ListNotification.java
deleted file mode 100644 (file)
index 6a7b086..0000000
+++ /dev/null
@@ -1,199 +0,0 @@
-/*
- * Sone - ListNotification.java - Copyright © 2010–2019 David Roden
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.sone.notify;
-
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.List;
-import java.util.concurrent.CopyOnWriteArrayList;
-
-import net.pterodactylus.util.notify.TemplateNotification;
-import net.pterodactylus.util.template.Template;
-
-/**
- * Notification that maintains a list of new elements.
- *
- * @param <T>
- *            The type of the items
- */
-public class ListNotification<T> extends TemplateNotification {
-
-       /** The key under which to store the elements in the template. */
-       private final String key;
-
-       /** The list of new elements. */
-       private final List<T> elements = new CopyOnWriteArrayList<>();
-
-       /**
-        * Creates a new list notification.
-        *
-        * @param id
-        *            The ID of the notification
-        * @param key
-        *            The key under which to store the elements in the template
-        * @param template
-        *            The template to render
-        */
-       public ListNotification(String id, String key, Template template) {
-               this(id, key, template, true);
-       }
-
-       /**
-        * Creates a new list notification.
-        *
-        * @param id
-        *            The ID of the notification
-        * @param key
-        *            The key under which to store the elements in the template
-        * @param template
-        *            The template to render
-        * @param dismissable
-        *            {@code true} if this notification should be dismissable by the
-        *            user, {@code false} otherwise
-        */
-       public ListNotification(String id, String key, Template template, boolean dismissable) {
-               super(id, System.currentTimeMillis(), System.currentTimeMillis(), dismissable, template);
-               this.key = key;
-               template.getInitialContext().set(key, elements);
-       }
-
-       /**
-        * Creates a new list notification that copies its ID and the template from
-        * the given list notification.
-        *
-        * @param listNotification
-        *            The list notification to copy
-        */
-       public ListNotification(ListNotification<T> listNotification) {
-               super(listNotification.getId(), listNotification.getCreatedTime(), listNotification.getLastUpdatedTime(), listNotification.isDismissable(), new Template());
-               this.key = listNotification.key;
-               getTemplate().add(listNotification.getTemplate());
-               getTemplate().getInitialContext().set(key, elements);
-       }
-
-       //
-       // ACTIONS
-       //
-
-       /**
-        * Returns the current list of elements.
-        *
-        * @return The current list of elements
-        */
-       public List<T> getElements() {
-               return new ArrayList<>(elements);
-       }
-
-       /**
-        * Sets the elements to show in this notification. This method will not call
-        * {@link #touch()}.
-        *
-        * @param elements
-        *            The elements to show
-        */
-       public void setElements(Collection<? extends T> elements) {
-               this.elements.clear();
-               this.elements.addAll(elements);
-               touch();
-       }
-
-       /**
-        * Returns whether there are any new elements.
-        *
-        * @return {@code true} if there are no new elements, {@code false} if there
-        *         are new elements
-        */
-       public boolean isEmpty() {
-               return elements.isEmpty();
-       }
-
-       /**
-        * Adds a discovered element.
-        *
-        * @param element
-        *            The new element
-        */
-       public void add(T element) {
-               elements.add(element);
-               touch();
-       }
-
-       /**
-        * Removes the given element from the list of new elements.
-        *
-        * @param element
-        *            The element to remove
-        */
-       public void remove(T element) {
-               while (elements.remove(element)) {
-                       /* do nothing, just remove all instances of the element. */
-               }
-               if (elements.isEmpty()) {
-                       dismiss();
-               }
-               touch();
-       }
-
-       //
-       // ABSTRACTNOTIFICATION METHODS
-       //
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       public void dismiss() {
-               super.dismiss();
-               elements.clear();
-       }
-
-       //
-       // OBJECT METHODS
-       //
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       public int hashCode() {
-               int hashCode = super.hashCode();
-               for (T element : elements) {
-                       hashCode ^= element.hashCode();
-               }
-               return hashCode;
-       }
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       public boolean equals(Object object) {
-               if (!(object instanceof ListNotification)) {
-                       return false;
-               }
-               ListNotification<?> listNotification = (ListNotification<?>) object;
-               if (!super.equals(listNotification)) {
-                       return false;
-               }
-               if (!key.equals(listNotification.key)) {
-                       return false;
-               }
-               return elements.equals(listNotification.elements);
-       }
-
-}
index 50a1087..739907c 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - ListNotificationFilter.java - Copyright © 2010–2019 David Roden
+ * Sone - ListNotificationFilter.java - Copyright © 2010–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
index 19b600e..87378cc 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - AlbumAccessor.java - Copyright © 2011–2019 David Roden
+ * Sone - AlbumAccessor.java - Copyright © 2011–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
index 8ce97ad..f960b08 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - CollectionAccessor.java - Copyright © 2010–2019 David Roden
+ * Sone - CollectionAccessor.java - Copyright © 2010–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
 
 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 1ff4f04..6439c44 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - CssClassNameFilter.java - Copyright © 2010–2019 David Roden
+ * Sone - CssClassNameFilter.java - Copyright © 2010–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
index 172b4b2..2910ec8 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - GetPagePlugin.java - Copyright © 2010–2019 David Roden
+ * Sone - GetPagePlugin.java - Copyright © 2010–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
index 0e6fda0..a43bd70 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - HttpRequestAccessor.java - Copyright © 2011–2019 David Roden
+ * Sone - HttpRequestAccessor.java - Copyright © 2011–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
index ed139de..5b229dd 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - IdentityAccessor.java - Copyright © 2010–2019 David Roden
+ * Sone - IdentityAccessor.java - Copyright © 2010–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
index 78c3aeb..6feb94e 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - ImageAccessor.java - Copyright © 2011–2019 David Roden
+ * Sone - ImageAccessor.java - Copyright © 2011–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
index fc4b803..85a7562 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - ImageLinkFilter.java - Copyright © 2011–2019 David Roden
+ * Sone - ImageLinkFilter.java - Copyright © 2011–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
index 8dd2b04..a5f734d 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - JavascriptFilter.java - Copyright © 2011–2019 David Roden
+ * Sone - JavascriptFilter.java - Copyright © 2011–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
diff --git a/src/main/java/net/pterodactylus/sone/template/PostAccessor.java b/src/main/java/net/pterodactylus/sone/template/PostAccessor.java
deleted file mode 100644 (file)
index 4025c7b..0000000
+++ /dev/null
@@ -1,72 +0,0 @@
-/*
- * Sone - PostAccessor.java - Copyright © 2010–2019 David Roden
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.sone.template;
-
-import net.pterodactylus.sone.core.Core;
-import net.pterodactylus.sone.data.Post;
-import net.pterodactylus.sone.data.Reply;
-import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.util.template.ReflectionAccessor;
-import net.pterodactylus.util.template.TemplateContext;
-
-import com.google.common.collect.Collections2;
-
-/**
- * Accessor for {@link Post} objects that adds additional properties:
- * <dl>
- * <dd>replies</dd>
- * <dt>All replies to this post, sorted by time, oldest first</dt>
- * </dl>
- */
-public class PostAccessor extends ReflectionAccessor {
-
-       /** The core to get the replies from. */
-       private final Core core;
-
-       /**
-        * Creates a new post accessor.
-        *
-        * @param core
-        *            The core to get the replies from
-        */
-       public PostAccessor(Core core) {
-               this.core = core;
-       }
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       public Object get(TemplateContext templateContext, Object object, String member) {
-               Post post = (Post) object;
-               if ("replies".equals(member)) {
-                       return Collections2.filter(core.getReplies(post.getId()), Reply.FUTURE_REPLY_FILTER);
-               } else if (member.equals("likes")) {
-                       return core.getLikes(post);
-               } else if (member.equals("liked")) {
-                       Sone currentSone = (Sone) templateContext.get("currentSone");
-                       return (currentSone != null) && (currentSone.isLikedPostId(post.getId()));
-               } else if (member.equals("new")) {
-                       return !post.isKnown();
-               } else if (member.equals("bookmarked")) {
-                       return core.isBookmarked(post);
-               }
-               return super.get(templateContext, object, member);
-       }
-
-}
index a152038..7dc1eb5 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - ProfileAccessor.java - Copyright © 2011–2019 David Roden
+ * Sone - ProfileAccessor.java - Copyright © 2011–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
index c6dea31..fe1ccd3 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - ReplyAccessor.java - Copyright © 2010–2019 David Roden
+ * Sone - ReplyAccessor.java - Copyright © 2010–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
index c093e6f..9623b62 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - ReplyGroupFilter.java - Copyright © 2010–2019 David Roden
+ * Sone - ReplyGroupFilter.java - Copyright © 2010–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
index d004dfc..16cb062 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - RequestChangeFilter.java - Copyright © 2010–2019 David Roden
+ * Sone - RequestChangeFilter.java - Copyright © 2010–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
index a8cbe57..b072dc9 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - SoneAccessor.java - Copyright © 2010–2019 David Roden
+ * Sone - SoneAccessor.java - Copyright © 2010–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
 
 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 355b6a9..2a2e526 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - SubstringFilter.java - Copyright © 2010–2019 David Roden
+ * Sone - SubstringFilter.java - Copyright © 2010–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
index 26a128b..6ccc897 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - TrustAccessor.java - Copyright © 2010–2019 David Roden
+ * Sone - TrustAccessor.java - Copyright © 2010–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
index 6fb584e..0e12448 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - UniqueElementFilter.java - Copyright © 2011–2019 David Roden
+ * Sone - UniqueElementFilter.java - Copyright © 2011–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
index 0d4872b..2d6c05c 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - UnknownDateFilter.java - Copyright © 2011–2019 David Roden
+ * Sone - UnknownDateFilter.java - Copyright © 2011–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -19,18 +19,18 @@ package net.pterodactylus.sone.template;
 
 import java.util.Map;
 
+import net.pterodactylus.sone.freenet.Translation;
 import net.pterodactylus.util.template.Filter;
 import net.pterodactylus.util.template.TemplateContext;
-import freenet.l10n.BaseL10n;
 
 /**
  * {@link Filter} implementation that replaces a {@link Long} with a value of
- * {@code 0} by a {@link String} from an {@link BaseL10n l10n handler}.
+ * {@code 0} by a {@link String} from a {@link Translation translation}.
  */
 public class UnknownDateFilter implements Filter {
 
-       /** The l10n handler. */
-       private BaseL10n l10nHandler;
+       /** The translation. */
+       private final Translation translation;
 
        /** The key for the text to show. */
        private final String unknownKey;
@@ -38,13 +38,11 @@ public class UnknownDateFilter implements Filter {
        /**
         * Creates a new unknown date filter.
         *
-        * @param l10nHandler
-        *            The l10n handler
-        * @param unknownKey
-        *            The key of the text to show
+        * @param translation The translation
+        * @param unknownKey  The key of the text to show
         */
-       public UnknownDateFilter(BaseL10n l10nHandler, String unknownKey) {
-               this.l10nHandler = l10nHandler;
+       public UnknownDateFilter(Translation translation, String unknownKey) {
+               this.translation = translation;
                this.unknownKey = unknownKey;
        }
 
@@ -55,7 +53,7 @@ public class UnknownDateFilter implements Filter {
        public Object format(TemplateContext templateContext, Object data, Map<String, Object> parameters) {
                if (data instanceof Long) {
                        if ((Long) data == 0) {
-                               return l10nHandler.getString(unknownKey);
+                               return translation.translate(unknownKey);
                        }
                }
                return data;
index 0daf30c..06eed37 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - Parser.java - Copyright © 2010–2019 David Roden
+ * Sone - Parser.java - Copyright © 2010–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
index 056fcc1..468b565 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - ParserContext.java - Copyright © 2010–2019 David Roden
+ * Sone - ParserContext.java - Copyright © 2010–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
index de957c5..fac2b81 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - PostPart.java - Copyright © 2011–2019 David Roden
+ * Sone - PostPart.java - Copyright © 2011–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
index 13a7a26..598765a 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - SoneTextParserContext.java - Copyright © 2011–2019 David Roden
+ * Sone - SoneTextParserContext.java - Copyright © 2011–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -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
index 0703f32..885843e 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - TextFilter.java - Copyright © 2011–2019 David Roden
+ * Sone - TextFilter.java - Copyright © 2011–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
diff --git a/src/main/java/net/pterodactylus/sone/utils/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 1fd7527..0000000
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- * Sone - IntegerRangePredicate.java - Copyright © 2013–2019 David Roden
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <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 9eccccf..0000000
+++ /dev/null
@@ -1,55 +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 distrustPage: DistrustPage
-       @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 trustPage: TrustPage
-       @Inject lateinit var unbookmarkPage: UnbookmarkPage
-       @Inject lateinit var unfollowSonePage: UnfollowSonePage
-       @Inject lateinit var unlikePage: UnlikePage
-       @Inject lateinit var unlockSonePage: UnlockSonePage
-       @Inject lateinit var untrustPage: UntrustPage
-       @Inject lateinit var uploadImagePage: UploadImagePage
-       @Inject lateinit var viewPostPage: ViewPostPage
-       @Inject lateinit var viewSonePage: ViewSonePage
-
-}
index 3d131c8..40e5708 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - WebInterface.java - Copyright © 2010–2019 David Roden
+ * Sone - WebInterface.java - Copyright © 2010–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -19,51 +19,23 @@ package net.pterodactylus.sone.web;
 
 import static com.google.common.collect.FluentIterable.from;
 import static java.util.logging.Logger.getLogger;
-import static net.pterodactylus.util.template.TemplateParser.parse;
 
-import java.io.StringReader;
 import java.util.Collection;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Map;
 import java.util.Set;
 import java.util.TimeZone;
-import java.util.UUID;
-import java.util.concurrent.Executors;
-import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.ScheduledFuture;
-import java.util.concurrent.TimeUnit;
 import java.util.logging.Logger;
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
+import javax.inject.Named;
 
 import net.pterodactylus.sone.core.Core;
 import net.pterodactylus.sone.core.ElementLoader;
-import net.pterodactylus.sone.core.event.ImageInsertAbortedEvent;
-import net.pterodactylus.sone.core.event.ImageInsertFailedEvent;
-import net.pterodactylus.sone.core.event.ImageInsertFinishedEvent;
-import net.pterodactylus.sone.core.event.ImageInsertStartedEvent;
-import net.pterodactylus.sone.core.event.MarkPostKnownEvent;
-import net.pterodactylus.sone.core.event.MarkPostReplyKnownEvent;
-import net.pterodactylus.sone.core.event.MarkSoneKnownEvent;
-import net.pterodactylus.sone.core.event.NewPostFoundEvent;
-import net.pterodactylus.sone.core.event.NewPostReplyFoundEvent;
-import net.pterodactylus.sone.core.event.NewSoneFoundEvent;
-import net.pterodactylus.sone.core.event.PostRemovedEvent;
-import net.pterodactylus.sone.core.event.PostReplyRemovedEvent;
-import net.pterodactylus.sone.core.event.SoneInsertAbortedEvent;
-import net.pterodactylus.sone.core.event.SoneInsertedEvent;
-import net.pterodactylus.sone.core.event.SoneInsertingEvent;
-import net.pterodactylus.sone.core.event.SoneLockedEvent;
-import net.pterodactylus.sone.core.event.SoneRemovedEvent;
-import net.pterodactylus.sone.core.event.SoneUnlockedEvent;
-import net.pterodactylus.sone.core.event.UpdateFoundEvent;
-import net.pterodactylus.sone.data.Image;
+import net.pterodactylus.sone.core.event.*;
 import net.pterodactylus.sone.data.Post;
 import net.pterodactylus.sone.data.PostReply;
 import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.freenet.L10nFilter;
+import net.pterodactylus.sone.freenet.Translation;
 import net.pterodactylus.sone.main.Loaders;
 import net.pterodactylus.sone.main.PluginHomepage;
 import net.pterodactylus.sone.main.PluginVersion;
@@ -77,9 +49,6 @@ import net.pterodactylus.sone.template.LinkedElementRenderFilter;
 import net.pterodactylus.sone.template.ParserFilter;
 import net.pterodactylus.sone.template.RenderFilter;
 import net.pterodactylus.sone.template.ShortenFilter;
-import net.pterodactylus.sone.text.Part;
-import net.pterodactylus.sone.text.SonePart;
-import net.pterodactylus.sone.text.SoneTextParser;
 import net.pterodactylus.sone.text.TimeTextConverter;
 import net.pterodactylus.sone.web.ajax.BookmarkAjaxPage;
 import net.pterodactylus.sone.web.ajax.CreatePostAjaxPage;
@@ -88,7 +57,6 @@ import net.pterodactylus.sone.web.ajax.DeletePostAjaxPage;
 import net.pterodactylus.sone.web.ajax.DeleteProfileFieldAjaxPage;
 import net.pterodactylus.sone.web.ajax.DeleteReplyAjaxPage;
 import net.pterodactylus.sone.web.ajax.DismissNotificationAjaxPage;
-import net.pterodactylus.sone.web.ajax.DistrustAjaxPage;
 import net.pterodactylus.sone.web.ajax.EditAlbumAjaxPage;
 import net.pterodactylus.sone.web.ajax.EditImageAjaxPage;
 import net.pterodactylus.sone.web.ajax.EditProfileFieldAjaxPage;
@@ -105,79 +73,26 @@ import net.pterodactylus.sone.web.ajax.LikeAjaxPage;
 import net.pterodactylus.sone.web.ajax.LockSoneAjaxPage;
 import net.pterodactylus.sone.web.ajax.MarkAsKnownAjaxPage;
 import net.pterodactylus.sone.web.ajax.MoveProfileFieldAjaxPage;
-import net.pterodactylus.sone.web.ajax.TrustAjaxPage;
 import net.pterodactylus.sone.web.ajax.UnbookmarkAjaxPage;
 import net.pterodactylus.sone.web.ajax.UnfollowSoneAjaxPage;
 import net.pterodactylus.sone.web.ajax.UnlikeAjaxPage;
 import net.pterodactylus.sone.web.ajax.UnlockSoneAjaxPage;
-import net.pterodactylus.sone.web.ajax.UntrustAjaxPage;
 import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.sone.web.page.TemplateRenderer;
-import net.pterodactylus.sone.web.pages.AboutPage;
-import net.pterodactylus.sone.web.pages.BookmarkPage;
-import net.pterodactylus.sone.web.pages.BookmarksPage;
-import net.pterodactylus.sone.web.pages.CreateAlbumPage;
-import net.pterodactylus.sone.web.pages.CreatePostPage;
-import net.pterodactylus.sone.web.pages.CreateReplyPage;
-import net.pterodactylus.sone.web.pages.CreateSonePage;
-import net.pterodactylus.sone.web.pages.DeleteAlbumPage;
-import net.pterodactylus.sone.web.pages.DeleteImagePage;
-import net.pterodactylus.sone.web.pages.DeletePostPage;
-import net.pterodactylus.sone.web.pages.DeleteProfileFieldPage;
-import net.pterodactylus.sone.web.pages.DeleteReplyPage;
-import net.pterodactylus.sone.web.pages.DeleteSonePage;
-import net.pterodactylus.sone.web.pages.DismissNotificationPage;
-import net.pterodactylus.sone.web.pages.DistrustPage;
-import net.pterodactylus.sone.web.pages.EditAlbumPage;
-import net.pterodactylus.sone.web.pages.EditImagePage;
-import net.pterodactylus.sone.web.pages.EditProfileFieldPage;
-import net.pterodactylus.sone.web.pages.EditProfilePage;
-import net.pterodactylus.sone.web.pages.EmptyAlbumTitlePage;
-import net.pterodactylus.sone.web.pages.EmptyImageTitlePage;
-import net.pterodactylus.sone.web.pages.FollowSonePage;
-import net.pterodactylus.sone.web.pages.GetImagePage;
-import net.pterodactylus.sone.web.pages.ImageBrowserPage;
-import net.pterodactylus.sone.web.pages.IndexPage;
-import net.pterodactylus.sone.web.pages.InvalidPage;
-import net.pterodactylus.sone.web.pages.KnownSonesPage;
-import net.pterodactylus.sone.web.pages.LikePage;
-import net.pterodactylus.sone.web.pages.LockSonePage;
-import net.pterodactylus.sone.web.pages.LoginPage;
-import net.pterodactylus.sone.web.pages.LogoutPage;
-import net.pterodactylus.sone.web.pages.MarkAsKnownPage;
-import net.pterodactylus.sone.web.pages.NewPage;
-import net.pterodactylus.sone.web.pages.NoPermissionPage;
-import net.pterodactylus.sone.web.pages.OptionsPage;
-import net.pterodactylus.sone.web.pages.RescuePage;
-import net.pterodactylus.sone.web.pages.SearchPage;
-import net.pterodactylus.sone.web.pages.SoneTemplatePage;
-import net.pterodactylus.sone.web.pages.TrustPage;
-import net.pterodactylus.sone.web.pages.UnbookmarkPage;
-import net.pterodactylus.sone.web.pages.UnfollowSonePage;
-import net.pterodactylus.sone.web.pages.UnlikePage;
-import net.pterodactylus.sone.web.pages.UnlockSonePage;
-import net.pterodactylus.sone.web.pages.UntrustPage;
-import net.pterodactylus.sone.web.pages.UploadImagePage;
-import net.pterodactylus.sone.web.pages.ViewPostPage;
-import net.pterodactylus.sone.web.pages.ViewSonePage;
+import net.pterodactylus.sone.web.pages.*;
 import net.pterodactylus.util.notify.Notification;
 import net.pterodactylus.util.notify.NotificationManager;
-import net.pterodactylus.util.notify.TemplateNotification;
 import net.pterodactylus.util.template.Template;
 import net.pterodactylus.util.template.TemplateContextFactory;
 import net.pterodactylus.util.web.RedirectPage;
 import net.pterodactylus.util.web.TemplatePage;
 
-import freenet.clients.http.SessionManager;
-import freenet.clients.http.SessionManager.Session;
-import freenet.clients.http.ToadletContext;
-import freenet.l10n.BaseL10n;
-
+import com.codahale.metrics.*;
 import com.google.common.base.Optional;
-import com.google.common.collect.Collections2;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.eventbus.Subscribe;
 import com.google.inject.Inject;
+import freenet.clients.http.ToadletContext;
 
 /**
  * Bundles functionality that a web interface of a Freenet plugin needs, e.g.
@@ -192,7 +107,7 @@ public class WebInterface implements SessionProvider {
        private final Loaders loaders;
 
        /** The notification manager. */
-       private final NotificationManager notificationManager = new NotificationManager();
+       private final NotificationManager notificationManager;
 
        /** The Sone plugin. */
        private final SonePlugin sonePlugin;
@@ -204,9 +119,6 @@ public class WebInterface implements SessionProvider {
        private final TemplateContextFactory templateContextFactory;
        private final TemplateRenderer templateRenderer;
 
-       /** The Sone text parser. */
-       private final SoneTextParser soneTextParser;
-
        /** The parser filter. */
        private final ParserFilter parserFilter;
        private final ShortenFilter shortenFilter;
@@ -222,9 +134,9 @@ public class WebInterface implements SessionProvider {
        private final L10nFilter l10nFilter;
 
        private final PageToadletRegistry pageToadletRegistry;
-
-       /** The “new Sone” notification. */
-       private final ListNotification<Sone> newSoneNotification;
+       private final MetricRegistry metricRegistry;
+       private final Translation translation;
+       private final SessionProvider sessionProvider;
 
        /** The “new post” notification. */
        private final ListNotification<Post> newPostNotification;
@@ -238,33 +150,6 @@ public class WebInterface implements SessionProvider {
        /** The invisible “local reply” notification. */
        private final ListNotification<PostReply> localReplyNotification;
 
-       /** The “you have been mentioned” notification. */
-       private final ListNotification<Post> mentionNotification;
-
-       /** Notifications for sone inserts. */
-       private final Map<Sone, TemplateNotification> soneInsertNotifications = new HashMap<>();
-
-       /** Sone locked notification ticker objects. */
-       private final Map<Sone, ScheduledFuture<?>> lockedSonesTickerObjects = Collections.synchronizedMap(new HashMap<Sone, ScheduledFuture<?>>());
-
-       /** The “Sone locked” notification. */
-       private final ListNotification<Sone> lockedSonesNotification;
-
-       /** The “new version” notification. */
-       private final TemplateNotification newVersionNotification;
-
-       /** The “inserting images” notification. */
-       private final ListNotification<Image> insertingImagesNotification;
-
-       /** The “inserted images” notification. */
-       private final ListNotification<Image> insertedImagesNotification;
-
-       /** The “image insert failed” notification. */
-       private final ListNotification<Image> imageInsertFailedNotification;
-
-       /** Scheduled executor for time-based notifications. */
-       private final ScheduledExecutorService ticker = Executors.newScheduledThreadPool(1);
-
        @Inject
        public WebInterface(SonePlugin sonePlugin, Loaders loaders, ListNotificationFilter listNotificationFilter,
                        PostVisibilityFilter postVisibilityFilter, ReplyVisibilityFilter replyVisibilityFilter,
@@ -273,7 +158,12 @@ public class WebInterface implements SessionProvider {
                        ParserFilter parserFilter, ShortenFilter shortenFilter,
                        RenderFilter renderFilter,
                        LinkedElementRenderFilter linkedElementRenderFilter,
-                       PageToadletRegistry pageToadletRegistry) {
+                       PageToadletRegistry pageToadletRegistry, MetricRegistry metricRegistry, Translation translation, L10nFilter l10nFilter,
+                       NotificationManager notificationManager, SessionProvider sessionProvider,
+                       @Named("newRemotePost") ListNotification<Post> newPostNotification,
+                       @Named("newRemotePostReply") ListNotification<PostReply> newReplyNotification,
+                       @Named("localPost") ListNotification<Post> localPostNotification,
+                       @Named("localReply") ListNotification<PostReply> localReplyNotification) {
                this.sonePlugin = sonePlugin;
                this.loaders = loaders;
                this.listNotificationFilter = listNotificationFilter;
@@ -286,47 +176,20 @@ public class WebInterface implements SessionProvider {
                this.renderFilter = renderFilter;
                this.linkedElementRenderFilter = linkedElementRenderFilter;
                this.pageToadletRegistry = pageToadletRegistry;
+               this.metricRegistry = metricRegistry;
+               this.l10nFilter = l10nFilter;
+               this.translation = translation;
+               this.notificationManager = notificationManager;
+               this.sessionProvider = sessionProvider;
+               this.newPostNotification = newPostNotification;
+               this.newReplyNotification = newReplyNotification;
+               this.localPostNotification = localPostNotification;
+               this.localReplyNotification = localReplyNotification;
                formPassword = sonePlugin.pluginRespirator().getToadletContainer().getFormPassword();
-               soneTextParser = new SoneTextParser(getCore(), getCore());
-               l10nFilter = new L10nFilter(getL10n());
 
                this.templateContextFactory = templateContextFactory;
                templateContextFactory.addTemplateObject("webInterface", this);
                templateContextFactory.addTemplateObject("formPassword", formPassword);
-
-               /* create notifications. */
-               Template newSoneNotificationTemplate = loaders.loadTemplate("/templates/notify/newSoneNotification.html");
-               newSoneNotification = new ListNotification<>("new-sone-notification", "sones", newSoneNotificationTemplate, false);
-
-               Template newPostNotificationTemplate = loaders.loadTemplate("/templates/notify/newPostNotification.html");
-               newPostNotification = new ListNotification<>("new-post-notification", "posts", newPostNotificationTemplate, false);
-
-               Template localPostNotificationTemplate = loaders.loadTemplate("/templates/notify/newPostNotification.html");
-               localPostNotification = new ListNotification<>("local-post-notification", "posts", localPostNotificationTemplate, false);
-
-               Template newReplyNotificationTemplate = loaders.loadTemplate("/templates/notify/newReplyNotification.html");
-               newReplyNotification = new ListNotification<>("new-reply-notification", "replies", newReplyNotificationTemplate, false);
-
-               Template localReplyNotificationTemplate = loaders.loadTemplate("/templates/notify/newReplyNotification.html");
-               localReplyNotification = new ListNotification<>("local-reply-notification", "replies", localReplyNotificationTemplate, false);
-
-               Template mentionNotificationTemplate = loaders.loadTemplate("/templates/notify/mentionNotification.html");
-               mentionNotification = new ListNotification<>("mention-notification", "posts", mentionNotificationTemplate, false);
-
-               Template lockedSonesTemplate = loaders.loadTemplate("/templates/notify/lockedSonesNotification.html");
-               lockedSonesNotification = new ListNotification<>("sones-locked-notification", "sones", lockedSonesTemplate);
-
-               Template newVersionTemplate = loaders.loadTemplate("/templates/notify/newVersionNotification.html");
-               newVersionNotification = new TemplateNotification("new-version-notification", newVersionTemplate);
-
-               Template insertingImagesTemplate = loaders.loadTemplate("/templates/notify/inserting-images-notification.html");
-               insertingImagesNotification = new ListNotification<>("inserting-images-notification", "images", insertingImagesTemplate);
-
-               Template insertedImagesTemplate = loaders.loadTemplate("/templates/notify/inserted-images-notification.html");
-               insertedImagesNotification = new ListNotification<>("inserted-images-notification", "images", insertedImagesTemplate);
-
-               Template imageInsertFailedTemplate = loaders.loadTemplate("/templates/notify/image-insert-failed-notification.html");
-               imageInsertFailedNotification = new ListNotification<>("image-insert-failed-notification", "images", imageInsertFailedTemplate);
        }
 
        //
@@ -352,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);
        }
 
        /**
@@ -442,22 +245,8 @@ public class WebInterface implements SessionProvider {
                return listNotificationFilter.filterNotifications(notificationManager.getNotifications(), currentSone);
        }
 
-       /**
-        * Returns the l10n helper of the node.
-        *
-        * @return The node’s l10n helper
-        */
-       public BaseL10n getL10n() {
-               return sonePlugin.l10n().getBase();
-       }
-
-       /**
-        * Returns the session manager of the node.
-        *
-        * @return The node’s session manager
-        */
-       public SessionManager getSessionManager() {
-               return sonePlugin.pluginRespirator().getSessionManager("Sone");
+       public Translation getTranslation() {
+               return translation;
        }
 
        /**
@@ -469,16 +258,6 @@ public class WebInterface implements SessionProvider {
                return formPassword;
        }
 
-       /**
-        * Returns the posts that have been announced as new in the
-        * {@link #newPostNotification}.
-        *
-        * @return The new posts
-        */
-       public Set<Post> getNewPosts() {
-               return ImmutableSet.<Post> builder().addAll(newPostNotification.getElements()).addAll(localPostNotification.getElements()).build();
-       }
-
        @Nonnull
        public Collection<Post> getNewPosts(@Nullable Sone currentSone) {
                Set<Post> allNewPosts = ImmutableSet.<Post> builder()
@@ -488,16 +267,6 @@ public class WebInterface implements SessionProvider {
                return from(allNewPosts).filter(postVisibilityFilter.isVisible(currentSone)).toSet();
        }
 
-       /**
-        * Returns the replies that have been announced as new in the
-        * {@link #newReplyNotification}.
-        *
-        * @return The new replies
-        */
-       public Set<PostReply> getNewReplies() {
-               return ImmutableSet.<PostReply> builder().addAll(newReplyNotification.getElements()).addAll(localReplyNotification.getElements()).build();
-       }
-
        @Nonnull
        public Collection<PostReply> getNewReplies(@Nullable Sone currentSone) {
                Set<PostReply> allNewReplies = ImmutableSet.<PostReply>builder()
@@ -507,51 +276,6 @@ public class WebInterface implements SessionProvider {
                return from(allNewReplies).filter(replyVisibilityFilter.isVisible(currentSone)).toSet();
        }
 
-       /**
-        * Sets whether the current start of the plugin is the first start. It is
-        * considered a first start if the configuration file does not exist.
-        *
-        * @param firstStart
-        *            {@code true} if no configuration file existed when Sone was
-        *            loaded, {@code false} otherwise
-        */
-       public void setFirstStart(boolean firstStart) {
-               if (firstStart) {
-                       Template firstStartNotificationTemplate = loaders.loadTemplate("/templates/notify/firstStartNotification.html");
-                       Notification firstStartNotification = new TemplateNotification("first-start-notification", firstStartNotificationTemplate);
-                       notificationManager.addNotification(firstStartNotification);
-               }
-       }
-
-       /**
-        * Sets whether Sone was started with a fresh configuration file.
-        *
-        * @param newConfig
-        *            {@code true} if Sone was started with a fresh configuration,
-        *            {@code false} if the existing configuration could be read
-        */
-       public void setNewConfig(boolean newConfig) {
-               if (newConfig && !hasFirstStartNotification()) {
-                       Template configNotReadNotificationTemplate = loaders.loadTemplate("/templates/notify/configNotReadNotification.html");
-                       Notification configNotReadNotification = new TemplateNotification("config-not-read-notification", configNotReadNotificationTemplate);
-                       notificationManager.addNotification(configNotReadNotification);
-               }
-       }
-
-       //
-       // PRIVATE ACCESSORS
-       //
-
-       /**
-        * Returns whether the first start notification is currently displayed.
-        *
-        * @return {@code true} if the first-start notification is currently
-        *         displayed, {@code false} otherwise
-        */
-       private boolean hasFirstStartNotification() {
-               return notificationManager.getNotification("first-start-notification") != null;
-       }
-
        //
        // ACTIONS
        //
@@ -561,36 +285,6 @@ public class WebInterface implements SessionProvider {
         */
        public void start() {
                registerToadlets();
-
-               /* notification templates. */
-               Template startupNotificationTemplate = loaders.loadTemplate("/templates/notify/startupNotification.html");
-
-               final TemplateNotification startupNotification = new TemplateNotification("startup-notification", startupNotificationTemplate);
-               notificationManager.addNotification(startupNotification);
-
-               ticker.schedule(new Runnable() {
-
-                       @Override
-                       public void run() {
-                               startupNotification.dismiss();
-                       }
-               }, 2, TimeUnit.MINUTES);
-
-               Template wotMissingNotificationTemplate = loaders.loadTemplate("/templates/notify/wotMissingNotification.html");
-               final TemplateNotification wotMissingNotification = new TemplateNotification("wot-missing-notification", wotMissingNotificationTemplate);
-               ticker.scheduleAtFixedRate(new Runnable() {
-
-                       @Override
-                       @SuppressWarnings("synthetic-access")
-                       public void run() {
-                               if (getCore().getIdentityManager().isConnected()) {
-                                       wotMissingNotification.dismiss();
-                               } else {
-                                       notificationManager.addNotification(wotMissingNotification);
-                               }
-                       }
-
-               }, 15, 15, TimeUnit.SECONDS);
        }
 
        /**
@@ -598,7 +292,6 @@ public class WebInterface implements SessionProvider {
         */
        public void stop() {
                pageToadletRegistry.unregisterToadlets();
-               ticker.shutdownNow();
        }
 
        //
@@ -640,9 +333,6 @@ public class WebInterface implements SessionProvider {
                pageToadletRegistry.addPage(new UploadImagePage(this, loaders, templateRenderer));
                pageToadletRegistry.addPage(new EditImagePage(this, loaders, templateRenderer));
                pageToadletRegistry.addPage(new DeleteImagePage(this, loaders, templateRenderer));
-               pageToadletRegistry.addPage(new TrustPage(this, loaders, templateRenderer));
-               pageToadletRegistry.addPage(new DistrustPage(this, loaders, templateRenderer));
-               pageToadletRegistry.addPage(new UntrustPage(this, loaders, templateRenderer));
                pageToadletRegistry.addPage(new MarkAsKnownPage(this, loaders, templateRenderer));
                pageToadletRegistry.addPage(new BookmarkPage(this, loaders, templateRenderer));
                pageToadletRegistry.addPage(new UnbookmarkPage(this, loaders, templateRenderer));
@@ -659,6 +349,8 @@ public class WebInterface implements SessionProvider {
                pageToadletRegistry.addPage(new EmptyImageTitlePage(this, loaders, templateRenderer));
                pageToadletRegistry.addPage(new EmptyAlbumTitlePage(this, loaders, templateRenderer));
                pageToadletRegistry.addPage(new DismissNotificationPage(this, loaders, templateRenderer));
+               pageToadletRegistry.addPage(new DebugPage(this, loaders, templateRenderer));
+               pageToadletRegistry.addDebugPage(new MetricsPage(this, loaders, templateRenderer, metricRegistry));
                pageToadletRegistry.addPage(loaders.<FreenetRequest>loadStaticPage("css/", "/static/css/", "text/css"));
                pageToadletRegistry.addPage(loaders.<FreenetRequest>loadStaticPage("javascript/", "/static/javascript/", "text/javascript"));
                pageToadletRegistry.addPage(loaders.<FreenetRequest>loadStaticPage("images/", "/static/images/", "image/png"));
@@ -683,9 +375,6 @@ public class WebInterface implements SessionProvider {
                pageToadletRegistry.addPage(new UnfollowSoneAjaxPage(this));
                pageToadletRegistry.addPage(new EditAlbumAjaxPage(this));
                pageToadletRegistry.addPage(new EditImageAjaxPage(this, parserFilter, shortenFilter, renderFilter));
-               pageToadletRegistry.addPage(new TrustAjaxPage(this));
-               pageToadletRegistry.addPage(new DistrustAjaxPage(this));
-               pageToadletRegistry.addPage(new UntrustAjaxPage(this));
                pageToadletRegistry.addPage(new LikeAjaxPage(this));
                pageToadletRegistry.addPage(new UnlikeAjaxPage(this));
                pageToadletRegistry.addPage(new GetLikesAjaxPage(this));
@@ -698,328 +387,9 @@ public class WebInterface implements SessionProvider {
                pageToadletRegistry.registerToadlets();
        }
 
-       /**
-        * Returns all {@link Sone#isLocal() local Sone}s that are referenced by
-        * {@link SonePart}s in the given text (after parsing it using
-        * {@link SoneTextParser}).
-        *
-        * @param text
-        *            The text to parse
-        * @return All mentioned local Sones
-        */
-       private Collection<Sone> getMentionedSones(String text) {
-               /* we need no context to find mentioned Sones. */
-               Set<Sone> mentionedSones = new HashSet<>();
-               for (Part part : soneTextParser.parse(text, null)) {
-                       if (part instanceof SonePart) {
-                               mentionedSones.add(((SonePart) part).getSone());
-                       }
-               }
-               return Collections2.filter(mentionedSones, Sone.LOCAL_SONE_FILTER);
-       }
-
-       /**
-        * Returns the Sone insert notification for the given Sone. If no
-        * notification for the given Sone exists, a new notification is created and
-        * cached.
-        *
-        * @param sone
-        *            The Sone to get the insert notification for
-        * @return The Sone insert notification
-        */
-       private TemplateNotification getSoneInsertNotification(Sone sone) {
-               synchronized (soneInsertNotifications) {
-                       TemplateNotification templateNotification = soneInsertNotifications.get(sone);
-                       if (templateNotification == null) {
-                               templateNotification = new TemplateNotification(loaders.loadTemplate("/templates/notify/soneInsertNotification.html"));
-                               templateNotification.set("insertSone", sone);
-                               soneInsertNotifications.put(sone, templateNotification);
-                       }
-                       return templateNotification;
-               }
-       }
-
-       private boolean localSoneMentionedInNewPostOrReply(Post post) {
-               if (!post.getSone().isLocal()) {
-                       if (!getMentionedSones(post.getText()).isEmpty() && !post.isKnown()) {
-                               return true;
-                       }
-               }
-               for (PostReply postReply : getCore().getReplies(post.getId())) {
-                       if (postReply.getSone().isLocal()) {
-                               continue;
-                       }
-                       if (!getMentionedSones(postReply.getText()).isEmpty() && !postReply.isKnown()) {
-                               return true;
-                       }
-               }
-               return false;
-       }
-
-       //
-       // EVENT HANDLERS
-       //
-
-       /**
-        * Notifies the web interface that a new {@link Sone} was found.
-        *
-        * @param newSoneFoundEvent
-        *            The event
-        */
-       @Subscribe
-       public void newSoneFound(NewSoneFoundEvent newSoneFoundEvent) {
-               newSoneNotification.add(newSoneFoundEvent.sone());
-               if (!hasFirstStartNotification()) {
-                       notificationManager.addNotification(newSoneNotification);
-               }
-       }
-
-       /**
-        * Notifies the web interface that a new {@link Post} was found.
-        *
-        * @param newPostFoundEvent
-        *            The event
-        */
-       @Subscribe
-       public void newPostFound(NewPostFoundEvent newPostFoundEvent) {
-               Post post = newPostFoundEvent.post();
-               boolean isLocal = post.getSone().isLocal();
-               if (isLocal) {
-                       localPostNotification.add(post);
-               } else {
-                       newPostNotification.add(post);
-               }
-               if (!hasFirstStartNotification()) {
-                       notificationManager.addNotification(isLocal ? localPostNotification : newPostNotification);
-                       if (!getMentionedSones(post.getText()).isEmpty() && !isLocal) {
-                               mentionNotification.add(post);
-                               notificationManager.addNotification(mentionNotification);
-                       }
-               } else {
-                       getCore().markPostKnown(post);
-               }
-       }
-
-       /**
-        * Notifies the web interface that a new {@link PostReply} was found.
-        *
-        * @param newPostReplyFoundEvent
-        *            The event
-        */
-       @Subscribe
-       public void newReplyFound(NewPostReplyFoundEvent newPostReplyFoundEvent) {
-               PostReply reply = newPostReplyFoundEvent.postReply();
-               boolean isLocal = reply.getSone().isLocal();
-               if (isLocal) {
-                       localReplyNotification.add(reply);
-               } else {
-                       newReplyNotification.add(reply);
-               }
-               if (!hasFirstStartNotification()) {
-                       notificationManager.addNotification(isLocal ? localReplyNotification : newReplyNotification);
-                       if (reply.getPost().isPresent() && localSoneMentionedInNewPostOrReply(reply.getPost().get())) {
-                               mentionNotification.add(reply.getPost().get());
-                               notificationManager.addNotification(mentionNotification);
-                       }
-               } else {
-                       getCore().markReplyKnown(reply);
-               }
-       }
-
-       /**
-        * Notifies the web interface that a {@link Sone} was marked as known.
-        *
-        * @param markSoneKnownEvent
-        *            The event
-        */
-       @Subscribe
-       public void markSoneKnown(MarkSoneKnownEvent markSoneKnownEvent) {
-               newSoneNotification.remove(markSoneKnownEvent.sone());
-       }
-
-       @Subscribe
-       public void markPostKnown(MarkPostKnownEvent markPostKnownEvent) {
-               removePost(markPostKnownEvent.post());
-       }
-
-       @Subscribe
-       public void markReplyKnown(MarkPostReplyKnownEvent markPostReplyKnownEvent) {
-               removeReply(markPostReplyKnownEvent.postReply());
-       }
-
-       @Subscribe
-       public void soneRemoved(SoneRemovedEvent soneRemovedEvent) {
-               newSoneNotification.remove(soneRemovedEvent.sone());
-       }
-
-       @Subscribe
-       public void postRemoved(PostRemovedEvent postRemovedEvent) {
-               removePost(postRemovedEvent.post());
-       }
-
-       private void removePost(Post post) {
-               newPostNotification.remove(post);
-               localPostNotification.remove(post);
-               if (!localSoneMentionedInNewPostOrReply(post)) {
-                       mentionNotification.remove(post);
-               }
-       }
-
-       @Subscribe
-       public void replyRemoved(PostReplyRemovedEvent postReplyRemovedEvent) {
-               removeReply(postReplyRemovedEvent.postReply());
-       }
-
-       private void removeReply(PostReply reply) {
-               newReplyNotification.remove(reply);
-               localReplyNotification.remove(reply);
-               if (reply.getPost().isPresent() && !localSoneMentionedInNewPostOrReply(reply.getPost().get())) {
-                       mentionNotification.remove(reply.getPost().get());
-               }
-       }
-
-       /**
-        * Notifies the web interface that a Sone was locked.
-        *
-        * @param soneLockedEvent
-        *            The event
-        */
-       @Subscribe
-       public void soneLocked(SoneLockedEvent soneLockedEvent) {
-               final Sone sone = soneLockedEvent.sone();
-               ScheduledFuture<?> tickerObject = ticker.schedule(new Runnable() {
-
-                       @Override
-                       @SuppressWarnings("synthetic-access")
-                       public void run() {
-                               lockedSonesNotification.add(sone);
-                               notificationManager.addNotification(lockedSonesNotification);
-                       }
-               }, 5, TimeUnit.MINUTES);
-               lockedSonesTickerObjects.put(sone, tickerObject);
-       }
-
-       /**
-        * Notifies the web interface that a Sone was unlocked.
-        *
-        * @param soneUnlockedEvent
-        *            The event
-        */
-       @Subscribe
-       public void soneUnlocked(SoneUnlockedEvent soneUnlockedEvent) {
-               lockedSonesNotification.remove(soneUnlockedEvent.sone());
-               lockedSonesTickerObjects.remove(soneUnlockedEvent.sone()).cancel(false);
-       }
-
-       /**
-        * Notifies the web interface that a {@link Sone} is being inserted.
-        *
-        * @param soneInsertingEvent
-        *            The event
-        */
-       @Subscribe
-       public void soneInserting(SoneInsertingEvent soneInsertingEvent) {
-               TemplateNotification soneInsertNotification = getSoneInsertNotification(soneInsertingEvent.sone());
-               soneInsertNotification.set("soneStatus", "inserting");
-               if (soneInsertingEvent.sone().getOptions().isSoneInsertNotificationEnabled()) {
-                       notificationManager.addNotification(soneInsertNotification);
-               }
-       }
-
-       /**
-        * Notifies the web interface that a {@link Sone} was inserted.
-        *
-        * @param soneInsertedEvent
-        *            The event
-        */
-       @Subscribe
-       public void soneInserted(SoneInsertedEvent soneInsertedEvent) {
-               TemplateNotification soneInsertNotification = getSoneInsertNotification(soneInsertedEvent.sone());
-               soneInsertNotification.set("soneStatus", "inserted");
-               soneInsertNotification.set("insertDuration", soneInsertedEvent.insertDuration() / 1000);
-               if (soneInsertedEvent.sone().getOptions().isSoneInsertNotificationEnabled()) {
-                       notificationManager.addNotification(soneInsertNotification);
-               }
-       }
-
-       /**
-        * Notifies the web interface that a {@link Sone} insert was aborted.
-        *
-        * @param soneInsertAbortedEvent
-        *            The event
-        */
-       @Subscribe
-       public void soneInsertAborted(SoneInsertAbortedEvent soneInsertAbortedEvent) {
-               TemplateNotification soneInsertNotification = getSoneInsertNotification(soneInsertAbortedEvent.sone());
-               soneInsertNotification.set("soneStatus", "insert-aborted");
-               soneInsertNotification.set("insert-error", soneInsertAbortedEvent.cause());
-               if (soneInsertAbortedEvent.sone().getOptions().isSoneInsertNotificationEnabled()) {
-                       notificationManager.addNotification(soneInsertNotification);
-               }
-       }
-
-       /**
-        * Notifies the web interface that a new Sone version was found.
-        *
-        * @param updateFoundEvent
-        *            The event
-        */
-       @Subscribe
-       public void updateFound(UpdateFoundEvent updateFoundEvent) {
-               newVersionNotification.set("latestVersion", updateFoundEvent.version());
-               newVersionNotification.set("latestEdition", updateFoundEvent.latestEdition());
-               newVersionNotification.set("releaseTime", updateFoundEvent.releaseTime());
-               newVersionNotification.set("disruptive", updateFoundEvent.disruptive());
-               notificationManager.addNotification(newVersionNotification);
-       }
-
-       /**
-        * Notifies the web interface that an image insert was started
-        *
-        * @param imageInsertStartedEvent
-        *            The event
-        */
-       @Subscribe
-       public void imageInsertStarted(ImageInsertStartedEvent imageInsertStartedEvent) {
-               insertingImagesNotification.add(imageInsertStartedEvent.image());
-               notificationManager.addNotification(insertingImagesNotification);
-       }
-
-       /**
-        * Notifies the web interface that an {@link Image} insert was aborted.
-        *
-        * @param imageInsertAbortedEvent
-        *            The event
-        */
-       @Subscribe
-       public void imageInsertAborted(ImageInsertAbortedEvent imageInsertAbortedEvent) {
-               insertingImagesNotification.remove(imageInsertAbortedEvent.image());
-       }
-
-       /**
-        * Notifies the web interface that an {@link Image} insert is finished.
-        *
-        * @param imageInsertFinishedEvent
-        *            The event
-        */
-       @Subscribe
-       public void imageInsertFinished(ImageInsertFinishedEvent imageInsertFinishedEvent) {
-               insertingImagesNotification.remove(imageInsertFinishedEvent.image());
-               insertedImagesNotification.add(imageInsertFinishedEvent.image());
-               notificationManager.addNotification(insertedImagesNotification);
-       }
-
-       /**
-        * Notifies the web interface that an {@link Image} insert has failed.
-        *
-        * @param imageInsertFailedEvent
-        *            The event
-        */
        @Subscribe
-       public void imageInsertFailed(ImageInsertFailedEvent imageInsertFailedEvent) {
-               insertingImagesNotification.remove(imageInsertFailedEvent.image());
-               imageInsertFailedNotification.add(imageInsertFailedEvent.image());
-               notificationManager.addNotification(imageInsertFailedNotification);
+       public void debugActivated(@Nonnull DebugActivatedEvent debugActivatedEvent) {
+               pageToadletRegistry.activateDebugMode();
        }
 
 }
diff --git a/src/main/java/net/pterodactylus/sone/web/page/PageToadlet.java b/src/main/java/net/pterodactylus/sone/web/page/PageToadlet.java
deleted file mode 100644 (file)
index 2b22377..0000000
+++ /dev/null
@@ -1,188 +0,0 @@
-/*
- * Sone - PageToadlet.java - Copyright © 2010–2019 David Roden
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <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.l10n.NodeL10n;
-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, NodeL10n.getBase(), 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, NodeL10n.getBase(), 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);
-       }
-
-}
index d01aeb4..409d18c 100644 (file)
@@ -23,9 +23,9 @@ class DefaultElementLoader(private val freenetInterface: FreenetInterface, ticke
 
        @Inject constructor(freenetInterface: FreenetInterface): this(freenetInterface, Ticker.systemTicker())
 
-       private val loadingLinks: Cache<String, Boolean> = CacheBuilder.newBuilder().build<String, Boolean>()
-       private val failureCache: Cache<String, Boolean> = CacheBuilder.newBuilder().ticker(ticker).expireAfterWrite(30, MINUTES).build<String, Boolean>()
-       private val elementCache: Cache<String, LinkedElement> = CacheBuilder.newBuilder().build<String, LinkedElement>()
+       private val loadingLinks: Cache<String, Boolean> = CacheBuilder.newBuilder().build()
+       private val failureCache: Cache<String, Boolean> = CacheBuilder.newBuilder().ticker(ticker).expireAfterWrite(30, MINUTES).build()
+       private val elementCache: Cache<String, LinkedElement> = CacheBuilder.newBuilder().build()
        private val callback = object: FreenetInterface.BackgroundFetchCallback {
                override fun shouldCancel(uri: FreenetURI, mimeType: String, size: Long): Boolean {
                        return (size > 2097152) || (!mimeType.startsWith("image/") && !mimeType.startsWith("text/html"))
diff --git a/src/main/kotlin/net/pterodactylus/sone/core/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 783552e..fe1e3c3 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - Preferences.kt - Copyright © 2013–2019 David Roden
+ * Sone - Preferences.kt - Copyright © 2013–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
 
 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
@@ -77,24 +80,6 @@ class Preferences(private val eventBus: EventBus) {
                get() = unsupported
                set(value) = _requireFullAccess.set(value)
 
-       private val _positiveTrust = DefaultOption(75, range(0, 100))
-       val positiveTrust: Int get() = _positiveTrust.get()
-       var newPositiveTrust: Int?
-               get() = unsupported
-               set(value) = _positiveTrust.set(value)
-
-       private val _negativeTrust = DefaultOption(-25, range(-100, 100))
-       val negativeTrust: Int get() = _negativeTrust.get()
-       var newNegativeTrust: Int?
-               get() = unsupported
-               set(value) = _negativeTrust.set(value)
-
-       private val _trustComment = DefaultOption("Set from Sone Web Interface")
-       val trustComment: String get() = _trustComment.get()
-       var newTrustComment: String?
-               get() = unsupported
-               set(value) = _trustComment.set(value)
-
        private val _fcpInterfaceActive = DefaultOption(false)
        val fcpInterfaceActive: Boolean get() = _fcpInterfaceActive.get()
        var newFcpInterfaceActive: Boolean?
@@ -116,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
@@ -125,11 +121,9 @@ class Preferences(private val eventBus: EventBus) {
                configuration.getIntValue("Option/CharactersPerPost").value = _charactersPerPost.real
                configuration.getIntValue("Option/PostCutOffLength").value = _postCutOffLength.real
                configuration.getBooleanValue("Option/RequireFullAccess").value = _requireFullAccess.real
-               configuration.getIntValue("Option/PositiveTrust").value = _positiveTrust.real
-               configuration.getIntValue("Option/NegativeTrust").value = _negativeTrust.real
-               configuration.getStringValue("Option/TrustComment").value = _trustComment.real
                configuration.getBooleanValue("Option/ActivateFcpInterface").value = _fcpInterfaceActive.real
                configuration.getIntValue("Option/FcpFullAccessRequired").value = toInt(_fcpFullAccessRequired.real)
+               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)
diff --git a/src/main/kotlin/net/pterodactylus/sone/core/UpdateChecker.kt b/src/main/kotlin/net/pterodactylus/sone/core/UpdateChecker.kt
new file mode 100644 (file)
index 0000000..0e67d0a
--- /dev/null
@@ -0,0 +1,123 @@
+/*
+ * Sone - UpdateChecker.kt - Copyright © 2011–2020 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.core
+
+import com.google.common.eventbus.*
+import com.google.common.primitives.*
+import com.google.inject.Inject
+import freenet.keys.*
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.sone.main.*
+import net.pterodactylus.sone.utils.*
+import net.pterodactylus.util.version.Version
+import java.io.*
+import java.util.*
+import java.util.logging.*
+import java.util.logging.Logger.*
+import javax.inject.Singleton
+
+/**
+ * Watches the official Sone homepage for new releases.
+ */
+@Singleton
+class UpdateChecker @Inject constructor(
+               private val eventBus: EventBus,
+               private val freenetInterface: FreenetInterface,
+               private val currentRunningVersion: Version,
+               pluginHomepage: PluginHomepage) {
+
+       private val logger: Logger = getLogger(UpdateChecker::class.java.name)
+
+       private val currentUri by lazy { FreenetURI(pluginHomepage.homepage) }
+
+       var latestEdition = SonePlugin.getLatestEdition()
+               private set
+
+       var latestVersion: Version = currentRunningVersion
+               private set
+
+       var latestVersionDate: Long = 0
+               private set
+
+       fun hasLatestVersion() =
+                       latestVersion > currentRunningVersion
+
+       fun start() {
+               freenetInterface.registerUsk(currentUri) { uri, edition, newKnownGood, newSlot ->
+                       logger.log(Level.FINEST, String.format("Found update for %s: %d, %s, %s", uri, edition, newKnownGood, newSlot))
+                       if (newKnownGood || newSlot) {
+                               try {
+                                       freenetInterface.fetchUri(uri.setMetaString(arrayOf("sone.properties")))
+                                                       ?.onNull {
+                                                               logger.log(Level.WARNING, String.format("Could not fetch properties of latest homepage: %s", uri))
+                                                       }?.fetchResult
+                                                       ?.asBucket()?.use { resultBucket ->
+                                                               resultBucket.inputStream
+                                                                               .let { parseProperties(it) }
+                                                                               .let { extractCurrentVersion(it) }
+                                                                               .onNull { logger.log(Level.INFO, "Invalid data parsed from properties.") }
+                                                                               ?.takeIf { it.version > latestVersion }
+                                                                               ?.also { updateVersionInformation(it, edition) }
+                                                                               ?.also { logger.info { "Found new version: %s (%tc%s)".format(it.version, it.time, if (it.disruptive) ", disruptive" else "") } }
+                                                                               ?.also { eventBus.post(UpdateFoundEvent(it.version, it.time, edition, it.disruptive)) }
+                                                       }
+                               } catch (ioe1: IOException) {
+                                       logger.log(Level.WARNING, String.format("Could not parse sone.properties of %s!", uri), ioe1)
+                               }
+                       }
+               }
+       }
+
+       fun stop() {
+               freenetInterface.unregisterUsk(currentUri)
+       }
+
+       private fun updateVersionInformation(versionInformation: VersionInformation, edition: Long) {
+               latestVersion = versionInformation.version
+               latestVersionDate = versionInformation.time
+               latestEdition = edition
+       }
+
+       private fun parseProperties(propertiesInputStream: InputStream) =
+                       Properties().apply {
+                               InputStreamReader(propertiesInputStream, "UTF-8").use { inputStreamReader ->
+                                       load(inputStreamReader)
+                               }
+                       }
+
+       private fun extractCurrentVersion(properties: Properties) =
+                       properties.getProperty("CurrentVersion/Version")
+                                       ?.let { Version.parse(it) }
+                                       ?.let { version ->
+                                               properties.getProperty("CurrentVersion/ReleaseTime")
+                                                               ?.let { Longs.tryParse(it) }
+                                                               ?.let { time ->
+                                                                       VersionInformation(version, time, disruptiveVersionBetweenCurrentAndFound(properties))
+                                                               }
+                                       }
+
+       private fun disruptiveVersionBetweenCurrentAndFound(properties: Properties) =
+                       properties.stringPropertyNames()
+                                       .filter { it.startsWith("DisruptiveVersion/") }
+                                       .map { it.removePrefix("DisruptiveVersion/") }
+                                       .map { Version.parse(it) }
+                                       .any { it > currentRunningVersion }
+
+}
+
+private data class VersionInformation(val version: Version, val time: Long, val disruptive: Boolean)
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/ConfigNotRead.kt b/src/main/kotlin/net/pterodactylus/sone/core/event/ConfigNotRead.kt
new file mode 100644 (file)
index 0000000..97f2843
--- /dev/null
@@ -0,0 +1,26 @@
+/**
+ * Sone - ConfigNotRead.kt - Copyright © 2019–2020 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.core.event
+
+/**
+ * Event that signals that Sone could not read an existing configuration
+ * successfully, and a new configuration was created. This is different from
+ * [FirstStart] in that `FirstStart` signals that there *was* no existing
+ * configuration to be read.
+ */
+class ConfigNotRead
diff --git a/src/main/kotlin/net/pterodactylus/sone/core/event/DebugActivatedEvent.kt b/src/main/kotlin/net/pterodactylus/sone/core/event/DebugActivatedEvent.kt
new file mode 100644 (file)
index 0000000..037cdb5
--- /dev/null
@@ -0,0 +1,20 @@
+/**
+ * Sone - DebugActivatedEvent.kt - Copyright © 2019–2020 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.core.event
+
+class DebugActivatedEvent
diff --git a/src/main/kotlin/net/pterodactylus/sone/core/event/FirstStart.kt b/src/main/kotlin/net/pterodactylus/sone/core/event/FirstStart.kt
new file mode 100644 (file)
index 0000000..c58db50
--- /dev/null
@@ -0,0 +1,24 @@
+/**
+ * Sone - FirstStart.kt - Copyright © 2019–2020 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.core.event
+
+/**
+ * Event that signals that Sone was started for the first time. This event
+ * will only be triggered once, on startup.
+ */
+class FirstStart
diff --git a/src/main/kotlin/net/pterodactylus/sone/core/event/ImageEvent.kt b/src/main/kotlin/net/pterodactylus/sone/core/event/ImageEvent.kt
new file mode 100644 (file)
index 0000000..12d9ca1
--- /dev/null
@@ -0,0 +1,25 @@
+/*
+ * Sone - ImageEvent.kt - Copyright © 2013–2020 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.core.event
+
+import net.pterodactylus.sone.data.*
+
+/**
+ * Base class for [Image] events.
+ */
+abstract class ImageEvent(val image: Image)
diff --git a/src/main/kotlin/net/pterodactylus/sone/core/event/ImageInsertAbortedEvent.kt b/src/main/kotlin/net/pterodactylus/sone/core/event/ImageInsertAbortedEvent.kt
new file mode 100644 (file)
index 0000000..37a221f
--- /dev/null
@@ -0,0 +1,25 @@
+/*
+ * Sone - ImageInsertAbortedEvent.kt - Copyright © 2013–2020 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.core.event
+
+import net.pterodactylus.sone.data.*
+
+/**
+ * Event that signals that an [Image] insert is aborted.
+ */
+class ImageInsertAbortedEvent(image: Image) : ImageEvent(image)
diff --git a/src/main/kotlin/net/pterodactylus/sone/core/event/ImageInsertFailedEvent.kt b/src/main/kotlin/net/pterodactylus/sone/core/event/ImageInsertFailedEvent.kt
new file mode 100644 (file)
index 0000000..e19b3d8
--- /dev/null
@@ -0,0 +1,25 @@
+/*
+ * Sone - ImageInsertFailedEvent.kt - Copyright © 2013–2020 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.core.event
+
+import net.pterodactylus.sone.data.*
+
+/**
+ * Event that signals that an [Image] insert has failed.
+ */
+class ImageInsertFailedEvent(image: Image, val cause: Throwable) : ImageEvent(image)
diff --git a/src/main/kotlin/net/pterodactylus/sone/core/event/ImageInsertFinishedEvent.kt b/src/main/kotlin/net/pterodactylus/sone/core/event/ImageInsertFinishedEvent.kt
new file mode 100644 (file)
index 0000000..46daec6
--- /dev/null
@@ -0,0 +1,26 @@
+/*
+ * Sone - ImageInsertFinishedEvent.kt - Copyright © 2013–2020 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.core.event
+
+import freenet.keys.*
+import net.pterodactylus.sone.data.*
+
+/**
+ * Event that signals that an [Image] insert is finished.
+ */
+class ImageInsertFinishedEvent(image: Image, val resultingUri: FreenetURI) : ImageEvent(image)
diff --git a/src/main/kotlin/net/pterodactylus/sone/core/event/ImageInsertStartedEvent.kt b/src/main/kotlin/net/pterodactylus/sone/core/event/ImageInsertStartedEvent.kt
new file mode 100644 (file)
index 0000000..57478fa
--- /dev/null
@@ -0,0 +1,25 @@
+/*
+ * Sone - ImageInsertStartedEvent.kt - Copyright © 2013–2020 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.core.event
+
+import net.pterodactylus.sone.data.*
+
+/**
+ * Event that signals that an [Image] is not being inserted.
+ */
+class ImageInsertStartedEvent(image: Image) : ImageEvent(image)
diff --git a/src/main/kotlin/net/pterodactylus/sone/core/event/MarkPostKnownEvent.kt b/src/main/kotlin/net/pterodactylus/sone/core/event/MarkPostKnownEvent.kt
new file mode 100644 (file)
index 0000000..e479202
--- /dev/null
@@ -0,0 +1,26 @@
+/*
+ * Sone - MarkPostKnownEvent.kt - Copyright © 2013–2020 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.core.event
+
+import net.pterodactylus.sone.data.*
+
+/**
+ * Event that signals that a [Post] has been marked as
+ * [known][Post.isKnown].
+ */
+class MarkPostKnownEvent(post: Post) : PostEvent(post)
diff --git a/src/main/kotlin/net/pterodactylus/sone/core/event/MarkPostReplyKnownEvent.kt b/src/main/kotlin/net/pterodactylus/sone/core/event/MarkPostReplyKnownEvent.kt
new file mode 100644 (file)
index 0000000..af9dce4
--- /dev/null
@@ -0,0 +1,26 @@
+/*
+ * Sone - MarkPostReplyKnownEvent.kt - Copyright © 2013–2020 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.core.event
+
+import net.pterodactylus.sone.data.*
+
+/**
+ * Event that signals that a [PostReply] has been marked as
+ * [known][PostReply.isKnown].
+ */
+class MarkPostReplyKnownEvent(postReply: PostReply) : PostReplyEvent(postReply)
diff --git a/src/main/kotlin/net/pterodactylus/sone/core/event/MarkSoneKnownEvent.kt b/src/main/kotlin/net/pterodactylus/sone/core/event/MarkSoneKnownEvent.kt
new file mode 100644 (file)
index 0000000..b3b8fbc
--- /dev/null
@@ -0,0 +1,26 @@
+/*
+ * Sone - MarkSoneKnownEvent.kt - Copyright © 2013–2020 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.core.event
+
+import net.pterodactylus.sone.data.*
+
+/**
+ * Event that signals that a [Sone] has been marked as
+ * [known][Sone.isKnown].
+ */
+class MarkSoneKnownEvent(sone: Sone) : SoneEvent(sone)
diff --git a/src/main/kotlin/net/pterodactylus/sone/core/event/MentionOfLocalSoneFoundEvent.kt b/src/main/kotlin/net/pterodactylus/sone/core/event/MentionOfLocalSoneFoundEvent.kt
new file mode 100644 (file)
index 0000000..8b9b542
--- /dev/null
@@ -0,0 +1,27 @@
+/**
+ * Sone - MentionOfLocalSoneFoundEvent.kt - Copyright © 2019–2020 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.core.event
+
+import net.pterodactylus.sone.data.*
+
+/**
+ * Event that signals that a new post or reply was found that mentioned a local
+ * Sone, which happens if the [SoneTextParser] locates a [SonePart] in a post
+ * or reply.
+ */
+data class MentionOfLocalSoneFoundEvent(val post: Post)
diff --git a/src/main/kotlin/net/pterodactylus/sone/core/event/MentionOfLocalSoneRemovedEvent.kt b/src/main/kotlin/net/pterodactylus/sone/core/event/MentionOfLocalSoneRemovedEvent.kt
new file mode 100644 (file)
index 0000000..2413a80
--- /dev/null
@@ -0,0 +1,27 @@
+/**
+ * Sone - MentionOfLocalSoneRemovedEvent.kt - Copyright © 2019–2020 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.core.event
+
+import net.pterodactylus.sone.data.*
+
+/**
+ * Event that signals that a post or reply that mentioned a local Sone was
+ * removed so that the given post and its replies do not contain a mention of
+ * a local Sone anymore.
+ */
+data class MentionOfLocalSoneRemovedEvent(val post: Post)
index ba7f957..876f79c 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - NewPostFoundEvent.kt - Copyright © 2013–2019 David Roden
+ * Sone - NewPostFoundEvent.kt - Copyright © 2013–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -22,9 +22,4 @@ import net.pterodactylus.sone.data.Post
 /**
  * Event that signals that a new post was found.
  */
-data class NewPostFoundEvent(val post: Post) {
-
-       @Deprecated(message = "will go away", replaceWith = ReplaceWith("post"))
-       fun post() = post
-
-}
+data class NewPostFoundEvent(val post: Post)
index a70d1b9..ec339d5 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - NewPostReplyFoundEvent.kt - Copyright © 2013–2019 David Roden
+ * Sone - NewPostReplyFoundEvent.kt - Copyright © 2013–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -22,9 +22,4 @@ import net.pterodactylus.sone.data.PostReply
 /**
  * Event that signals that a new [PostReply] was found.
  */
-data class NewPostReplyFoundEvent(val postReply: PostReply) {
-
-       @Deprecated(message = "will go away", replaceWith = ReplaceWith("postReply"))
-       fun postReply() = postReply
-
-}
+data class NewPostReplyFoundEvent(val postReply: PostReply)
diff --git a/src/main/kotlin/net/pterodactylus/sone/core/event/NewSoneFoundEvent.kt b/src/main/kotlin/net/pterodactylus/sone/core/event/NewSoneFoundEvent.kt
new file mode 100644 (file)
index 0000000..bb7c933
--- /dev/null
@@ -0,0 +1,25 @@
+/*
+ * Sone - NewSoneFoundEvent.kt - Copyright © 2013–2020 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.core.event
+
+import net.pterodactylus.sone.data.*
+
+/**
+ * Event that signals that a new remote Sone was found.
+ */
+class NewSoneFoundEvent(sone: Sone) : SoneEvent(sone)
diff --git a/src/main/kotlin/net/pterodactylus/sone/core/event/PostEvent.kt b/src/main/kotlin/net/pterodactylus/sone/core/event/PostEvent.kt
new file mode 100644 (file)
index 0000000..becafdd
--- /dev/null
@@ -0,0 +1,25 @@
+/*
+ * Sone - PostEvent.kt - Copyright © 2013–2020 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.core.event
+
+import net.pterodactylus.sone.data.*
+
+/**
+ * Base class for post events.
+ */
+abstract class PostEvent(val post: Post)
index 117d800..f68214b 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - PostRemovedEvent.kt - Copyright © 2013–2019 David Roden
+ * Sone - PostRemovedEvent.kt - Copyright © 2013–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -22,9 +22,4 @@ import net.pterodactylus.sone.data.Post
 /**
  * Event that signals that a [Post] was removed.
  */
-data class PostRemovedEvent(val post: Post) {
-
-       @Deprecated(message = "will go away", replaceWith = ReplaceWith("post"))
-       fun post() = post
-
-}
+data class PostRemovedEvent(val post: Post)
diff --git a/src/main/kotlin/net/pterodactylus/sone/core/event/PostReplyEvent.kt b/src/main/kotlin/net/pterodactylus/sone/core/event/PostReplyEvent.kt
new file mode 100644 (file)
index 0000000..72e0daa
--- /dev/null
@@ -0,0 +1,25 @@
+/*
+ * Sone - PostReplyEvent.kt - Copyright © 2013–2020 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.core.event
+
+import net.pterodactylus.sone.data.*
+
+/**
+ * Base class for [PostReply] events.
+ */
+open class PostReplyEvent(val postReply: PostReply)
index 6aa495b..6c71b8e 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - PostReplyRemovedEvent.kt - Copyright © 2013–2019 David Roden
+ * Sone - PostReplyRemovedEvent.kt - Copyright © 2013–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
diff --git a/src/main/kotlin/net/pterodactylus/sone/core/event/Shutdown.kt b/src/main/kotlin/net/pterodactylus/sone/core/event/Shutdown.kt
new file mode 100644 (file)
index 0000000..975dd96
--- /dev/null
@@ -0,0 +1,23 @@
+/**
+ * Sone - Shutdown.kt - Copyright © 2019–2020 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.core.event
+
+/**
+ * Event that signals the shutdown of Sone.
+ */
+class Shutdown
diff --git a/src/main/kotlin/net/pterodactylus/sone/core/event/SoneEvent.kt b/src/main/kotlin/net/pterodactylus/sone/core/event/SoneEvent.kt
new file mode 100644 (file)
index 0000000..d84bab5
--- /dev/null
@@ -0,0 +1,25 @@
+/*
+ * Sone - SoneEvent.kt - Copyright © 2013–2020 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.core.event
+
+import net.pterodactylus.sone.data.*
+
+/**
+ * Base class for Sone events.
+ */
+abstract class SoneEvent(val sone: Sone)
diff --git a/src/main/kotlin/net/pterodactylus/sone/core/event/SoneInsertAbortedEvent.kt b/src/main/kotlin/net/pterodactylus/sone/core/event/SoneInsertAbortedEvent.kt
new file mode 100644 (file)
index 0000000..73ea564
--- /dev/null
@@ -0,0 +1,25 @@
+/*
+ * Sone - SoneInsertAbortedEvent.kt - Copyright © 2013–2020 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.core.event
+
+import net.pterodactylus.sone.data.*
+
+/**
+ * Event that signals that a [Sone] insert was aborted.
+ */
+class SoneInsertAbortedEvent(sone: Sone, val cause: Throwable) : SoneEvent(sone)
diff --git a/src/main/kotlin/net/pterodactylus/sone/core/event/SoneInsertedEvent.kt b/src/main/kotlin/net/pterodactylus/sone/core/event/SoneInsertedEvent.kt
new file mode 100644 (file)
index 0000000..f22dabe
--- /dev/null
@@ -0,0 +1,25 @@
+/*
+ * Sone - SoneInsertedEvent.kt - Copyright © 2013–2020 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.core.event
+
+import net.pterodactylus.sone.data.*
+
+/**
+ * Event that signals that a [Sone] was inserted.
+ */
+class SoneInsertedEvent(sone: Sone, val insertDuration: Long, val insertFingerprint: String) : SoneEvent(sone)
diff --git a/src/main/kotlin/net/pterodactylus/sone/core/event/SoneInsertingEvent.kt b/src/main/kotlin/net/pterodactylus/sone/core/event/SoneInsertingEvent.kt
new file mode 100644 (file)
index 0000000..f928556
--- /dev/null
@@ -0,0 +1,25 @@
+/*
+ * Sone - SoneInsertingEvent.kt - Copyright © 2013–2020 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.core.event
+
+import net.pterodactylus.sone.data.*
+
+/**
+ * Event that signals that a [Sone] is now being inserted.
+ */
+class SoneInsertingEvent(sone: Sone) : SoneEvent(sone)
diff --git a/src/main/kotlin/net/pterodactylus/sone/core/event/SoneLockedEvent.kt b/src/main/kotlin/net/pterodactylus/sone/core/event/SoneLockedEvent.kt
new file mode 100644 (file)
index 0000000..1d77318
--- /dev/null
@@ -0,0 +1,26 @@
+/*
+ * Sone - SoneLockedEvent.kt - Copyright © 2013–2020 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.core.event
+
+import net.pterodactylus.sone.data.*
+
+/**
+ * Event that signals that a [Sone] was locked. Only
+ * [local Sones][Sone.isLocal] can be locked.
+ */
+class SoneLockedEvent(sone: Sone) : SoneEvent(sone)
diff --git a/src/main/kotlin/net/pterodactylus/sone/core/event/SoneLockedOnStartup.kt b/src/main/kotlin/net/pterodactylus/sone/core/event/SoneLockedOnStartup.kt
new file mode 100644 (file)
index 0000000..ac11c4d
--- /dev/null
@@ -0,0 +1,25 @@
+/**
+ * Sone - SoneLockedOnStartup.kt - Copyright © 2019–2020 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.core.event
+
+import net.pterodactylus.sone.data.*
+
+/**
+ * Signals that a Sone was locked on startup because it’s empty.
+ */
+class SoneLockedOnStartup(sone: Sone) : SoneEvent(sone)
diff --git a/src/main/kotlin/net/pterodactylus/sone/core/event/SoneRemovedEvent.kt b/src/main/kotlin/net/pterodactylus/sone/core/event/SoneRemovedEvent.kt
new file mode 100644 (file)
index 0000000..ea08dae
--- /dev/null
@@ -0,0 +1,25 @@
+/*
+ * Sone - SoneRemovedEvent.kt - Copyright © 2013–2020 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.core.event
+
+import net.pterodactylus.sone.data.*
+
+/**
+ * Event that signals that a [Sone] was removed.
+ */
+class SoneRemovedEvent(sone: Sone) : SoneEvent(sone)
diff --git a/src/main/kotlin/net/pterodactylus/sone/core/event/SoneUnlockedEvent.kt b/src/main/kotlin/net/pterodactylus/sone/core/event/SoneUnlockedEvent.kt
new file mode 100644 (file)
index 0000000..3cc4203
--- /dev/null
@@ -0,0 +1,26 @@
+/*
+ * Sone - SoneUnlockedEvent.kt - Copyright © 2013–2020 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.core.event
+
+import net.pterodactylus.sone.data.*
+
+/**
+ * Event that signals that a [Sone] was unlocked. Only
+ * [local Sones][Sone.isLocal] can be locked.
+ */
+class SoneUnlockedEvent(sone: Sone) : SoneEvent(sone)
diff --git a/src/main/kotlin/net/pterodactylus/sone/core/event/Startup.kt b/src/main/kotlin/net/pterodactylus/sone/core/event/Startup.kt
new file mode 100644 (file)
index 0000000..79bd7a9
--- /dev/null
@@ -0,0 +1,23 @@
+/**
+ * Sone - Startup.kt - Copyright © 2019–2020 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.core.event
+
+/**
+ * Event that signals the startup of Sone.
+ */
+class Startup
diff --git a/src/main/kotlin/net/pterodactylus/sone/core/event/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/core/event/UpdateFoundEvent.kt b/src/main/kotlin/net/pterodactylus/sone/core/event/UpdateFoundEvent.kt
new file mode 100644 (file)
index 0000000..82a4cf5
--- /dev/null
@@ -0,0 +1,25 @@
+/*
+ * Sone - UpdateFoundEvent.kt - Copyright © 2013–2020 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.core.event
+
+import net.pterodactylus.util.version.*
+
+/**
+ * Event that signals that an update for Sone was found.
+ */
+data class UpdateFoundEvent(val version: Version, val releaseTime: Long, val latestEdition: Long, val isDisruptive: Boolean)
diff --git a/src/main/kotlin/net/pterodactylus/sone/core/event/WebOfTrustAppeared.kt b/src/main/kotlin/net/pterodactylus/sone/core/event/WebOfTrustAppeared.kt
new file mode 100644 (file)
index 0000000..46fc327
--- /dev/null
@@ -0,0 +1,23 @@
+/**
+ * Sone - WebOfTrustAppeared.kt - Copyright © 2019–2020 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.core.event
+
+/**
+ * Event that signals that the web of trust is reachable.
+ */
+class WebOfTrustAppeared
diff --git a/src/main/kotlin/net/pterodactylus/sone/core/event/WebOfTrustDisappeared.kt b/src/main/kotlin/net/pterodactylus/sone/core/event/WebOfTrustDisappeared.kt
new file mode 100644 (file)
index 0000000..8382895
--- /dev/null
@@ -0,0 +1,23 @@
+/**
+ * Sone - WebOfTrustDisappeared.kt - Copyright © 2019–2020 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.core.event
+
+/**
+ * Event that signals that the web of trust is not reachable.
+ */
+class WebOfTrustDisappeared
diff --git a/src/main/kotlin/net/pterodactylus/sone/data/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)
+       }
+}
index 97a54f8..7bba095 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - Fingerprintable.kt - Copyright © 2011–2019 David Roden
+ * Sone - Fingerprintable.kt - Copyright © 2011–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
index 1cbad81..28e456f 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - Identified.kt - Copyright © 2013–2019 David Roden
+ * Sone - Identified.kt - Copyright © 2013–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
diff --git a/src/main/kotlin/net/pterodactylus/sone/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 acace46..dd42f07 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - AlbumBuilder.kt - Copyright © 2013–2019 David Roden
+ * Sone - AlbumBuilder.kt - Copyright © 2013–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
index f153f33..e27528f 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - AlbumBuilderFactory.kt - Copyright © 2013–2019 David Roden
+ * Sone - AlbumBuilderFactory.kt - Copyright © 2013–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
index e61d429..3ccaef4 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - AlbumDatabase.kt - Copyright © 2013–2019 David Roden
+ * Sone - AlbumDatabase.kt - Copyright © 2013–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
index db0146d..b1fcf62 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - AlbumProvider.kt - Copyright © 2013–2019 David Roden
+ * Sone - AlbumProvider.kt - Copyright © 2013–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
index ddc7bb2..3d93ff1 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - AlbumStore.kt - Copyright © 2013–2019 David Roden
+ * Sone - AlbumStore.kt - Copyright © 2013–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
index 17337e2..28417e8 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - Database.kt - Copyright © 2013–2019 David Roden
+ * Sone - Database.kt - Copyright © 2013–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
index c9ddbaa..d104602 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - ImageBuilder.kt - Copyright © 2013–2019 David Roden
+ * Sone - ImageBuilder.kt - Copyright © 2013–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
index f9f8be7..faad2ca 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - ImageBuilderFactory.kt - Copyright © 2013–2019 David Roden
+ * Sone - ImageBuilderFactory.kt - Copyright © 2013–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
index b55c073..81eecc6 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - ImageDatabase.kt - Copyright © 2013–2019 David Roden
+ * Sone - ImageDatabase.kt - Copyright © 2013–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
index 0cde52a..9aa81ac 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - ImageProvider.kt - Copyright © 2013–2019 David Roden
+ * Sone - ImageProvider.kt - Copyright © 2013–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
index 4be9dd9..4bff9e7 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - ImageStore.kt - Copyright © 2013–2019 David Roden
+ * Sone - ImageStore.kt - Copyright © 2013–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
index 542bf78..634eda6 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - PostBuilder.kt - Copyright © 2013–2019 David Roden
+ * Sone - PostBuilder.kt - Copyright © 2013–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
index e20248f..ba1b3a1 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - PostBuilderFactory.kt - Copyright © 2013–2019 David Roden
+ * Sone - PostBuilderFactory.kt - Copyright © 2013–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
index 02b9fd1..cef238a 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - PostDatabase.kt - Copyright © 2013–2019 David Roden
+ * Sone - PostDatabase.kt - Copyright © 2013–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
index abf7ab9..2641e3b 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - PostProvider.kt - Copyright © 2011–2019 David Roden
+ * Sone - PostProvider.kt - Copyright © 2011–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
index d8d7f8d..9ff01b9 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - PostReplyBuilder.kt - Copyright © 2013–2019 David Roden
+ * Sone - PostReplyBuilder.kt - Copyright © 2013–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
index 50b58e8..b2788b5 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - PostReplyBuilderFactory.kt - Copyright © 2013–2019 David Roden
+ * Sone - PostReplyBuilderFactory.kt - Copyright © 2013–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
index 316c772..458ba0c 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - PostReplyDatabase.kt - Copyright © 2013–2019 David Roden
+ * Sone - PostReplyDatabase.kt - Copyright © 2013–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
index cc797d7..c29e443 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - PostReplyProvider.kt - Copyright © 2013–2019 David Roden
+ * Sone - PostReplyProvider.kt - Copyright © 2013–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
 
 package net.pterodactylus.sone.database
 
+import com.google.inject.*
 import net.pterodactylus.sone.data.*
+import net.pterodactylus.sone.database.memory.*
 
 /**
  * Interface for objects that can provide [PostReply]s.
  */
+@ImplementedBy(MemoryDatabase::class)
 interface PostReplyProvider {
 
        fun getPostReply(id: String): PostReply?
index f4839b1..859cbb0 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - PostReplyStore.kt - Copyright © 2013–2019 David Roden
+ * Sone - PostReplyStore.kt - Copyright © 2013–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -26,5 +26,6 @@ interface PostReplyStore {
 
        fun storePostReply(postReply: PostReply)
        fun removePostReply(postReply: PostReply)
+       fun setPostReplyKnown(postReply: PostReply)
 
 }
index 84ea39e..f033d59 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - PostStore.kt - Copyright © 2013–2019 David Roden
+ * Sone - PostStore.kt - Copyright © 2013–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
index bfc74ac..47d0e0d 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - ReplyBuilder.kt - Copyright © 2013–2019 David Roden
+ * Sone - ReplyBuilder.kt - Copyright © 2013–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
index 4156d66..fa57340 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - SoneProvider.kt - Copyright © 2011–2019 David Roden
+ * Sone - SoneProvider.kt - Copyright © 2011–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
diff --git a/src/main/kotlin/net/pterodactylus/sone/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 915c536..f884e34 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - AbstractSoneCommand.kt - Copyright © 2011–2019 David Roden
+ * Sone - AbstractSoneCommand.kt - Copyright © 2011–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -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) {
diff --git a/src/main/kotlin/net/pterodactylus/sone/freenet/AsyncFreenetInterface.kt b/src/main/kotlin/net/pterodactylus/sone/freenet/AsyncFreenetInterface.kt
new file mode 100644 (file)
index 0000000..8219d7e
--- /dev/null
@@ -0,0 +1,27 @@
+package net.pterodactylus.sone.freenet
+
+import freenet.client.*
+import freenet.keys.*
+import kotlinx.coroutines.*
+import net.pterodactylus.sone.core.*
+
+class AsyncFreenetInterface(private val freenetClient: FreenetClient) {
+
+       suspend fun fetchUri(freenetUri: FreenetURI): Fetched {
+               var currentUri = freenetUri
+               var result: FetchResult? = null
+               while (result == null) {
+                       try {
+                               result = withContext(Dispatchers.Default) { freenetClient.fetch(currentUri) }
+                       } catch (fetchException: FetchException) {
+                               if (fetchException.mode == FetchException.FetchExceptionMode.PERMANENT_REDIRECT) {
+                                       currentUri = fetchException.newURI
+                                       continue
+                               } else
+                                       throw fetchException
+                       }
+               }
+               return Fetched(currentUri, result)
+       }
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/freenet/BaseL10nTranslation.kt b/src/main/kotlin/net/pterodactylus/sone/freenet/BaseL10nTranslation.kt
new file mode 100644 (file)
index 0000000..1b41f70
--- /dev/null
@@ -0,0 +1,16 @@
+package net.pterodactylus.sone.freenet
+
+import freenet.l10n.*
+import java.util.*
+
+/**
+ * [Translation] implementation based on Fred’s [BaseL10n].
+ */
+class BaseL10nTranslation(private val baseL10n: BaseL10n) : Translation {
+
+       override val currentLocale: Locale
+               get() = Locale(baseL10n.selectedLanguage.shortCode)
+
+       override fun translate(key: String): String = baseL10n.getString(key)
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/freenet/FreenetClient.kt b/src/main/kotlin/net/pterodactylus/sone/freenet/FreenetClient.kt
new file mode 100644 (file)
index 0000000..8fbbe57
--- /dev/null
@@ -0,0 +1,20 @@
+package net.pterodactylus.sone.freenet
+
+import freenet.client.*
+import freenet.keys.*
+
+/**
+ * Facade for Freenet’s [freenet.client.HighLevelSimpleClient] to allow testing.
+ */
+interface FreenetClient {
+
+       fun fetch(freenetKey: FreenetURI): FetchResult
+
+}
+
+class DefaultFreenetClient(private val highLevelSimpleClient: HighLevelSimpleClient) : FreenetClient {
+
+       override fun fetch(freenetKey: FreenetURI): FetchResult =
+                       highLevelSimpleClient.fetch(freenetKey)
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/freenet/FreenetURIs.kt b/src/main/kotlin/net/pterodactylus/sone/freenet/FreenetURIs.kt
new file mode 100644 (file)
index 0000000..6d40b17
--- /dev/null
@@ -0,0 +1,6 @@
+package net.pterodactylus.sone.freenet
+
+import freenet.keys.*
+import net.pterodactylus.sone.utils.*
+
+val FreenetURI.routingKeyString: String get() = routingKey.asFreenetBase64
diff --git a/src/main/kotlin/net/pterodactylus/sone/freenet/L10nFilter.kt b/src/main/kotlin/net/pterodactylus/sone/freenet/L10nFilter.kt
new file mode 100644 (file)
index 0000000..50d240b
--- /dev/null
@@ -0,0 +1,51 @@
+/*
+ * Sone - L10nFilter.kt - Copyright © 2010–2020 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.freenet
+
+import net.pterodactylus.util.template.*
+import java.text.*
+
+/**
+ * [Filter] implementation replaces [String] values with their
+ * translated equivalents.
+ */
+class L10nFilter(private val translation: Translation) : Filter {
+
+       override fun format(templateContext: TemplateContext?, data: Any?, parameters: Map<String, Any>?): String {
+               val parameterValues = getParameters(data, parameters)
+               val text = getText(data)
+               return if (parameterValues.isEmpty()) {
+                       translation.translate(text)
+               } else
+                       MessageFormat(translation.translate(text), translation.currentLocale).format(parameterValues.toTypedArray())
+       }
+
+       private fun getText(data: Any?) = (data as? L10nText)?.text ?: data.toString()
+
+       private fun getParameters(data: Any?, parameters: Map<String, Any>?) =
+                       if (data is L10nText)
+                               data.parameters
+                       else
+                               (parameters ?: emptyMap()).let { params ->
+                                       generateSequence(0) { it + 1 }
+                                                       .takeWhile { it.toString() in params }
+                                                       .map { params[it.toString()] }
+                                                       .toList()
+                               }
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/freenet/Translation.kt b/src/main/kotlin/net/pterodactylus/sone/freenet/Translation.kt
new file mode 100644 (file)
index 0000000..d583c49
--- /dev/null
@@ -0,0 +1,38 @@
+/**
+ * Sone - Translation.kt - Copyright © 2019–2020 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.freenet
+
+import java.util.*
+
+/**
+ * Facade for Fred’s [freenet.l10n.BaseL10n] object.
+ */
+interface Translation {
+
+       /** The currently selected locale. */
+       val currentLocale: Locale
+
+       /**
+        * Returns the translated string for the given key, defaulting to `""`.
+        *
+        * @param key The key to return the translated string for
+        * @return The translated string, or `""` if there is no translation
+        */
+       fun translate(key: String): String
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/freenet/plugin/FredPluginConnector.kt b/src/main/kotlin/net/pterodactylus/sone/freenet/plugin/FredPluginConnector.kt
new file mode 100644 (file)
index 0000000..98849c4
--- /dev/null
@@ -0,0 +1,35 @@
+/* Fred’s plugin stuff is mostly deprecated. ¯\_(ツ)_/¯ */
+@file:Suppress("DEPRECATION")
+
+package net.pterodactylus.sone.freenet.plugin
+
+import freenet.pluginmanager.*
+import freenet.support.*
+import freenet.support.api.*
+import kotlinx.coroutines.*
+import kotlinx.coroutines.channels.*
+import javax.inject.*
+
+/**
+ * [PluginConnector] implementation that uses a [PluginRespiratorFacade] and coroutines to send
+ * a request to another plugin and receive a reply.
+ */
+class FredPluginConnector @Inject constructor(private val pluginRespiratorFacade: PluginRespiratorFacade) : PluginConnector {
+
+       override suspend fun sendRequest(pluginName: String, fields: SimpleFieldSet, data: Bucket?): PluginReply {
+               val receivedReply = Channel<PluginReply>()
+               val responseReceiver = FredPluginTalker { _, _, responseFields, responseData ->
+                       GlobalScope.launch {
+                               receivedReply.send(PluginReply(responseFields, responseData))
+                       }
+               }
+               try {
+                       val pluginTalker = pluginRespiratorFacade.getPluginTalker(responseReceiver, pluginName, "")
+                       pluginTalker.send(fields, data)
+                       return receivedReply.receive()
+               } catch (e: PluginNotFoundException) {
+                       throw PluginException(cause = e)
+               }
+       }
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/freenet/plugin/PluginConnector.kt b/src/main/kotlin/net/pterodactylus/sone/freenet/plugin/PluginConnector.kt
new file mode 100644 (file)
index 0000000..88feb13
--- /dev/null
@@ -0,0 +1,43 @@
+/*
+ * Sone - PluginConnector.kt - Copyright © 2010–2020 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.freenet.plugin
+
+import freenet.support.*
+import freenet.support.api.*
+
+/**
+ * Interface for talking to other plugins. Other plugins are identified by their
+ * name and a unique connection identifier.
+ */
+interface PluginConnector {
+
+       /**
+        * Sends a message to another plugin running in the same node.
+        *
+        * @param pluginName The fully qualified name of the plugin
+        * @param fields The message being sent
+        * @param data Optional data
+        * @return The reply from the plugin
+        * @throws PluginException if the plugin identified by [pluginName] does not exist
+        */
+       @Throws(PluginException::class)
+       suspend fun sendRequest(pluginName: String, fields: SimpleFieldSet, data: Bucket? = null): PluginReply
+
+}
+
+data class PluginReply(val fields: SimpleFieldSet, val data: Bucket?)
diff --git a/src/main/kotlin/net/pterodactylus/sone/freenet/plugin/PluginException.kt b/src/main/kotlin/net/pterodactylus/sone/freenet/plugin/PluginException.kt
new file mode 100644 (file)
index 0000000..fadd8b5
--- /dev/null
@@ -0,0 +1,25 @@
+/*
+ * Sone - PluginException.kt - Copyright © 2010–2020 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.freenet.plugin
+
+import net.pterodactylus.sone.freenet.wot.*
+
+/**
+ * Exception that signals an error when communicating with a plugin.
+ */
+class PluginException(message: String? = null, cause: Throwable? = null) : WebOfTrustException(message, cause)
diff --git a/src/main/kotlin/net/pterodactylus/sone/freenet/plugin/PluginRespiratorFacade.kt b/src/main/kotlin/net/pterodactylus/sone/freenet/plugin/PluginRespiratorFacade.kt
new file mode 100644 (file)
index 0000000..e1d5591
--- /dev/null
@@ -0,0 +1,66 @@
+/**
+ * Sone - PluginRespiratorFacade.kt - Copyright © 2019–2020 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/* Yes, this handle Fred-based stuff that’s mostly deprecated. */
+@file:Suppress("DEPRECATION")
+
+package net.pterodactylus.sone.freenet.plugin
+
+import freenet.pluginmanager.*
+import freenet.support.*
+import freenet.support.api.*
+import javax.inject.*
+
+/**
+ * Facade for the only method of a [plugin respirator][PluginRespirator] that Sone actually uses,
+ * for easier testing.
+ */
+interface PluginRespiratorFacade {
+
+       @Throws(PluginNotFoundException::class)
+       fun getPluginTalker(pluginTalker: FredPluginTalker, pluginName: String, identifier: String): PluginTalkerFacade
+
+}
+
+/**
+ * Facade for a [plugin talker][PluginTalker], for easier testing.
+ */
+interface PluginTalkerFacade {
+
+       fun send(pluginParameters: SimpleFieldSet, data: Bucket?)
+
+}
+
+/**
+ * Fred-based [PluginRespiratorFacade] implementation that proxies the given real [PluginRespirator].
+ */
+class FredPluginRespiratorFacade @Inject constructor(private val pluginRespirator: PluginRespirator) : PluginRespiratorFacade {
+
+       override fun getPluginTalker(pluginTalker: FredPluginTalker, pluginName: String, identifier: String) =
+                       FredPluginTalkerFacade(pluginRespirator.getPluginTalker(pluginTalker, pluginName, identifier))
+
+}
+
+/**
+ * Fred-based [PluginTalkerFacade] implementation that proxies the given real [PluginTalker].
+ */
+class FredPluginTalkerFacade(private val pluginTalker: PluginTalker) : PluginTalkerFacade {
+
+       override fun send(pluginParameters: SimpleFieldSet, data: Bucket?) =
+                       pluginTalker.send(pluginParameters, data)
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/freenet/wot/Context.kt b/src/main/kotlin/net/pterodactylus/sone/freenet/wot/Context.kt
new file mode 100644 (file)
index 0000000..8b9dda6
--- /dev/null
@@ -0,0 +1,24 @@
+/*
+ * Sone - Context.kt - Copyright © 2014–2020 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.freenet.wot
+
+/**
+ * Custom container for the Web of Trust context. This allows easier
+ * configuration of dependency injection.
+ */
+class Context(val context: String)
diff --git a/src/main/kotlin/net/pterodactylus/sone/freenet/wot/DefaultIdentity.kt b/src/main/kotlin/net/pterodactylus/sone/freenet/wot/DefaultIdentity.kt
new file mode 100644 (file)
index 0000000..88527b3
--- /dev/null
@@ -0,0 +1,115 @@
+/*
+ * Sone - DefaultIdentity.kt - Copyright © 2010–2020 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.freenet.wot
+
+import java.util.Collections.synchronizedMap
+import java.util.Collections.synchronizedSet
+import kotlin.collections.set
+
+/**
+ * A Web of Trust identity.
+ */
+open class DefaultIdentity(private val id: String, private val nickname: String?, private val requestUri: String) : Identity {
+
+       private val contexts = mutableSetOf<String>().synchronized()
+       private val properties = mutableMapOf<String, String>().synchronized()
+       private val trustCache = mutableMapOf<OwnIdentity, Trust>().synchronized()
+
+       override fun getId() = id
+       override fun getNickname() = nickname
+       override fun getRequestUri() = requestUri
+       override fun getContexts() = synchronized(contexts) { contexts.toSet() }
+
+       override fun hasContext(context: String) = context in contexts
+
+       override fun setContexts(contexts: Set<String>) {
+               synchronized(this.contexts) {
+                       this.contexts.clear()
+                       this.contexts.addAll(contexts)
+               }
+       }
+
+       override fun addContext(context: String): Identity = apply {
+               synchronized(this.contexts) {
+                       contexts += context
+               }
+       }
+
+       override fun removeContext(context: String): Identity = apply {
+               synchronized(this.contexts) {
+                       contexts -= context
+               }
+       }
+
+       override fun getProperties() = synchronized(properties) { properties.toMap() }
+
+       override fun setProperties(properties: Map<String, String>) {
+               synchronized(this.properties) {
+                       this.properties.clear()
+                       this.properties.putAll(properties)
+               }
+       }
+
+       override fun getProperty(name: String) = synchronized(properties) { properties[name] }
+
+       override fun setProperty(name: String, value: String): Identity = apply {
+               synchronized(properties) {
+                       properties[name] = value
+               }
+       }
+
+       override fun removeProperty(name: String): Identity = apply {
+               synchronized(properties) {
+                       properties -= name
+               }
+       }
+
+       override fun getTrust(): Map<OwnIdentity, Trust> = synchronized(trustCache) {
+               trustCache.toMap()
+       }
+
+       override fun getTrust(ownIdentity: OwnIdentity) = synchronized(trustCache) {
+               trustCache[ownIdentity]
+       }
+
+       override fun setTrust(ownIdentity: OwnIdentity, trust: Trust) = apply {
+               synchronized(trustCache) {
+                       trustCache[ownIdentity] = trust
+               }
+       }
+
+       override fun removeTrust(ownIdentity: OwnIdentity) = apply {
+               synchronized(trustCache) {
+                       trustCache -= ownIdentity
+               }
+       }
+
+       override fun hashCode() = id.hashCode()
+
+       override fun equals(other: Any?) = if (other !is Identity) {
+               false
+       } else {
+               other.id == getId()
+       }
+
+       override fun toString() = "${javaClass.simpleName}[id=$id,nickname=$nickname,contexts=$contexts,properties=$properties]"
+
+}
+
+private fun <T> Set<T>.synchronized(): MutableSet<T> = synchronizedSet(this)
+private fun <K, V> Map<K, V>.synchronized(): MutableMap<K, V> = synchronizedMap(this)
diff --git a/src/main/kotlin/net/pterodactylus/sone/freenet/wot/DefaultOwnIdentity.kt b/src/main/kotlin/net/pterodactylus/sone/freenet/wot/DefaultOwnIdentity.kt
new file mode 100644 (file)
index 0000000..59b6c17
--- /dev/null
@@ -0,0 +1,38 @@
+/*
+ * Sone - DefaultOwnIdentity.kt - Copyright © 2010–2020 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.freenet.wot
+
+/**
+ * An own identity is an identity that the owner of the node has full control
+ * over.
+ */
+class DefaultOwnIdentity(id: String, nickname: String, requestUri: String, private val insertUri: String) : DefaultIdentity(id, nickname, requestUri), OwnIdentity {
+
+       override fun getInsertUri(): String {
+               return insertUri
+       }
+
+       override fun addContext(context: String) = super.addContext(context) as OwnIdentity
+
+       override fun removeContext(context: String) = super.removeContext(context) as OwnIdentity
+
+       override fun setProperty(name: String, value: String) = super.setProperty(name, value) as OwnIdentity
+
+       override fun removeProperty(name: String) = super.removeProperty(name) as OwnIdentity
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/freenet/wot/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) }
diff --git a/src/main/kotlin/net/pterodactylus/sone/freenet/wot/IdentityChangeEventSender.kt b/src/main/kotlin/net/pterodactylus/sone/freenet/wot/IdentityChangeEventSender.kt
new file mode 100644 (file)
index 0000000..e6a33af
--- /dev/null
@@ -0,0 +1,63 @@
+/*
+ * Sone - IdentityChangeEventSender.kt - Copyright © 2013–2020 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.freenet.wot
+
+import com.google.common.eventbus.*
+import net.pterodactylus.sone.freenet.wot.event.*
+
+/**
+ * Detects changes in [Identity]s trusted by multiple [OwnIdentity]s.
+ *
+ * @see IdentityChangeDetector
+ */
+class IdentityChangeEventSender(private val eventBus: EventBus, private val oldIdentities: Map<OwnIdentity, Collection<Identity>>) {
+
+       fun detectChanges(identities: Map<OwnIdentity, Collection<Identity>>) {
+               val identityChangeDetector = IdentityChangeDetector(oldIdentities.keys)
+               identityChangeDetector.onNewIdentity = addNewOwnIdentityAndItsTrustedIdentities(identities)
+               identityChangeDetector.onRemovedIdentity = removeOwnIdentityAndItsTrustedIdentities(oldIdentities)
+               identityChangeDetector.onUnchangedIdentity = detectChangesInTrustedIdentities(identities, oldIdentities)
+               identityChangeDetector.detectChanges(identities.keys)
+       }
+
+       private fun addNewOwnIdentityAndItsTrustedIdentities(newIdentities: Map<OwnIdentity, Collection<Identity>>) =
+                       { identity: Identity ->
+                               eventBus.post(OwnIdentityAddedEvent(identity as OwnIdentity))
+                               newIdentities[identity]
+                                               ?.map { IdentityAddedEvent(identity, it) }
+                                               ?.forEach(eventBus::post) ?: Unit
+                       }
+
+       private fun removeOwnIdentityAndItsTrustedIdentities(oldIdentities: Map<OwnIdentity, Collection<Identity>>) =
+                       { identity: Identity ->
+                               eventBus.post(OwnIdentityRemovedEvent(identity as OwnIdentity))
+                               oldIdentities[identity]
+                                               ?.map { IdentityRemovedEvent(identity, it) }
+                                               ?.forEach(eventBus::post) ?: Unit
+                       }
+
+       private fun detectChangesInTrustedIdentities(newIdentities: Map<OwnIdentity, Collection<Identity>>, oldIdentities: Map<OwnIdentity, Collection<Identity>>) =
+                       { ownIdentity: Identity ->
+                               val identityChangeDetector = IdentityChangeDetector(oldIdentities[ownIdentity as OwnIdentity]!!)
+                               identityChangeDetector.onNewIdentity = { eventBus.post(IdentityAddedEvent(ownIdentity, it)) }
+                               identityChangeDetector.onRemovedIdentity = { eventBus.post(IdentityRemovedEvent(ownIdentity, it)) }
+                               identityChangeDetector.onChangedIdentity = { eventBus.post(IdentityUpdatedEvent(ownIdentity, it)) }
+                               identityChangeDetector.detectChanges(newIdentities[ownIdentity]!!)
+                       }
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/freenet/wot/IdentityLoader.kt b/src/main/kotlin/net/pterodactylus/sone/freenet/wot/IdentityLoader.kt
new file mode 100644 (file)
index 0000000..f6e1d59
--- /dev/null
@@ -0,0 +1,85 @@
+/*
+ * Sone - IdentityLoader.kt - Copyright © 2013–2020 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.freenet.wot
+
+import com.google.common.base.*
+import com.google.inject.*
+import net.pterodactylus.sone.freenet.plugin.*
+import java.util.concurrent.TimeUnit.*
+import java.util.logging.*
+
+/**
+ * Loads [OwnIdentity]s and the [Identity]s they trust.
+ */
+class IdentityLoader @Inject constructor(private val webOfTrustConnector: WebOfTrustConnector, private val context: Context? = null) {
+
+       private val logger: Logger = Logger.getLogger(IdentityLoader::class.java.name)
+
+       @Throws(WebOfTrustException::class)
+       fun 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
+                                       .also { logger.fine { "Getting trusted identities for ${it.size} own identities..." } }
+                                       .associateWith { ownIdentity ->
+                                               logger.fine { "Getting trusted identities for $ownIdentity..." }
+                                               if (ownIdentity.doesNotHaveCorrectContext()) {
+                                                       logger.fine { "Skipping $ownIdentity because of incorrect context." }
+                                                       emptySet()
+                                               } else {
+                                                       logger.fine { "Loading trusted identities for $ownIdentity from WoT..." }
+                                                       time({ stopwatch, identities -> "Loaded ${identities.size} identities for ${ownIdentity.nickname} in ${stopwatch.elapsed(MILLISECONDS) / 1000.0}s." }) {
+                                                               webOfTrustConnector.loadTrustedIdentities(ownIdentity, context?.context)
+                                                       }
+                                               }
+                                       }
+
+       private fun 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
+
+       private fun <R> time(logMessage: (Stopwatch, R) -> String, loader: () -> R) =
+                       Stopwatch.createStarted().let { stopwatch ->
+                               loader().also { logger.fine(logMessage(stopwatch, it)) }
+                       }
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/freenet/wot/IdentityManager.kt b/src/main/kotlin/net/pterodactylus/sone/freenet/wot/IdentityManager.kt
new file mode 100644 (file)
index 0000000..e90af91
--- /dev/null
@@ -0,0 +1,18 @@
+package net.pterodactylus.sone.freenet.wot
+
+import net.pterodactylus.util.service.Service
+
+import com.google.common.eventbus.EventBus
+import com.google.inject.ImplementedBy
+
+/**
+ * Connects to a [WebOfTrustConnector] and sends identity events to an
+ * [EventBus].
+ */
+@ImplementedBy(IdentityManagerImpl::class)
+interface IdentityManager : Service {
+
+       val isConnected: Boolean
+       val allOwnIdentities: Set<OwnIdentity>
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/freenet/wot/IdentityManagerImpl.kt b/src/main/kotlin/net/pterodactylus/sone/freenet/wot/IdentityManagerImpl.kt
new file mode 100644 (file)
index 0000000..829affc
--- /dev/null
@@ -0,0 +1,137 @@
+/*
+ * Sone - IdentityManagerImpl.kt - Copyright © 2010–2020 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.freenet.wot
+
+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
+ * contexts, and properties. It does so in a way that does not expose errors via
+ * exceptions but it only logs them and tries to return sensible defaults.
+ *
+ *
+ * It is also responsible for polling identities from the Web of Trust plugin
+ * and sending events to the [EventBus] when [Identity]s and
+ * [OwnIdentity]s are discovered or disappearing.
+ */
+@Singleton
+class IdentityManagerImpl @Inject constructor(
+               private val eventBus: EventBus,
+               private val webOfTrustConnector: WebOfTrustConnector,
+               private val identityLoader: IdentityLoader
+) : AbstractService("Sone Identity Manager", false), IdentityManager {
+
+       private val currentOwnIdentities = mutableSetOf<OwnIdentity>()
+       private val strictFiltering = AtomicBoolean(false)
+
+       override val isConnected: Boolean
+               get() = notThrowing { webOfTrustConnector.ping() }
+
+       override val allOwnIdentities: Set<OwnIdentity>
+               get() = synchronized(currentOwnIdentities) {
+                       currentOwnIdentities.toSet()
+               }
+
+       override fun serviceRun() {
+               var oldIdentities = mapOf<OwnIdentity, Collection<Identity>>()
+
+               while (!shouldStop()) {
+                       try {
+                               val currentIdentities = identityLoader.loadAllIdentities().applyStrictFiltering()
+
+                               val identityChangeEventSender = IdentityChangeEventSender(eventBus, oldIdentities)
+                               identityChangeEventSender.detectChanges(currentIdentities)
+
+                               oldIdentities = currentIdentities
+
+                               synchronized(currentOwnIdentities) {
+                                       currentOwnIdentities.clear()
+                                       currentOwnIdentities.addAll(currentIdentities.keys)
+                               }
+                       } catch (wote1: WebOfTrustException) {
+                               logger.log(Level.WARNING, "WoT has disappeared!", wote1)
+                       } catch (e: Exception) {
+                               logger.log(Level.SEVERE, "Uncaught exception in IdentityManager thread!", e)
+                       }
+
+                       /* wait a minute before checking again. */
+                       sleep(SECONDS.toMillis(60))
+               }
+       }
+
+       private 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)
+
+private fun notThrowing(action: () -> Unit): Boolean =
+               try {
+                       action()
+                       true
+               } catch (e: Exception) {
+                       false
+               }
+
+private fun Trust.hasZeroOrPositiveTrust() =
+               if (explicit == null) {
+                       implicit == null || implicit >= 0
+               } else {
+                       explicit >= 0
+               }
diff --git a/src/main/kotlin/net/pterodactylus/sone/freenet/wot/PluginWebOfTrustConnector.kt b/src/main/kotlin/net/pterodactylus/sone/freenet/wot/PluginWebOfTrustConnector.kt
new file mode 100644 (file)
index 0000000..d829734
--- /dev/null
@@ -0,0 +1,131 @@
+/*
+ * Sone - PluginWebOfTrustConnector.kt - Copyright © 2010–2020 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.freenet.wot
+
+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.getLogger
+
+/**
+ * Connector for the Web of Trust plugin.
+ */
+class PluginWebOfTrustConnector @Inject constructor(private val pluginConnector: PluginConnector) : WebOfTrustConnector {
+
+       private val logger: Logger = getLogger(PluginWebOfTrustConnector::class.java.name)
+
+       @Throws(PluginException::class)
+       override fun loadAllOwnIdentities(): Set<OwnIdentity> =
+                       performRequest(SimpleFieldSetBuilder().put("Message", "GetOwnIdentities").get())
+                                       .fields
+                                       .parseIdentities { parseOwnIdentity(it) }
+
+       @Throws(PluginException::class)
+       override fun loadTrustedIdentities(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) }
+
+       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())
+       }
+
+       @Throws(PluginException::class)
+       override fun removeContext(ownIdentity: OwnIdentity, context: String) {
+               performRequest(SimpleFieldSetBuilder().put("Message", "RemoveContext").put("Identity", ownIdentity.id).put("Context", context).get())
+       }
+
+       override fun setProperty(ownIdentity: OwnIdentity, name: String, value: String) {
+               performRequest(SimpleFieldSetBuilder().put("Message", "SetProperty").put("Identity", ownIdentity.id).put("Property", name).put("Value", value).get())
+       }
+
+       override fun removeProperty(ownIdentity: OwnIdentity, name: String) {
+               performRequest(SimpleFieldSetBuilder().put("Message", "RemoveProperty").put("Identity", ownIdentity.id).put("Property", name).get())
+       }
+
+       override fun ping() {
+               performRequest(SimpleFieldSetBuilder().put("Message", "Ping").get())
+       }
+
+       private fun performRequest(fields: SimpleFieldSet): PluginReply {
+               logger.log(Level.FINE, format("Sending FCP Request: %s", fields.get("Message")))
+               return runBlocking {
+                       pluginConnector.sendRequest(WOT_PLUGIN_NAME, fields).also {
+                               logger.log(Level.FINEST, format("Received FCP Response for %s: %s", fields.get("Message"), it.fields.get("Message")))
+                               if ("Error" == it.fields.get("Message")) {
+                                       throw PluginException("Could not perform request for " + fields.get("Message"))
+                               }
+                       }
+               }
+       }
+
+}
+
+private const val WOT_PLUGIN_NAME = "plugins.WebOfTrust.WebOfTrust"
+
+private fun <I> SimpleFieldSet.parseIdentities(parser: SimpleFieldSet.(Int) -> I) =
+               scanPrefix { "Identity$it" }
+                               .map { parser(this, it) }
+                               .toSet()
+
+private fun SimpleFieldSet.parseOwnIdentity(index: Int) =
+               DefaultOwnIdentity(get("Identity$index"), get("Nickname$index"), get("RequestURI$index"), get("InsertURI$index"))
+                               .setContextsAndProperties(this@parseOwnIdentity, index)
+
+private fun SimpleFieldSet.parseTrustedIdentity(index: Int, ownIdentity: OwnIdentity) =
+               DefaultIdentity(get("Identity$index"), get("Nickname$index"), get("RequestURI$index"))
+                               .setContextsAndProperties(this@parseTrustedIdentity, index)
+                               .apply { setTrust(ownIdentity, this@parseTrustedIdentity.parseTrust(index.toString())) }
+
+private fun <I : Identity> I.setContextsAndProperties(simpleFieldSet: SimpleFieldSet, index: Int) = apply {
+       contexts = simpleFieldSet.contexts("Contexts$index.")
+       properties = simpleFieldSet.properties("Properties$index.")
+}
+
+private fun SimpleFieldSet.parseTrust(index: String = "") =
+               Trust(get("Trust$index")?.toIntOrNull(), get("Score$index")?.toIntOrNull(), get("Rank$index")?.toIntOrNull())
+
+private fun SimpleFieldSet.contexts(prefix: String) =
+               scanPrefix { "${prefix}Context$it" }
+                               .map { get("${prefix}Context$it") }
+                               .toSet()
+
+private fun SimpleFieldSet.properties(prefix: String) =
+               scanPrefix { "${prefix}Property${it}.Name" }
+                               .map { get("${prefix}Property${it}.Name") to get("${prefix}Property${it}.Value") }
+                               .toMap()
+
+private fun SimpleFieldSet.scanPrefix(prefix: (Int) -> String) =
+               generateSequence(0, Int::inc)
+                               .takeWhile { get(prefix(it)) != null }
diff --git a/src/main/kotlin/net/pterodactylus/sone/freenet/wot/Trust.kt b/src/main/kotlin/net/pterodactylus/sone/freenet/wot/Trust.kt
new file mode 100644 (file)
index 0000000..779dd20
--- /dev/null
@@ -0,0 +1,23 @@
+/*
+ * Sone - Trust.kt - Copyright © 2010–2020 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.freenet.wot
+
+/**
+ * Container class for trust in the web of trust.
+ */
+data class Trust(val explicit: Int?, val implicit: Int?, val distance: Int?)
diff --git a/src/main/kotlin/net/pterodactylus/sone/freenet/wot/WebOfTrustConnector.kt b/src/main/kotlin/net/pterodactylus/sone/freenet/wot/WebOfTrustConnector.kt
new file mode 100644 (file)
index 0000000..a407e2a
--- /dev/null
@@ -0,0 +1,96 @@
+package net.pterodactylus.sone.freenet.wot
+
+import net.pterodactylus.sone.freenet.plugin.*
+
+/**
+ * Connector for the web of trust plugin.
+ */
+interface WebOfTrustConnector {
+
+       /**
+        * Loads all own identities from the Web of Trust plugin.
+        *
+        * @return All own identity
+        * @throws WebOfTrustException if the own identities can not be loaded
+        */
+       @Throws(WebOfTrustException::class)
+       fun loadAllOwnIdentities(): Set<OwnIdentity>
+
+       /**
+        * Loads all identities that the given identities trusts with a score of
+        * more than 0 and the (optional) given context.
+        *
+        * @param ownIdentity The own identity
+        * @param context The context to filter, or `null`
+        * @return All trusted identities
+        * @throws PluginException if an error occured talking to the Web of Trust plugin
+        */
+       @Throws(PluginException::class)
+       fun loadTrustedIdentities(ownIdentity: OwnIdentity, context: String? = null): Set<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
+        * @param context The context to add
+        * @throws PluginException if an error occured talking to the Web of Trust plugin
+        */
+       @Throws(PluginException::class)
+       fun addContext(ownIdentity: OwnIdentity, context: String)
+
+       /**
+        * Removes the given context from the given identity.
+        *
+        * @param ownIdentity The identity to remove the context from
+        * @param context The context to remove
+        * @throws PluginException if an error occured talking to the Web of Trust plugin
+        */
+       @Throws(PluginException::class)
+       fun removeContext(ownIdentity: OwnIdentity, context: String)
+
+       /**
+        * Sets the property with the given name to the given value.
+        *
+        * @param ownIdentity The identity to set the property on
+        * @param name The name of the property to set
+        * @param value The value to set
+        * @throws PluginException if an error occured talking to the Web of Trust plugin
+        */
+       @Throws(PluginException::class)
+       fun setProperty(ownIdentity: OwnIdentity, name: String, value: String)
+
+       /**
+        * Removes the property with the given name.
+        *
+        * @param ownIdentity The identity to remove the property from
+        * @param name The name of the property to remove
+        * @throws PluginException if an error occured talking to the Web of Trust plugin
+        */
+       @Throws(PluginException::class)
+       fun removeProperty(ownIdentity: OwnIdentity, name: String)
+
+       /**
+        * Pings the Web of Trust plugin. If the plugin can not be reached, a
+        * [PluginException] is thrown.
+        *
+        * @throws PluginException if the plugin is not loaded
+        */
+       @Throws(PluginException::class)
+       fun ping()
+
+       /**
+        * Stops the web of trust connector.
+        */
+       fun stop() = Unit
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/freenet/wot/WebOfTrustException.kt b/src/main/kotlin/net/pterodactylus/sone/freenet/wot/WebOfTrustException.kt
new file mode 100644 (file)
index 0000000..7d730e9
--- /dev/null
@@ -0,0 +1,24 @@
+/*
+ * Sone - WebOfTrustException.kt - Copyright © 2010–2020 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.freenet.wot
+
+/**
+ * Exception that signals an error processing web of trust identities, mostly
+ * when communicating with the web of trust plugin.
+ */
+open class WebOfTrustException(message: String? = null, cause: Throwable?) : Exception(message, cause)
diff --git a/src/main/kotlin/net/pterodactylus/sone/freenet/wot/WebOfTrustPinger.kt b/src/main/kotlin/net/pterodactylus/sone/freenet/wot/WebOfTrustPinger.kt
new file mode 100644 (file)
index 0000000..db9809e
--- /dev/null
@@ -0,0 +1,56 @@
+/**
+ * Sone - WebOfTrustPinger.kt - Copyright © 2019–2020 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.freenet.wot
+
+import com.google.common.eventbus.*
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.sone.freenet.plugin.*
+import net.pterodactylus.sone.utils.*
+import java.util.concurrent.atomic.*
+import java.util.function.*
+import javax.inject.*
+
+/**
+ * [Runnable] that is scheduled via an [Executor][java.util.concurrent.Executor],
+ * checks whether the web of trust plugin can be communicated with, sends
+ * events if its status changes and reschedules itself.
+ */
+class WebOfTrustPinger @Inject constructor(
+               private val eventBus: EventBus,
+               @Named("webOfTrustReacher") private val webOfTrustReacher: Runnable,
+               @Named("webOfTrustReschedule") private val reschedule: Consumer<Runnable>) : Runnable {
+
+       private val lastState = AtomicBoolean(false)
+
+       override fun run() {
+               try {
+                       webOfTrustReacher()
+                       if (!lastState.get()) {
+                               eventBus.post(WebOfTrustAppeared())
+                               lastState.set(true)
+                       }
+               } catch (e: PluginException) {
+                       if (lastState.get()) {
+                               eventBus.post(WebOfTrustDisappeared())
+                               lastState.set(false)
+                       }
+               }
+               reschedule(this)
+       }
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/freenet/wot/event/IdentityAddedEvent.kt b/src/main/kotlin/net/pterodactylus/sone/freenet/wot/event/IdentityAddedEvent.kt
new file mode 100644 (file)
index 0000000..d316841
--- /dev/null
@@ -0,0 +1,25 @@
+/*
+ * Sone - IdentityAddedEvent.kt - Copyright © 2013–2020 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.freenet.wot.event
+
+import net.pterodactylus.sone.freenet.wot.*
+
+/**
+ * Event that signals that an [Identity] was added.
+ */
+data class IdentityAddedEvent(val ownIdentity: OwnIdentity, val identity: Identity)
diff --git a/src/main/kotlin/net/pterodactylus/sone/freenet/wot/event/IdentityRemovedEvent.kt b/src/main/kotlin/net/pterodactylus/sone/freenet/wot/event/IdentityRemovedEvent.kt
new file mode 100644 (file)
index 0000000..72c262c
--- /dev/null
@@ -0,0 +1,26 @@
+/*
+ * Sone - IdentityRemovedEvent.kt - Copyright © 2013–2020 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.freenet.wot.event
+
+import net.pterodactylus.sone.freenet.wot.Identity
+import net.pterodactylus.sone.freenet.wot.OwnIdentity
+
+/**
+ * Event that signals that an [Identity] was removed.
+ */
+data class IdentityRemovedEvent(val ownIdentity: OwnIdentity, val identity: Identity)
diff --git a/src/main/kotlin/net/pterodactylus/sone/freenet/wot/event/IdentityUpdatedEvent.kt b/src/main/kotlin/net/pterodactylus/sone/freenet/wot/event/IdentityUpdatedEvent.kt
new file mode 100644 (file)
index 0000000..edf34c1
--- /dev/null
@@ -0,0 +1,25 @@
+/*
+ * Sone - IdentityUpdatedEvent.kt - Copyright © 2013–2020 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.freenet.wot.event
+
+import net.pterodactylus.sone.freenet.wot.*
+
+/**
+ * Event that signals that an [Identity] was updated.
+ */
+data class IdentityUpdatedEvent(val ownIdentity: OwnIdentity, val identity: Identity)
diff --git a/src/main/kotlin/net/pterodactylus/sone/freenet/wot/event/OwnIdentityAddedEvent.kt b/src/main/kotlin/net/pterodactylus/sone/freenet/wot/event/OwnIdentityAddedEvent.kt
new file mode 100644 (file)
index 0000000..3620237
--- /dev/null
@@ -0,0 +1,25 @@
+/*
+ * Sone - OwnIdentityAddedEvent.kt - Copyright © 2013–2020 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.freenet.wot.event
+
+import net.pterodactylus.sone.freenet.wot.OwnIdentity
+
+/**
+ * Event that signals that an [OwnIdentity] was added.
+ */
+data class OwnIdentityAddedEvent(val ownIdentity: OwnIdentity)
diff --git a/src/main/kotlin/net/pterodactylus/sone/freenet/wot/event/OwnIdentityRemovedEvent.kt b/src/main/kotlin/net/pterodactylus/sone/freenet/wot/event/OwnIdentityRemovedEvent.kt
new file mode 100644 (file)
index 0000000..3f6fdfc
--- /dev/null
@@ -0,0 +1,25 @@
+/*
+ * Sone - OwnIdentityRemovedEvent.kt - Copyright © 2013–2020 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.freenet.wot.event
+
+import net.pterodactylus.sone.freenet.wot.*
+
+/**
+ * Event that signals that an [OwnIdentity] was removed.
+ */
+data class OwnIdentityRemovedEvent(val ownIdentity: OwnIdentity)
index 9b5fa2e..1438c2a 100644 (file)
@@ -5,6 +5,7 @@ import freenet.client.*
 import freenet.clients.http.*
 import freenet.node.*
 import freenet.pluginmanager.*
+import net.pterodactylus.sone.freenet.plugin.*
 import javax.inject.Provider
 import javax.inject.Singleton
 
@@ -14,8 +15,9 @@ import javax.inject.Singleton
 class FreenetModule(private val pluginRespirator: PluginRespirator) : Module {
 
        override fun configure(binder: Binder): Unit = binder.run {
-               bind(PluginRespirator::class.java).toProvider(Provider<PluginRespirator> { pluginRespirator })
-               pluginRespirator.node!!.let { node -> bind(Node::class.java).toProvider(Provider<Node> { node }) }
+               bind(PluginRespiratorFacade::class.java).toProvider(Provider { FredPluginRespiratorFacade(pluginRespirator) }).`in`(Singleton::class.java)
+               bind(PluginConnector::class.java).to(FredPluginConnector::class.java).`in`(Singleton::class.java)
+               bind(Node::class.java).toProvider(Provider { pluginRespirator.node })
                bind(HighLevelSimpleClient::class.java).toProvider(Provider<HighLevelSimpleClient> { pluginRespirator.hlSimpleClient!! })
                bind(ToadletContainer::class.java).toProvider(Provider<ToadletContainer> { pluginRespirator.toadletContainer })
                bind(PageMaker::class.java).toProvider(Provider<PageMaker> { pluginRespirator.pageMaker })
diff --git a/src/main/kotlin/net/pterodactylus/sone/main/SoneModule.kt b/src/main/kotlin/net/pterodactylus/sone/main/SoneModule.kt
new file mode 100644 (file)
index 0000000..a95cf78
--- /dev/null
@@ -0,0 +1,90 @@
+package net.pterodactylus.sone.main
+
+import com.codahale.metrics.*
+import com.google.common.base.*
+import com.google.common.eventbus.*
+import com.google.inject.*
+import com.google.inject.matcher.*
+import com.google.inject.name.Names.*
+import com.google.inject.spi.*
+import net.pterodactylus.sone.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.*
+import net.pterodactylus.util.version.Version
+import java.io.*
+import java.util.concurrent.*
+import java.util.concurrent.Executors.*
+import java.util.logging.*
+import javax.inject.*
+import javax.inject.Singleton
+
+open class SoneModule(private val sonePlugin: SonePlugin, private val eventBus: EventBus) : AbstractModule() {
+
+       override fun configure() {
+               val sonePropertiesFile = File("sone.properties")
+               val firstStart = !sonePropertiesFile.exists()
+               var newConfig = false
+               val configuration = try {
+                       Configuration(MapConfigurationBackend(sonePropertiesFile, false))
+               } catch (ce: ConfigurationException) {
+                       sonePropertiesFile.delete()
+                       newConfig = true
+                       Configuration(MapConfigurationBackend(sonePropertiesFile, true))
+               }
+               val context = Context("Sone")
+               val loaders = configuration.getStringValue("Developer.LoadFromFilesystem")
+                               .getValue(null)
+                               ?.let {
+                                       configuration.getStringValue("Developer.FilesystemPath")
+                                                       .getValue(null)
+                                                       ?.let { DebugLoaders(it) }
+                               }
+
+               bind(Configuration::class.java).toInstance(configuration)
+               bind(EventBus::class.java).toInstance(eventBus)
+               bind(Boolean::class.java).annotatedWith(named("FirstStart")).toInstance(firstStart)
+               bind(Boolean::class.java).annotatedWith(named("NewConfig")).toInstance(newConfig)
+               bind(Context::class.java).toInstance(context)
+               bind(object : TypeLiteral<Optional<Context>>() {}).toInstance(Optional.of(context))
+               bind(SonePlugin::class.java).toInstance(sonePlugin)
+               bind(Version::class.java).toInstance(sonePlugin.version.drop(1).parseVersion())
+               bind(PluginVersion::class.java).toInstance(PluginVersion(sonePlugin.version))
+               bind(PluginYear::class.java).toInstance(PluginYear(sonePlugin.year))
+               bind(PluginHomepage::class.java).toInstance(PluginHomepage(sonePlugin.homepage))
+               bind(Database::class.java).to(MemoryDatabase::class.java).`in`(Singleton::class.java)
+               bind(Translation::class.java).toInstance(BaseL10nTranslation(sonePlugin.l10n().base))
+               loaders?.let { bind(Loaders::class.java).toInstance(it) }
+               bind(MetricRegistry::class.java).`in`(Singleton::class.java)
+               bind(WebOfTrustConnector::class.java).to(PluginWebOfTrustConnector::class.java).`in`(Singleton::class.java)
+               bind(TickerShutdown::class.java).`in`(Singleton::class.java)
+               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>) {
+                               typeEncounter.register(InjectionListener { injectee ->
+                                       logger.fine { "Injecting $injectee..." }
+                                       eventBus.register(injectee)
+                               })
+                       }
+               })
+       }
+
+       @Provides
+       @Singleton
+       @Named("notification")
+       fun getNotificationTicker(): ScheduledExecutorService =
+                       newSingleThreadScheduledExecutor()
+
+       private val logger: Logger = Logging.getLogger(javaClass)
+
+}
+
+private fun String.parseVersion(): Version = Version.parse(this)
diff --git a/src/main/kotlin/net/pterodactylus/sone/main/SoneModuleCreator.kt b/src/main/kotlin/net/pterodactylus/sone/main/SoneModuleCreator.kt
deleted file mode 100644 (file)
index 58c8d9e..0000000
+++ /dev/null
@@ -1,65 +0,0 @@
-package net.pterodactylus.sone.main
-
-import com.google.common.base.*
-import com.google.common.eventbus.*
-import com.google.inject.*
-import com.google.inject.matcher.*
-import com.google.inject.name.Names.*
-import com.google.inject.spi.*
-import net.pterodactylus.sone.database.*
-import net.pterodactylus.sone.database.memory.*
-import net.pterodactylus.sone.freenet.wot.*
-import net.pterodactylus.util.config.*
-import net.pterodactylus.util.config.ConfigurationException
-import net.pterodactylus.util.version.Version
-import java.io.*
-
-class SoneModuleCreator {
-
-       fun createModule(sonePlugin: SonePlugin) = object : AbstractModule() {
-               override fun configure() {
-                       val sonePropertiesFile = File("sone.properties")
-                       val firstStart = !sonePropertiesFile.exists()
-                       var newConfig = false
-                       val configuration = try {
-                               Configuration(MapConfigurationBackend(sonePropertiesFile, false))
-                       } catch (ce: ConfigurationException) {
-                               sonePropertiesFile.delete()
-                               newConfig = true
-                               Configuration(MapConfigurationBackend(sonePropertiesFile, true))
-                       }
-                       val context = Context("Sone")
-                       val loaders = configuration.getStringValue("Developer.LoadFromFilesystem")
-                                       .getValue(null)
-                                       ?.let {
-                                               configuration.getStringValue("Developer.FilesystemPath")
-                                                               .getValue(null)
-                                                               ?.let { DebugLoaders(it) }
-                                       }
-                       val eventBus = EventBus()
-
-                       bind(Configuration::class.java).toInstance(configuration)
-                       bind(EventBus::class.java).toInstance(eventBus)
-                       bind(Boolean::class.java).annotatedWith(named("FirstStart")).toInstance(firstStart)
-                       bind(Boolean::class.java).annotatedWith(named("NewConfig")).toInstance(newConfig)
-                       bind(Context::class.java).toInstance(context)
-                       bind(object : TypeLiteral<Optional<Context>>() {}).toInstance(Optional.of(context))
-                       bind(SonePlugin::class.java).toInstance(sonePlugin)
-                       bind(Version::class.java).toInstance(sonePlugin.version.parseVersion())
-                       bind(PluginVersion::class.java).toInstance(PluginVersion(sonePlugin.version))
-                       bind(PluginYear::class.java).toInstance(PluginYear(sonePlugin.year))
-                       bind(PluginHomepage::class.java).toInstance(PluginHomepage(sonePlugin.homepage))
-                       bind(Database::class.java).to(MemoryDatabase::class.java).`in`(Singleton::class.java)
-                       loaders?.let { bind(Loaders::class.java).toInstance(it) }
-
-                       bindListener(Matchers.any(), object : TypeListener {
-                               override fun <I> hear(typeLiteral: TypeLiteral<I>, typeEncounter: TypeEncounter<I>) {
-                                       typeEncounter.register(InjectionListener { injectee -> eventBus.register(injectee) })
-                               }
-                       })
-               }
-       }
-
-}
-
-private fun String.parseVersion(): Version = Version.parse(this)
diff --git a/src/main/kotlin/net/pterodactylus/sone/main/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)
diff --git a/src/main/kotlin/net/pterodactylus/sone/main/TickerShutdown.kt b/src/main/kotlin/net/pterodactylus/sone/main/TickerShutdown.kt
new file mode 100644 (file)
index 0000000..de15cbe
--- /dev/null
@@ -0,0 +1,36 @@
+/**
+ * Sone - TickerShutdown.kt - Copyright © 2019–2020 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.main
+
+import com.google.common.eventbus.*
+import net.pterodactylus.sone.core.event.*
+import java.util.concurrent.*
+import javax.inject.*
+
+/**
+ * Wrapper around all [tickers][ScheduledExecutorService] used in Sone,
+ * ensuring proper shutdown.
+ */
+class TickerShutdown @Inject constructor(@Named("notification") private val notificationTicker: ScheduledExecutorService) {
+
+       @Subscribe
+       fun shutdown(@Suppress("UNUSED_PARAMETER") shutdown: Shutdown) {
+               notificationTicker.shutdown()
+       }
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/notify/ListNotification.kt b/src/main/kotlin/net/pterodactylus/sone/notify/ListNotification.kt
new file mode 100644 (file)
index 0000000..60d7093
--- /dev/null
@@ -0,0 +1,95 @@
+/*
+ * Sone - ListNotification.kt - Copyright © 2010–2020 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.notify
+
+import net.pterodactylus.util.notify.*
+import net.pterodactylus.util.template.*
+import java.lang.System.*
+import java.util.concurrent.*
+
+/**
+ * Notification that maintains a list of elements.
+ *
+ * @param <T>
+ * The type of the items
+ */
+class ListNotification<T> : TemplateNotification {
+
+       private val key: String
+       private val realElements = CopyOnWriteArrayList<T>()
+
+       val elements: List<T> get() = realElements.toList()
+
+       val isEmpty
+               get() = elements.isEmpty()
+
+       @JvmOverloads
+       constructor(id: String, key: String, template: Template, dismissable: Boolean = true) : super(id, currentTimeMillis(), currentTimeMillis(), dismissable, template) {
+               this.key = key
+               template.initialContext.set(key, realElements)
+       }
+
+       constructor(listNotification: ListNotification<T>) : super(listNotification.id, listNotification.createdTime, listNotification.lastUpdatedTime, listNotification.isDismissable, Template()) {
+               this.key = listNotification.key
+               template.add(listNotification.template)
+               template.initialContext.set(key, realElements)
+       }
+
+       fun setElements(elements: Collection<T>) {
+               realElements.clear()
+               realElements.addAll(elements.distinct())
+               touch()
+       }
+
+       fun add(element: T) {
+               if (element !in realElements) {
+                       realElements.add(element)
+                       touch()
+               }
+       }
+
+       fun remove(element: T) {
+               while (realElements.remove(element)) {
+                       /* do nothing, just remove all instances of the element. */
+               }
+               if (realElements.isEmpty()) {
+                       dismiss()
+               }
+               touch()
+       }
+
+       override fun dismiss() {
+               super.dismiss()
+               realElements.clear()
+       }
+
+       override fun hashCode() =
+                       realElements.fold(super.hashCode()) { hash, element -> hash xor element.hashCode() }
+
+       override fun equals(other: Any?): Boolean {
+               if (other !is ListNotification<*>) {
+                       return false
+               }
+               val listNotification = other as ListNotification<*>?
+               if (!super.equals(listNotification)) {
+                       return false
+               }
+               return (key == listNotification.key) && (realElements == listNotification.realElements)
+       }
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/notify/Notifications.kt b/src/main/kotlin/net/pterodactylus/sone/notify/Notifications.kt
new file mode 100644 (file)
index 0000000..5df9484
--- /dev/null
@@ -0,0 +1,32 @@
+/**
+ * Sone - Notifications.kt - Copyright © 2019–2020 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.notify
+
+import net.pterodactylus.util.notify.*
+
+/**
+ * Returns whether the notification manager contains a notification with the given ID.
+ */
+operator fun NotificationManager.contains(id: String) =
+               getNotification(id) != null
+
+/**
+ * Returns whether the notification manager currently has a “first start” notification.
+ */
+fun NotificationManager.hasFirstStartNotification() =
+               "first-start-notification" in this
diff --git a/src/main/kotlin/net/pterodactylus/sone/template/DurationFormatFilter.kt b/src/main/kotlin/net/pterodactylus/sone/template/DurationFormatFilter.kt
new file mode 100644 (file)
index 0000000..db49ef1
--- /dev/null
@@ -0,0 +1,67 @@
+package net.pterodactylus.sone.template
+
+import net.pterodactylus.util.template.*
+import java.time.*
+
+class DurationFormatFilter : Filter {
+
+       override fun format(templateContext: TemplateContext?, data: Any?, parameters: Map<String, Any?>?): Any? {
+               if (data is Number) {
+                       val scale = parameters?.get("scale")
+                       val duration = when (scale) {
+                               "ms" -> Duration.ofSeconds(data.toLong() / 1_000, (data.toDouble() * 1_000_000 % 1_000_000_000).toLong())
+                               "μs" -> Duration.ofSeconds(data.toLong() / 1_000_000, (data.toDouble() * 1_000 % 1_000_000_000).toLong())
+                               "ns" -> Duration.ofSeconds(data.toLong() / 1_000_000_000, data.toLong() % 1_000_000_000)
+                               else -> Duration.ofSeconds(data.toLong(), (data.toDouble() * 1_000_000_000 % 1_000_000_000).toLong())
+                       }
+                       return FixedDuration.values()
+                                       .map { it to it.number(duration) }
+                                       .firstOrNull { it.second >= 1 }
+                                       ?.let { "${"%.1f".format(it.second)}${it.first.symbol}" }
+                                       ?: "0s"
+               }
+               return data
+       }
+
+}
+
+@Suppress("unused")
+private enum class FixedDuration {
+
+       WEEKS {
+               override fun number(duration: Duration) = DAYS.number(duration) / 7.0
+               override val symbol = "w"
+       },
+       DAYS {
+               override fun number(duration: Duration) = HOURS.number(duration) / 24
+               override val symbol = "d"
+       },
+       HOURS {
+               override fun number(duration: Duration) = MINUTES.number(duration) / 60
+               override val symbol = "h"
+       },
+       MINUTES {
+               override fun number(duration: Duration) = SECONDS.number(duration) / 60
+               override val symbol = "m"
+       },
+       SECONDS {
+               override fun number(duration: Duration) = duration.seconds + duration.nano / 1_000_000_000.0
+               override val symbol = "s"
+       },
+       MILLIS {
+               override fun number(duration: Duration) = duration.nano / 1_000_000.0
+               override val symbol = "ms"
+       },
+       MICROS {
+               override fun number(duration: Duration) = duration.nano / 1_000.0
+               override val symbol = "μs"
+       },
+       NANOS {
+               override fun number(duration: Duration) = duration.nano.toDouble()
+               override val symbol = "ns"
+       };
+
+       abstract fun number(duration: Duration): Double
+       abstract val symbol: String
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/template/HistogramRenderer.kt b/src/main/kotlin/net/pterodactylus/sone/template/HistogramRenderer.kt
new file mode 100644 (file)
index 0000000..dba32b2
--- /dev/null
@@ -0,0 +1,46 @@
+package net.pterodactylus.sone.template
+
+import com.codahale.metrics.*
+import net.pterodactylus.sone.utils.*
+import net.pterodactylus.util.template.*
+
+/**
+ * [Filter] that renders a [Histogram] as a table row.
+ */
+class HistogramRenderer : Filter {
+
+       override fun format(templateContext: TemplateContext, data: Any?, parameters: Map<String, Any?>?): Any? {
+               templateContext["metricName"] = (parameters?.get("name") as String?)?.dotToCamel()?.let { "Page.Metrics.$it.Title" }
+               (data as? Histogram)?.snapshot?.run {
+                       templateContext["count"] = data.count
+                       templateContext["min"] = min
+                       templateContext["max"] = max
+                       templateContext["mean"] = mean
+                       templateContext["median"] = median
+                       templateContext["percentile75"] = get75thPercentile()
+                       templateContext["percentile95"] = get95thPercentile()
+                       templateContext["percentile98"] = get98thPercentile()
+                       templateContext["percentile99"] = get99thPercentile()
+                       templateContext["percentile999"] = get999thPercentile()
+               }
+               return template.render(templateContext)
+       }
+
+}
+
+private val template = """<tr>
+       <td><% metricName|l10n|html></td>
+       <td class="numeric"><% count|html></td>
+       <td class="numeric"><% min|duration scale=='μs'|html></td>
+       <td class="numeric"><% max|duration scale=='μs'|html></td>
+       <td class="numeric"><% mean|duration scale=='μs'|html></td>
+       <td class="numeric"><% median|duration scale=='μs'|html></td>
+       <td class="numeric"><% percentile75|duration scale=='μs'|html></td>
+       <td class="numeric"><% percentile95|duration scale=='μs'|html></td>
+       <td class="numeric"><% percentile98|duration scale=='μs'|html></td>
+       <td class="numeric"><% percentile99|duration scale=='μs'|html></td>
+       <td class="numeric"><% percentile999|duration scale=='μs'|html></td>
+</tr>""".asTemplate()
+
+private fun String.dotToCamel() =
+               split(".").joinToString("", transform = String::capitalize)
diff --git a/src/main/kotlin/net/pterodactylus/sone/template/PostAccessor.kt b/src/main/kotlin/net/pterodactylus/sone/template/PostAccessor.kt
new file mode 100644 (file)
index 0000000..42239aa
--- /dev/null
@@ -0,0 +1,55 @@
+/*
+ * Sone - PostAccessor.kt - Copyright © 2010–2020 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.template
+
+import net.pterodactylus.sone.core.*
+import net.pterodactylus.sone.data.*
+import net.pterodactylus.sone.utils.*
+import net.pterodactylus.util.template.*
+
+/**
+ * Accessor for [Post] objects that adds additional properties:
+ *
+ * * `replies`: All replies to this post, sorted by time, oldest first
+ * * `likes`: All Sones that have liked the post
+ * * `liked`: `true` if the current Sone from the [template context][TemplateContext] has liked the post
+ * * `new`: `true` if the post is not known
+ * * `bookmarked`: `true` if the post is bookmarked
+ */
+class PostAccessor(private val core: Core) : ReflectionAccessor() {
+
+       override fun get(templateContext: TemplateContext?, `object`: Any?, member: String): Any? =
+                       (`object` as Post).let { post ->
+                               when (member) {
+                                       "replies" -> core.getReplies(post)
+                                       "likes" -> core.getLikes(post)
+                                       "liked" -> templateContext.currentSone?.isLikedPostId(post.id) ?: false
+                                       "new" -> !post.isKnown
+                                       "bookmarked" -> core.isBookmarked(post)
+                                       "replySone" -> core.getReplies(post).lastOrNull { it.sone.isLocal }?.sone
+                                                       ?: post.recipient.let { it.takeIf { it.isLocal } }
+                                                       ?: post.sone.takeIf { it.isLocal }
+                                                       ?: templateContext.currentSone
+                                       else -> super.get(templateContext, `object`, member)
+                               }
+                       }
+
+}
+
+private fun Core.getReplies(post: Post) = getReplies(post.id).filter(noFutureReply)
+private val TemplateContext?.currentSone: Sone? get() = this?.get("currentSone") as? Sone
diff --git a/src/main/kotlin/net/pterodactylus/sone/text/SoneMentionDetector.kt b/src/main/kotlin/net/pterodactylus/sone/text/SoneMentionDetector.kt
new file mode 100644 (file)
index 0000000..3e15ed3
--- /dev/null
@@ -0,0 +1,94 @@
+/**
+ * Sone - SoneMentionDetector.kt - Copyright © 2019–2020 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.text
+
+import com.google.common.eventbus.*
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.sone.data.*
+import net.pterodactylus.sone.database.*
+import net.pterodactylus.sone.utils.*
+import javax.inject.*
+
+/**
+ * Listens to [NewPostFoundEvent]s and [NewPostReplyFoundEvent], parses the
+ * texts and emits a [MentionOfLocalSoneFoundEvent] if a [SoneTextParser]
+ * finds a [SonePart] that points to a local [Sone].
+ */
+class SoneMentionDetector @Inject constructor(private val eventBus: EventBus, private val soneTextParser: SoneTextParser, private val postReplyProvider: PostReplyProvider) {
+
+       @Subscribe
+       fun onNewPost(newPostFoundEvent: NewPostFoundEvent) {
+               newPostFoundEvent.post.let { post ->
+                       post.sone.isLocal.onFalse {
+                               if (post.text.hasLinksToLocalSones()) {
+                                       mentionedPosts += post
+                                       eventBus.post(MentionOfLocalSoneFoundEvent(post))
+                               }
+                       }
+               }
+       }
+
+       @Subscribe
+       fun onNewPostReply(event: NewPostReplyFoundEvent) {
+               event.postReply.let { postReply ->
+                       postReply.sone.isLocal.onFalse {
+                               if (postReply.text.hasLinksToLocalSones()) {
+                                       postReply.post
+                                                       .also { mentionedPosts += it }
+                                                       .let(::MentionOfLocalSoneFoundEvent)
+                                                       ?.also(eventBus::post)
+                               }
+                       }
+               }
+       }
+
+       @Subscribe
+       fun onPostRemoved(event: PostRemovedEvent) {
+               unmentionPost(event.post)
+       }
+
+       @Subscribe
+       fun onPostMarkedKnown(event: MarkPostKnownEvent) {
+               unmentionPost(event.post)
+       }
+
+       @Subscribe
+       fun onReplyRemoved(event: PostReplyRemovedEvent) {
+               event.postReply.post.let {
+                       if ((!it.text.hasLinksToLocalSones() || it.isKnown) && (it.replies.filterNot { it == event.postReply }.none { it.text.hasLinksToLocalSones() && !it.isKnown })) {
+                               unmentionPost(it)
+                       }
+               }
+       }
+
+       private fun unmentionPost(post: Post) {
+               if (post in mentionedPosts) {
+                       eventBus.post(MentionOfLocalSoneRemovedEvent(post))
+                       mentionedPosts -= post
+               }
+       }
+
+       private val mentionedPosts = mutableSetOf<Post>()
+
+       private fun String.hasLinksToLocalSones() = soneTextParser.parse(this, null)
+                       .filterIsInstance<SonePart>()
+                       .any { it.sone.isLocal }
+
+       private val Post.replies get() = postReplyProvider.getReplies(id)
+
+}
index 554c19b..24b04fa 100644 (file)
@@ -1,12 +1,12 @@
 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.*
 import net.pterodactylus.sone.text.LinkType.*
 import net.pterodactylus.sone.text.LinkType.USK
+import net.pterodactylus.sone.utils.*
 import org.bitpedia.util.*
 import java.net.*
 import javax.inject.*
@@ -71,7 +71,7 @@ class SoneTextParser @Inject constructor(private val soneProvider: SoneProvider?
                                                        ?.takeIf { (it.size > 1) || ((it.size == 1) && (it.single() != "")) }
                                                        ?.lastOrNull()
                                                        ?: uri.docName
-                                                       ?: "${uri.keyType}@${uri.routingKey.freenetBase64}"
+                                                       ?: "${uri.keyType}@${uri.routingKey.asFreenetBase64}"
                                }.let { FreenetLinkPart(linkWithoutBacklink.removeSuffix("/"), it, trusted = context?.routingKey?.contentEquals(FreenetURI(linkWithoutBacklink).routingKey) == true) }
                        } catch (e: MalformedURLException) {
                                PlainTextPart(linkWithoutBacklink)
@@ -115,7 +115,7 @@ private fun List<Part>.mergeAdjacentPlainTextParts() = fold(emptyList<Part>()) {
 
 private fun List<Part>.removeEmptyPlainTextParts() = filterNot { it == PlainTextPart("") }
 
-private val String.decodedId: String get() = Base64.encode(Base32.decode(this))
+private val String.decodedId: String get() = Base32.decode(this).asFreenetBase64
 private val String.withoutProtocol get() = substring(indexOf("//") + 2)
 private val String.withoutUrlParameters get() = split('?').first()
 
@@ -138,7 +138,7 @@ private val String.withoutMiddlePathComponents
        }
 private val String.withoutTrailingSlash get() = if (endsWith("/")) substring(0, length - 1) else this
 private val SoneTextParserContext.routingKey: ByteArray? get() = postingSone?.routingKey
-private val Sone.routingKey: ByteArray get() = Base64.decode(id)
+private val Sone.routingKey: ByteArray get() = id.fromFreenetBase64
 
 private enum class LinkType(private val scheme: String, private val freenetLink: Boolean) {
 
@@ -199,5 +199,3 @@ private fun isPunctuation(char: Char) = char in punctuationChars
 private val whitespace = Regex("[\\u000a\u0020\u00a0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u200b\u200c\u200d\u202f\u205f\u2060\u2800\u3000]")
 
 private data class NextLink(val position: Int, val linkType: LinkType, val link: String, val remainder: String)
-
-private val ByteArray.freenetBase64 get() = Base64.encode(this)!!
diff --git a/src/main/kotlin/net/pterodactylus/sone/utils/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()
-       }
-
-}
index bfcb319..1d3e097 100644 (file)
@@ -9,3 +9,19 @@ fun <R> Boolean.ifTrue(block: () -> R): R? = if (this) block() else null
  * Returns the value of [block] if `this` is false, returns `null` otherwise.
  */
 fun <R> Boolean.ifFalse(block: () -> R): R? = if (!this) block() else null
+
+/**
+ * Returns `this` but runs the given block if `this`  is `true`.
+ *
+ * @param block The block to run if `this` is `true`
+ * @return `this`
+ */
+fun Boolean.onTrue(block: () -> Unit): Boolean = also { if (this) block() }
+
+/**
+ * Returns `this` but runs the given block if `this`  is `false`.
+ *
+ * @param block The block to run if `this` is `false`
+ * @return `this`
+ */
+fun Boolean.onFalse(block: () -> Unit): Boolean = this.also { if (!this) block() }
diff --git a/src/main/kotlin/net/pterodactylus/sone/utils/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/utils/Freenet.kt b/src/main/kotlin/net/pterodactylus/sone/utils/Freenet.kt
new file mode 100644 (file)
index 0000000..2eb8ab0
--- /dev/null
@@ -0,0 +1,23 @@
+/**
+ * Sone - Freenet.kt - Copyright © 2019–2020 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.utils
+
+import freenet.support.*
+
+val ByteArray.asFreenetBase64: String get() = Base64.encode(this)
+val String.fromFreenetBase64: ByteArray get() = Base64.decode(this)
diff --git a/src/main/kotlin/net/pterodactylus/sone/utils/Functions.kt b/src/main/kotlin/net/pterodactylus/sone/utils/Functions.kt
new file mode 100644 (file)
index 0000000..99f43b6
--- /dev/null
@@ -0,0 +1,9 @@
+package net.pterodactylus.sone.utils
+
+import java.util.function.*
+
+/** Allows easy invocation of Java Consumers. */
+operator fun <T> Consumer<T>.invoke(t: T) = accept(t)
+
+/** Allows easy invocation of Java Runnables. */
+operator fun Runnable.invoke() = run()
index 6d1d413..d9ec162 100644 (file)
@@ -5,3 +5,9 @@ val Any?.unit get() = Unit
 
 fun <T> T?.throwOnNullIf(throwCondition: Boolean, exception: () -> Throwable) =
                if (this == null && throwCondition) throw exception() else this
+
+fun <T> T?.onNull(block: () -> Unit) = this.also {
+       if (this == null) {
+               block()
+       }
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/utils/Renderables.kt b/src/main/kotlin/net/pterodactylus/sone/utils/Renderables.kt
new file mode 100644 (file)
index 0000000..3ba9f8f
--- /dev/null
@@ -0,0 +1,27 @@
+/**
+ * Sone - Renderables.kt - Copyright © 2019–2020 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.utils
+
+import net.pterodactylus.util.io.*
+import java.io.*
+
+/**
+ * Renders the [Renderable] into a [String].
+ */
+fun Renderable.render() =
+               StringWriter().use { it.also(::render) }.toString()
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/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 97e6cfe..651bffd 100644 (file)
@@ -18,21 +18,28 @@ class PageToadletRegistry @Inject constructor(
 ) {
 
        private val pages = mutableListOf<Page<FreenetRequest>>()
+       private val debugPages = mutableListOf<Page<FreenetRequest>>()
        private val registeredToadlets = mutableListOf<PageToadlet>()
        private val registered = AtomicBoolean(false)
+       private val debugActivated = AtomicBoolean(false)
 
        fun addPage(page: Page<FreenetRequest>) {
                if (registered.get()) throw IllegalStateException()
                pages += page
        }
 
+       fun addDebugPage(page: Page<FreenetRequest>) {
+               if (registered.get()) throw IllegalStateException()
+               debugPages += page
+       }
+
        fun registerToadlets() {
                registered.set(true)
                pageMaker.addNavigationCategory("/Sone/index.html", soneMenuName, "$soneMenu.Tooltip", sonePlugin)
                addPages()
        }
 
-       private fun addPages() =
+       private fun addPages(pages: List<Page<FreenetRequest>> = this.pages) =
                        pages
                                        .map { pageToadletFactory.createPageToadlet(it) }
                                        .onEach(registeredToadlets::plusAssign)
@@ -55,4 +62,11 @@ class PageToadletRegistry @Inject constructor(
                registeredToadlets.forEach(toadletContainer::unregister)
        }
 
+       fun activateDebugMode() {
+               if (!debugActivated.get()) {
+                       addPages(debugPages)
+                       debugActivated.set(true)
+               }
+       }
+
 }
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 dd5e9f6..3d87aa7 100644 (file)
@@ -1,7 +1,6 @@
 package net.pterodactylus.sone.web
 
 import com.google.inject.*
-import freenet.l10n.*
 import freenet.support.api.*
 import net.pterodactylus.sone.core.*
 import net.pterodactylus.sone.data.*
@@ -11,6 +10,7 @@ import net.pterodactylus.sone.freenet.wot.*
 import net.pterodactylus.sone.main.*
 import net.pterodactylus.sone.template.*
 import net.pterodactylus.sone.text.*
+import net.pterodactylus.util.notify.*
 import net.pterodactylus.util.template.*
 import javax.inject.*
 import javax.inject.Singleton
@@ -65,6 +65,7 @@ class WebInterfaceModule : AbstractModule() {
                                addFilter("reparse", ReparseFilter())
                                addFilter("unknown", unknownDateFilter)
                                addFilter("format", FormatFilter())
+                               addFilter("duration", DurationFormatFilter())
                                addFilter("sort", CollectionSortFilter())
                                addFilter("image-link", imageLinkFilter)
                                addFilter("replyGroup", ReplyGroupFilter())
@@ -72,6 +73,7 @@ class WebInterfaceModule : AbstractModule() {
                                addFilter("unique", UniqueElementFilter())
                                addFilter("mod", ModFilter())
                                addFilter("paginate", PaginationFilter())
+                               addFilter("render-histogram", HistogramRenderer())
 
                                addProvider(TemplateProvider.TEMPLATE_CONTEXT_PROVIDER)
                                addProvider(loaders.templateProvider)
@@ -98,8 +100,8 @@ class WebInterfaceModule : AbstractModule() {
                        ProfileAccessor(core)
 
        @Provides
-       fun getL10nFilter(l10n: BaseL10n) =
-                       L10nFilter(l10n)
+       fun getL10nFilter(translation: Translation) =
+                       L10nFilter(translation)
 
        @Provides
        fun getParserFilter(core: Core, soneTextParser: SoneTextParser) =
@@ -114,8 +116,8 @@ class WebInterfaceModule : AbstractModule() {
                        LinkedElementsFilter(elementLoader)
 
        @Provides
-       fun getUnknownDateFilter(l10n: BaseL10n) =
-                       UnknownDateFilter(l10n, "View.Sone.Text.UnknownDate")
+       fun getUnknownDateFilter(translation: Translation) =
+                       UnknownDateFilter(translation, "View.Sone.Text.UnknownDate")
 
        @Provides
        fun getImageLinkFilter(core: Core) =
@@ -125,4 +127,9 @@ class WebInterfaceModule : AbstractModule() {
        @Named("toadletPathPrefix")
        fun getPathPrefix(): String = "/Sone/"
 
+       @Provides
+       @Singleton
+       fun getNotificationManager() =
+                       NotificationManager()
+
 }
index 79e66ad..28af3e3 100644 (file)
@@ -19,7 +19,7 @@ class CreatePostAjaxPage @Inject constructor(webInterface: WebInterface) : Logge
                                        ?.let { text ->
                                                val sender = request.parameters["sender"].emptyToNull?.let(core::getSone) ?: currentSone
                                                val recipient = request.parameters["recipient"]?.let(core::getSone)
-                                               core.createPost(sender, recipient.asOptional(), text).let { post ->
+                                               core.createPost(sender, recipient, text).let { post ->
                                                        createSuccessJsonObject().apply {
                                                                put("postId", post.id)
                                                                put("sone", sender.id)
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/ajax/DistrustAjaxPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/ajax/DistrustAjaxPage.kt
deleted file mode 100644 (file)
index cbeed6e..0000000
+++ /dev/null
@@ -1,29 +0,0 @@
-package net.pterodactylus.sone.web.ajax
-
-import net.pterodactylus.sone.core.*
-import net.pterodactylus.sone.data.*
-import net.pterodactylus.sone.utils.*
-import net.pterodactylus.sone.web.*
-import net.pterodactylus.sone.web.page.*
-import javax.inject.*
-
-/**
- * AJAX page that lets the user distrust a Sone.
- *
- * @see Core.distrustSone(Sone, Sone)
- */
-@ToadletPath("distrustSone.ajax")
-class DistrustAjaxPage @Inject constructor(webInterface: WebInterface) : LoggedInJsonPage(webInterface) {
-
-       override fun createJsonObject(currentSone: Sone, request: FreenetRequest) =
-                       request.parameters["sone"]
-                                       ?.let(core::getSone)
-                                       ?.let { sone ->
-                                               createSuccessJsonObject()
-                                                               .put("trustValue", core.preferences.negativeTrust)
-                                                               .also {
-                                                                       core.distrustSone(currentSone, sone)
-                                                               }
-                                       } ?: createErrorJsonObject("invalid-sone-id")
-
-}
index db6e5c5..c60fe56 100644 (file)
@@ -3,8 +3,7 @@ package net.pterodactylus.sone.web.ajax
 import net.pterodactylus.sone.data.Sone
 import net.pterodactylus.sone.data.SoneOptions
 import net.pterodactylus.sone.main.SonePlugin
-import net.pterodactylus.sone.utils.jsonArray
-import net.pterodactylus.sone.utils.jsonObject
+import net.pterodactylus.sone.utils.*
 import net.pterodactylus.sone.web.WebInterface
 import net.pterodactylus.sone.web.page.*
 import net.pterodactylus.util.notify.Notification
@@ -22,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 ->
@@ -74,5 +73,3 @@ private val SoneOptions?.asJsonObject
                                "ShowNotification/NewReplies" to options.isShowNewReplyNotifications
                )
        } ?: jsonObject {}
-
-private fun Notification.render() = StringWriter().use { it.also { render(it) } }.toString()
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 41188ea..e2c41b6 100644 (file)
@@ -16,6 +16,6 @@ class GetTranslationAjaxPage @Inject constructor(webInterface: WebInterface) : J
 
        override fun createJsonObject(request: FreenetRequest) =
                        createSuccessJsonObject()
-                                       .put("value", webInterface.l10n.getString(request.parameters["key"]))
+                                       .put("value", webInterface.translation.translate(request.parameters["key"] ?: ""))
 
 }
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 {
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/ajax/TrustAjaxPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/ajax/TrustAjaxPage.kt
deleted file mode 100644 (file)
index 9f0de87..0000000
+++ /dev/null
@@ -1,24 +0,0 @@
-package net.pterodactylus.sone.web.ajax
-
-import net.pterodactylus.sone.data.*
-import net.pterodactylus.sone.utils.*
-import net.pterodactylus.sone.web.*
-import net.pterodactylus.sone.web.page.*
-import javax.inject.*
-
-/**
- * AJAX page that lets the user trust a Sone.
- *
- * @see net.pterodactylus.sone.core.Core.trustSone
- */
-@ToadletPath("trustSone.ajax")
-class TrustAjaxPage @Inject constructor(webInterface: WebInterface) : LoggedInJsonPage(webInterface) {
-
-       override fun createJsonObject(currentSone: Sone, request: FreenetRequest) =
-                       request.parameters["sone"]
-                                       ?.let(core::getSone)
-                                       ?.let { core.trustSone(currentSone, it) }
-                                       ?.let { createSuccessJsonObject().put("trustValue", core.preferences.positiveTrust) }
-                                       ?: createErrorJsonObject("invalid-sone-id")
-
-}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/ajax/UntrustAjaxPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/ajax/UntrustAjaxPage.kt
deleted file mode 100644 (file)
index e4b5edb..0000000
+++ /dev/null
@@ -1,22 +0,0 @@
-package net.pterodactylus.sone.web.ajax
-
-import net.pterodactylus.sone.data.Sone
-import net.pterodactylus.sone.utils.parameters
-import net.pterodactylus.sone.web.WebInterface
-import net.pterodactylus.sone.web.page.*
-import javax.inject.Inject
-
-/**
- * AJAX page that lets the user [untrust][net.pterodactylus.sone.core.Core.untrustSone] a [Sone].
- */
-@ToadletPath("untrustSone.ajax")
-class UntrustAjaxPage @Inject constructor(webInterface: WebInterface) : LoggedInJsonPage(webInterface) {
-
-       override fun createJsonObject(currentSone: Sone, request: FreenetRequest) =
-                       request.parameters["sone"]
-                                       ?.let(core::getSone)
-                                       ?.also { core.untrustSone(currentSone, it) }
-                                       ?.let { createSuccessJsonObject() }
-                                       ?: createErrorJsonObject("invalid-sone-id")
-
-}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/notification/ConfigNotReadHandler.kt b/src/main/kotlin/net/pterodactylus/sone/web/notification/ConfigNotReadHandler.kt
new file mode 100644 (file)
index 0000000..6092311
--- /dev/null
@@ -0,0 +1,35 @@
+/**
+ * Sone - ConfigNotReadHandler.kt - Copyright © 2019–2020 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.notification
+
+import com.google.common.eventbus.*
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.util.notify.*
+import javax.inject.*
+
+/**
+ * Handler for [ConfigNotRead] events.
+ */
+class ConfigNotReadHandler @Inject constructor(private val notificationManager: NotificationManager, @Named("configNotRead") private val notification: TemplateNotification) {
+
+       @Subscribe
+       fun configNotRead(@Suppress("UNUSED_PARAMETER") configNotRead: ConfigNotRead) {
+               notificationManager.addNotification(notification)
+       }
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/notification/FirstStartHandler.kt b/src/main/kotlin/net/pterodactylus/sone/web/notification/FirstStartHandler.kt
new file mode 100644 (file)
index 0000000..3dc8689
--- /dev/null
@@ -0,0 +1,35 @@
+/**
+ * Sone - FirstStartHandler.kt - Copyright © 2019–2020 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.notification
+
+import com.google.common.eventbus.*
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.util.notify.*
+import javax.inject.*
+
+/**
+ * Handles the notification shown on first start of Sone.
+ */
+class FirstStartHandler @Inject constructor(private val notificationManager: NotificationManager, @Named("firstStart") private val notification: TemplateNotification) {
+
+       @Subscribe
+       fun firstStart(@Suppress("UNUSED_PARAMETER") firstStart: FirstStart) {
+               notificationManager.addNotification(notification)
+       }
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/notification/ImageInsertHandler.kt b/src/main/kotlin/net/pterodactylus/sone/web/notification/ImageInsertHandler.kt
new file mode 100644 (file)
index 0000000..bab7599
--- /dev/null
@@ -0,0 +1,66 @@
+/**
+ * Sone - ImageInsertHandler.kt - Copyright © 2019–2020 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.notification
+
+import com.google.common.eventbus.*
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.sone.data.*
+import net.pterodactylus.sone.notify.*
+import net.pterodactylus.util.notify.*
+import javax.inject.*
+
+/**
+ * Notification handler for the various image-insert-related events.
+ *
+ * @see ImageInsertStartedEvent
+ * @see ImageInsertAbortedEvent
+ * @see ImageInsertFailedEvent
+ * @see ImageInsertFinishedEvent
+ */
+class ImageInsertHandler @Inject constructor(
+               private val notificationManager: NotificationManager,
+               @Named("imageInserting") private val imageInsertingNotification: ListNotification<Image>,
+               @Named("imageFailed") private val imageFailedNotification: ListNotification<Image>,
+               @Named("imageInserted") private val imageInsertedNotification: ListNotification<Image>) {
+
+       @Subscribe
+       fun imageInsertStarted(imageInsertStartedEvent: ImageInsertStartedEvent) {
+               imageInsertingNotification.add(imageInsertStartedEvent.image)
+               notificationManager.addNotification(imageInsertingNotification)
+       }
+
+       @Subscribe
+       fun imageInsertAborted(imageInsertAbortedEvent: ImageInsertAbortedEvent) {
+               imageInsertingNotification.remove(imageInsertAbortedEvent.image)
+       }
+
+       @Subscribe
+       fun imageInsertFailed(imageInsertFailedEvent: ImageInsertFailedEvent) {
+               imageInsertingNotification.remove(imageInsertFailedEvent.image)
+               imageFailedNotification.add(imageInsertFailedEvent.image)
+               notificationManager.addNotification(imageFailedNotification)
+       }
+
+       @Subscribe
+       fun imageInsertFinished(imageInsertFinishedEvent: ImageInsertFinishedEvent) {
+               imageInsertingNotification.remove(imageInsertFinishedEvent.image)
+               imageInsertedNotification.add(imageInsertFinishedEvent.image)
+               notificationManager.addNotification(imageInsertedNotification)
+       }
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/notification/LocalPostHandler.kt b/src/main/kotlin/net/pterodactylus/sone/web/notification/LocalPostHandler.kt
new file mode 100644 (file)
index 0000000..e7b8f05
--- /dev/null
@@ -0,0 +1,59 @@
+/**
+ * Sone - LocalPostHandler.kt - Copyright © 2019–2020 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.notification
+
+import com.google.common.eventbus.*
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.sone.data.*
+import net.pterodactylus.sone.notify.*
+import net.pterodactylus.util.notify.*
+import javax.inject.*
+
+/**
+ * Handler for local posts.
+ */
+class LocalPostHandler @Inject constructor(private val notificationManager: NotificationManager, @Named("localPost") private val notification: ListNotification<Post>) {
+
+       @Subscribe
+       fun newPostFound(newPostFoundEvent: NewPostFoundEvent) {
+               newPostFoundEvent.post.onLocal { post ->
+                       notification.add(post)
+                       if (!notificationManager.hasFirstStartNotification()) {
+                               notificationManager.addNotification(notification)
+                       }
+               }
+       }
+
+       @Subscribe
+       fun postRemoved(postRemovedEvent: PostRemovedEvent) {
+               postRemovedEvent.post.onLocal { post ->
+                       notification.remove(post)
+               }
+       }
+
+       @Subscribe
+       fun postMarkedAsKnown(markPostKnownEvent: MarkPostKnownEvent) {
+               markPostKnownEvent.post.onLocal { post ->
+                       notification.remove(post)
+               }
+       }
+
+}
+
+private fun Post.onLocal(action: (Post) -> Unit) =
+               if (sone.isLocal) action(this) else Unit
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/notification/LocalReplyHandler.kt b/src/main/kotlin/net/pterodactylus/sone/web/notification/LocalReplyHandler.kt
new file mode 100644 (file)
index 0000000..7e392d0
--- /dev/null
@@ -0,0 +1,54 @@
+/**
+ * Sone - LocalReplyHandler.kt - Copyright © 2019–2020 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.notification
+
+import com.google.common.eventbus.*
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.sone.data.*
+import net.pterodactylus.sone.notify.*
+import net.pterodactylus.util.notify.*
+import javax.inject.*
+
+/**
+ * Handler for local replies.
+ */
+class LocalReplyHandler @Inject constructor(private val notificationManager: NotificationManager, @Named("localReply") private val notification: ListNotification<PostReply>) {
+
+       @Subscribe
+       fun newReplyFound(event: NewPostReplyFoundEvent) =
+                       event.postReply.onLocal {
+                               notification.add(it)
+                               if (!notificationManager.hasFirstStartNotification()) {
+                                       notificationManager.addNotification(notification)
+                               }
+                       }
+
+       @Subscribe
+       fun replyRemoved(event: PostReplyRemovedEvent) {
+               notification.remove(event.postReply)
+       }
+
+       @Subscribe
+       fun replyMarkedAsKnown(event: MarkPostReplyKnownEvent) {
+               notification.remove(event.postReply)
+       }
+
+}
+
+private fun PostReply.onLocal(action: (PostReply) -> Unit) =
+               if (sone.isLocal) action(this) else Unit
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/notification/MarkPostKnownDuringFirstStartHandler.kt b/src/main/kotlin/net/pterodactylus/sone/web/notification/MarkPostKnownDuringFirstStartHandler.kt
new file mode 100644 (file)
index 0000000..57962c2
--- /dev/null
@@ -0,0 +1,43 @@
+/**
+ * Sone - MarkPostKnownDuringFirstStartHandler.kt - Copyright © 2019–2020 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.notification
+
+import com.google.common.eventbus.*
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.sone.data.*
+import net.pterodactylus.sone.notify.*
+import net.pterodactylus.sone.utils.*
+import net.pterodactylus.util.notify.*
+import java.util.function.*
+import javax.inject.*
+
+/**
+ * Handler that marks a [new][NewPostFoundEvent] [post][Post] as known while
+ * the [notification manager][NotificationManager] shows a [first start notification]
+ * [NotificationManager.hasFirstStartNotification].
+ */
+class MarkPostKnownDuringFirstStartHandler @Inject constructor(private val notificationManager: NotificationManager, private val markPostAsKnown: Consumer<Post>) {
+
+       @Subscribe
+       fun newPostFound(newPostFoundEvent: NewPostFoundEvent) {
+               if (notificationManager.hasFirstStartNotification()) {
+                       markPostAsKnown(newPostFoundEvent.post)
+               }
+       }
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/notification/MarkPostReplyKnownDuringFirstStartHandler.kt b/src/main/kotlin/net/pterodactylus/sone/web/notification/MarkPostReplyKnownDuringFirstStartHandler.kt
new file mode 100644 (file)
index 0000000..64285ee
--- /dev/null
@@ -0,0 +1,43 @@
+/**
+ * Sone - MarkPostReplyKnownDuringFirstStartHandler.kt - Copyright © 2020 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.notification
+
+import com.google.common.eventbus.*
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.sone.data.*
+import net.pterodactylus.sone.notify.*
+import net.pterodactylus.sone.utils.*
+import net.pterodactylus.util.notify.*
+import java.util.function.*
+import javax.inject.*
+
+/**
+ * Handler that marks post replies [as known][net.pterodactylus.sone.core.Core.markReplyKnown]
+ * while the [first start notification][net.pterodactylus.util.notify.NotificationManager.hasFirstStartNotification]
+ * is shown.
+ */
+class MarkPostReplyKnownDuringFirstStartHandler @Inject constructor(private val notificationManager: NotificationManager, private val markAsKnown: Consumer<PostReply>) {
+
+       @Subscribe
+       fun newPostReply(event: NewPostReplyFoundEvent) {
+               if (notificationManager.hasFirstStartNotification()) {
+                       markAsKnown(event.postReply)
+               }
+       }
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/notification/NewRemotePostHandler.kt b/src/main/kotlin/net/pterodactylus/sone/web/notification/NewRemotePostHandler.kt
new file mode 100644 (file)
index 0000000..91eaae0
--- /dev/null
@@ -0,0 +1,53 @@
+/**
+ * Sone - NewRemotePostHandler.kt - Copyright © 2019–2020 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.notification
+
+import com.google.common.eventbus.*
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.sone.data.*
+import net.pterodactylus.sone.notify.*
+import net.pterodactylus.util.notify.*
+import javax.inject.*
+
+/**
+ * Handler for [NewPostFoundEvent]s that adds the new post to the “new posts” notification and
+ * displays the notification if the “first start” notification is not being shown.
+ */
+class NewRemotePostHandler @Inject constructor(private val notificationManager: NotificationManager, @Named("newRemotePost") private val notification: ListNotification<Post>) {
+
+       @Subscribe
+       fun newPostFound(newPostFoundEvent: NewPostFoundEvent) {
+               if (!newPostFoundEvent.post.sone.isLocal) {
+                       notification.add(newPostFoundEvent.post)
+                       if (!notificationManager.hasFirstStartNotification()) {
+                               notificationManager.addNotification(notification)
+                       }
+               }
+       }
+
+       @Subscribe
+       fun postRemoved(event: PostRemovedEvent) {
+               notification.remove(event.post)
+       }
+
+       @Subscribe
+       fun postMarkedKnown(event: MarkPostKnownEvent) {
+               notification.remove(event.post)
+       }
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/notification/NewSoneHandler.kt b/src/main/kotlin/net/pterodactylus/sone/web/notification/NewSoneHandler.kt
new file mode 100644 (file)
index 0000000..eb384c7
--- /dev/null
@@ -0,0 +1,50 @@
+/**
+ * Sone - NewSoneHandler.kt - Copyright © 2019–2020 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.notification
+
+import com.google.common.eventbus.*
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.sone.data.*
+import net.pterodactylus.sone.notify.*
+import net.pterodactylus.util.notify.*
+import javax.inject.*
+
+/**
+ * Notification handler for “new Sone discovered” events.
+ */
+class NewSoneHandler @Inject constructor(private val notificationManager: NotificationManager, @Named("newSone") private val notification: ListNotification<Sone>) {
+
+       @Subscribe
+       fun newSoneFound(newSoneFoundEvent: NewSoneFoundEvent) {
+               if (!notificationManager.hasFirstStartNotification()) {
+                       notification.add(newSoneFoundEvent.sone)
+                       notificationManager.addNotification(notification)
+               }
+       }
+
+       @Subscribe
+       fun markedSoneKnown(markSoneKnownEvent: MarkSoneKnownEvent) {
+               notification.remove(markSoneKnownEvent.sone)
+       }
+
+       @Subscribe
+       fun soneRemoved(soneRemovedEvent: SoneRemovedEvent) {
+               notification.remove(soneRemovedEvent.sone)
+       }
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/notification/NewVersionHandler.kt b/src/main/kotlin/net/pterodactylus/sone/web/notification/NewVersionHandler.kt
new file mode 100644 (file)
index 0000000..f48d89c
--- /dev/null
@@ -0,0 +1,39 @@
+/**
+ * Sone - NewVersionHandler.kt - Copyright © 2019–2020 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.notification
+
+import com.google.common.eventbus.*
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.util.notify.*
+import javax.inject.*
+
+/**
+ * Handler for the “new version” notification.
+ */
+class NewVersionHandler @Inject constructor(private val notificationManager: NotificationManager, @Named("newVersion") private val notification: TemplateNotification) {
+
+       @Subscribe
+       fun newVersionFound(updateFoundEvent: UpdateFoundEvent) {
+               notification.set("latestVersion", updateFoundEvent.version)
+               notification.set("releaseTime", updateFoundEvent.releaseTime)
+               notification.set("latestEdition", updateFoundEvent.latestEdition)
+               notification.set("disruptive", updateFoundEvent.isDisruptive)
+               notificationManager.addNotification(notification)
+       }
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/notification/NotificationHandler.kt b/src/main/kotlin/net/pterodactylus/sone/web/notification/NotificationHandler.kt
new file mode 100644 (file)
index 0000000..a24a6d8
--- /dev/null
@@ -0,0 +1,49 @@
+/**
+ * Sone - NotificationHandler.kt - Copyright © 2019–2020 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.notification
+
+import net.pterodactylus.sone.freenet.wot.*
+import net.pterodactylus.sone.text.*
+import javax.inject.*
+
+/**
+ * Container that causes notification handlers to be created and (more importantly) registered
+ * on creation with the event bus.
+ */
+@Suppress("UNUSED_PARAMETER")
+class NotificationHandler @Inject constructor(
+               markPostKnownDuringFirstStartHandler: MarkPostKnownDuringFirstStartHandler,
+               markPostReplyKnownDuringFirstStartHandler: MarkPostReplyKnownDuringFirstStartHandler,
+               newSoneHandler: NewSoneHandler,
+               newRemotePostHandler: NewRemotePostHandler,
+               remotePostReplyHandler: RemotePostReplyHandler,
+               soneLockedOnStartupHandler: SoneLockedOnStartupHandler,
+               soneLockedHandler: SoneLockedHandler,
+               localPostHandler: LocalPostHandler,
+               localReplyHandler: LocalReplyHandler,
+               newVersionHandler: NewVersionHandler,
+               imageInsertHandler: ImageInsertHandler,
+               firstStartHandler: FirstStartHandler,
+               configNotReadHandler: ConfigNotReadHandler,
+               startupHandler: StartupHandler,
+               webOfTrustPinger: WebOfTrustPinger,
+               webOfTrustHandler: WebOfTrustHandler,
+               soneMentionDetector: SoneMentionDetector,
+               soneMentionedHandler: SoneMentionedHandler,
+               soneInsertHandler: SoneInsertHandler
+)
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/notification/NotificationHandlerModule.kt b/src/main/kotlin/net/pterodactylus/sone/web/notification/NotificationHandlerModule.kt
new file mode 100644 (file)
index 0000000..18aefd6
--- /dev/null
@@ -0,0 +1,192 @@
+/**
+ * Sone - NotificationHandlerModule.kt - Copyright © 2019–2020 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.notification
+
+import com.google.inject.*
+import com.google.inject.binder.*
+import net.pterodactylus.sone.core.*
+import net.pterodactylus.sone.data.*
+import net.pterodactylus.sone.freenet.wot.*
+import net.pterodactylus.sone.main.*
+import net.pterodactylus.sone.notify.*
+import net.pterodactylus.sone.text.*
+import net.pterodactylus.util.notify.*
+import java.util.concurrent.*
+import java.util.concurrent.TimeUnit.*
+import java.util.function.*
+import javax.inject.*
+import javax.inject.Singleton
+
+/**
+ * Guice module for creating all notification handlers.
+ */
+class NotificationHandlerModule : AbstractModule() {
+
+       override fun configure() {
+               bind(NotificationHandler::class.java).`in`(Singleton::class.java)
+               bind<MarkPostKnownDuringFirstStartHandler>().asSingleton()
+               bind<MarkPostReplyKnownDuringFirstStartHandler>().asSingleton()
+               bind<SoneLockedOnStartupHandler>().asSingleton()
+               bind<NewSoneHandler>().asSingleton()
+               bind<NewRemotePostHandler>().asSingleton()
+               bind<RemotePostReplyHandler>().asSingleton()
+               bind<SoneLockedHandler>().asSingleton()
+               bind<LocalPostHandler>().asSingleton()
+               bind<LocalReplyHandler>().asSingleton()
+               bind<NewVersionHandler>().asSingleton()
+               bind<ImageInsertHandler>().asSingleton()
+               bind<FirstStartHandler>().asSingleton()
+               bind<ConfigNotReadHandler>().asSingleton()
+               bind<StartupHandler>().asSingleton()
+               bind<WebOfTrustHandler>().asSingleton()
+               bind<SoneMentionDetector>().asSingleton()
+               bind<SoneMentionedHandler>().asSingleton()
+               bind<SoneInsertHandler>().asSingleton()
+       }
+
+       @Provides
+       fun getMarkPostKnownHandler(core: Core): Consumer<Post> = Consumer { core.markPostKnown(it) }
+
+       @Provides
+       fun getMarkPostReplyKnownHandler(core: Core): Consumer<PostReply> = Consumer { core.markReplyKnown(it) }
+
+       @Provides
+       @Singleton
+       @Named("soneLockedOnStartup")
+       fun getSoneLockedOnStartupNotification(loaders: Loaders) =
+                       ListNotification<Sone>("sone-locked-on-startup", "sones", loaders.loadTemplate("/templates/notify/soneLockedOnStartupNotification.html"))
+
+       @Provides
+       @Named("newSone")
+       fun getNewSoneNotification(loaders: Loaders) =
+                       ListNotification<Sone>("new-sone-notification", "sones", loaders.loadTemplate("/templates/notify/newSoneNotification.html"), dismissable = false)
+
+       @Provides
+       @Singleton
+       @Named("newRemotePost")
+       fun getNewPostNotification(loaders: Loaders) =
+                       ListNotification<Post>("new-post-notification", "posts", loaders.loadTemplate("/templates/notify/newPostNotification.html"), dismissable = false)
+
+       @Provides
+       @Singleton
+       @Named("newRemotePostReply")
+       fun getNewRemotePostReplyNotification(loaders: Loaders) =
+                       ListNotification<PostReply>("new-reply-notification", "replies", loaders.loadTemplate("/templates/notify/newReplyNotification.html"), dismissable = false)
+
+       @Provides
+       @Singleton
+       @Named("soneLocked")
+       fun getSoneLockedNotification(loaders: Loaders) =
+                       ListNotification<Sone>("sones-locked-notification", "sones", loaders.loadTemplate("/templates/notify/lockedSonesNotification.html"), dismissable = true)
+
+       @Provides
+       @Singleton
+       @Named("localPost")
+       fun getLocalPostNotification(loaders: Loaders) =
+                       ListNotification<Post>("local-post-notification", "posts", loaders.loadTemplate("/templates/notify/newPostNotification.html"), dismissable = false)
+
+       @Provides
+       @Singleton
+       @Named("localReply")
+       fun getLocalReplyNotification(loaders: Loaders) =
+                       ListNotification<PostReply>("local-reply-notification", "replies", loaders.loadTemplate("/templates/notify/newReplyNotification.html"), dismissable = false)
+
+       @Provides
+       @Singleton
+       @Named("newVersion")
+       fun getNewVersionNotification(loaders: Loaders) =
+                       TemplateNotification("new-version-notification", loaders.loadTemplate("/templates/notify/newVersionNotification.html"))
+
+       @Provides
+       @Singleton
+       @Named("imageInserting")
+       fun getImageInsertingNotification(loaders: Loaders) =
+                       ListNotification<Image>("inserting-images-notification", "images", loaders.loadTemplate("/templates/notify/inserting-images-notification.html"), dismissable = true)
+
+       @Provides
+       @Singleton
+       @Named("imageFailed")
+       fun getImageInsertingFailedNotification(loaders: Loaders) =
+                       ListNotification<Image>("image-insert-failed-notification", "images", loaders.loadTemplate("/templates/notify/image-insert-failed-notification.html"), dismissable = true)
+
+       @Provides
+       @Singleton
+       @Named("imageInserted")
+       fun getImageInsertedNotification(loaders: Loaders) =
+                       ListNotification<Image>("inserted-images-notification", "images", loaders.loadTemplate("/templates/notify/inserted-images-notification.html"), dismissable = true)
+
+       @Provides
+       @Singleton
+       @Named("firstStart")
+       fun getFirstStartNotification(loaders: Loaders) =
+                       TemplateNotification("first-start-notification", loaders.loadTemplate("/templates/notify/firstStartNotification.html"))
+
+       @Provides
+       @Singleton
+       @Named("configNotRead")
+       fun getConfigNotReadNotification(loaders: Loaders) =
+                       TemplateNotification("config-not-read-notification", loaders.loadTemplate("/templates/notify/configNotReadNotification.html"))
+
+       @Provides
+       @Singleton
+       @Named("startup")
+       fun getStartupNotification(loaders: Loaders) =
+                       TemplateNotification("startup-notification", loaders.loadTemplate("/templates/notify/startupNotification.html"))
+
+       @Provides
+       @Singleton
+       @Named("webOfTrust")
+       fun getWebOfTrustNotification(loaders: Loaders) =
+                       TemplateNotification("wot-missing-notification", loaders.loadTemplate("/templates/notify/wotMissingNotification.html"))
+
+       @Provides
+       @Singleton
+       @Named("webOfTrustReacher")
+       fun getWebOfTrustReacher(webOfTrustConnector: WebOfTrustConnector): Runnable =
+                       Runnable { webOfTrustConnector.ping() }
+
+       @Provides
+       @Singleton
+       @Named("webOfTrustReschedule")
+       fun getWebOfTrustReschedule(@Named("notification") ticker: ScheduledExecutorService) =
+                       Consumer<Runnable> { ticker.schedule(it, 15, SECONDS) }
+
+       @Provides
+       @Singleton
+       @Named("soneMentioned")
+       fun getSoneMentionedNotification(loaders: Loaders) =
+                       ListNotification<Post>("mention-notification", "posts", loaders.loadTemplate("/templates/notify/mentionNotification.html"), dismissable = false)
+
+       @Provides
+       @Singleton
+       fun getSoneNotificationSupplier(loaders: Loaders): SoneInsertNotificationSupplier =
+                       mutableMapOf<Sone, TemplateNotification>()
+                                       .let { cache ->
+                                               { sone ->
+                                                       cache.computeIfAbsent(sone) {
+                                                               loaders.loadTemplate("/templates/notify/soneInsertNotification.html")
+                                                                               .let(::TemplateNotification)
+                                                                               .also { it["insertSone"] = sone }
+                                                       }
+                                               }
+                                       }
+
+       private inline fun <reified T> bind(): AnnotatedBindingBuilder<T> = bind(T::class.java)
+       private fun ScopedBindingBuilder.asSingleton() = `in`(Singleton::class.java)
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/notification/RemotePostReplyHandler.kt b/src/main/kotlin/net/pterodactylus/sone/web/notification/RemotePostReplyHandler.kt
new file mode 100644 (file)
index 0000000..7dec7c9
--- /dev/null
@@ -0,0 +1,55 @@
+/**
+ * Sone - RemotePostReplyHandler.kt - Copyright © 2020 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.notification
+
+import com.google.common.eventbus.*
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.sone.data.*
+import net.pterodactylus.sone.notify.*
+import net.pterodactylus.sone.utils.*
+import net.pterodactylus.util.notify.*
+import javax.inject.*
+
+/**
+ * Handler for remote replies.
+ */
+class RemotePostReplyHandler @Inject constructor(private val notificationManager: NotificationManager, @Named("newRemotePostReply") private val notification: ListNotification<PostReply>) {
+
+       @Subscribe
+       fun newPostReplyFound(event: NewPostReplyFoundEvent) {
+               event.postReply.let { postReply ->
+                       postReply.sone.isLocal.onFalse {
+                               if (!notificationManager.hasFirstStartNotification()) {
+                                       notification.add(event.postReply)
+                                       notificationManager.addNotification(notification)
+                               }
+                       }
+               }
+       }
+
+       @Subscribe
+       fun postReplyRemoved(event: PostReplyRemovedEvent) {
+               notification.remove(event.postReply)
+       }
+
+       @Subscribe
+       fun postReplyMarkedAsKnown(event: MarkPostReplyKnownEvent) {
+               notification.remove(event.postReply)
+       }
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/notification/SoneInsertHandler.kt b/src/main/kotlin/net/pterodactylus/sone/web/notification/SoneInsertHandler.kt
new file mode 100644 (file)
index 0000000..b12acb4
--- /dev/null
@@ -0,0 +1,58 @@
+/**
+ * Sone - SoneInsertHandler.kt - Copyright © 2020 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.notification
+
+import com.google.common.eventbus.*
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.sone.data.*
+import net.pterodactylus.util.notify.*
+import javax.inject.*
+
+/**
+ * Handler for all notifications concerning Sone-insert events.
+ */
+class SoneInsertHandler @Inject constructor(private val notificationManager: NotificationManager, private val soneNotifications: SoneInsertNotificationSupplier) {
+
+       @Subscribe
+       fun soneInserting(event: SoneInsertingEvent) {
+               showNotification(event.sone, "inserting")
+       }
+
+       @Subscribe
+       fun soneInserted(event: SoneInsertedEvent) {
+               showNotification(event.sone, "inserted", "insertDuration" to event.insertDuration / 1000)
+       }
+
+       @Subscribe
+       fun soneInsertAborted(event: SoneInsertAbortedEvent) {
+               showNotification(event.sone, "insert-aborted")
+       }
+
+       private fun showNotification(sone: Sone, status: String, vararg templateVariables: Pair<String, Any>) {
+               if (sone.options.isSoneInsertNotificationEnabled) {
+                       soneNotifications(sone).let { notification ->
+                               notification["soneStatus"] = status
+                               templateVariables.forEach { notification[it.first] = it.second }
+                               notificationManager.addNotification(notification)
+                       }
+               }
+       }
+
+}
+
+typealias SoneInsertNotificationSupplier = (@JvmSuppressWildcards Sone) -> @JvmSuppressWildcards TemplateNotification
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/notification/SoneLockedHandler.kt b/src/main/kotlin/net/pterodactylus/sone/web/notification/SoneLockedHandler.kt
new file mode 100644 (file)
index 0000000..cb799a8
--- /dev/null
@@ -0,0 +1,65 @@
+/**
+ * Sone - SoneLockedHandler.kt - Copyright © 2019–2020 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.notification
+
+import com.google.common.eventbus.*
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.sone.data.*
+import net.pterodactylus.sone.notify.*
+import net.pterodactylus.util.notify.*
+import java.util.concurrent.*
+import java.util.concurrent.atomic.*
+import javax.inject.*
+
+/**
+ * Handler for [SoneLockedEvent]s and [SoneUnlockedEvent]s that can schedule notifications after
+ * a certain timeout.
+ */
+class SoneLockedHandler @Inject constructor(
+               private val notificationManager: NotificationManager,
+               @Named("soneLocked") private val notification: ListNotification<Sone>,
+               @Named("notification") private val executor: ScheduledExecutorService) {
+
+       private val future: AtomicReference<ScheduledFuture<*>> = AtomicReference()
+
+       @Subscribe
+       fun soneLocked(soneLockedEvent: SoneLockedEvent) {
+               synchronized(future) {
+                       notification.add(soneLockedEvent.sone)
+                       future.get()?.also(this::cancelPreviousFuture)
+                       future.set(executor.schedule(::showNotification, 5, TimeUnit.MINUTES))
+               }
+       }
+
+       @Subscribe
+       fun soneUnlocked(soneUnlockedEvent: SoneUnlockedEvent) {
+               synchronized(future) {
+                       notification.remove(soneUnlockedEvent.sone)
+                       future.get()?.also(::cancelPreviousFuture)
+               }
+       }
+
+       private fun cancelPreviousFuture(future: ScheduledFuture<*>) {
+               future.cancel(true)
+       }
+
+       private fun showNotification() {
+               notificationManager.addNotification(notification)
+       }
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/notification/SoneLockedOnStartupHandler.kt b/src/main/kotlin/net/pterodactylus/sone/web/notification/SoneLockedOnStartupHandler.kt
new file mode 100644 (file)
index 0000000..8adea76
--- /dev/null
@@ -0,0 +1,40 @@
+/**
+ * Sone - SoneLockedOnStartupHandler.kt - Copyright © 2019–2020 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.notification
+
+import com.google.common.eventbus.*
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.sone.data.*
+import net.pterodactylus.sone.notify.*
+import net.pterodactylus.util.notify.*
+import javax.inject.*
+
+/**
+ * Handler for [SoneLockedOnStartup][net.pterodactylus.sone.core.event.SoneLockedOnStartup] events
+ * that adds the appropriate notification to the [NotificationManager].
+ */
+class SoneLockedOnStartupHandler @Inject constructor(private val notificationManager: NotificationManager, @Named("soneLockedOnStartup") private val notification: ListNotification<Sone>) {
+
+       @Subscribe
+       @Suppress("UnstableApiUsage")
+       fun soneLockedOnStartup(soneLockedOnStartup: SoneLockedOnStartup) {
+               notification.add(soneLockedOnStartup.sone)
+               notificationManager.addNotification(notification)
+       }
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/notification/SoneMentionedHandler.kt b/src/main/kotlin/net/pterodactylus/sone/web/notification/SoneMentionedHandler.kt
new file mode 100644 (file)
index 0000000..a03e490
--- /dev/null
@@ -0,0 +1,47 @@
+/**
+ * Sone - SoneMentionedHandler.kt - Copyright © 2020 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.notification
+
+import com.google.common.eventbus.*
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.sone.data.*
+import net.pterodactylus.sone.notify.*
+import net.pterodactylus.util.notify.*
+import javax.inject.*
+
+/**
+ * Handler for the [MentionOfLocalSoneFoundEvent] and
+ * [MentionOfLocalSoneRemovedEvent] events that add the corresponding
+ * notification to the notification manager.
+ */
+class SoneMentionedHandler @Inject constructor(private val notificationManager: NotificationManager, @Named("soneMentioned") private val notification: ListNotification<Post>) {
+
+       @Subscribe
+       fun mentionOfLocalSoneFound(event: MentionOfLocalSoneFoundEvent) {
+               if (!notificationManager.hasFirstStartNotification()) {
+                       notification.add(event.post)
+                       notificationManager.addNotification(notification)
+               }
+       }
+
+       @Subscribe
+       fun mentionOfLocalSoneRemoved(event: MentionOfLocalSoneRemovedEvent) {
+               notification.remove(event.post)
+       }
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/notification/StartupHandler.kt b/src/main/kotlin/net/pterodactylus/sone/web/notification/StartupHandler.kt
new file mode 100644 (file)
index 0000000..3776d43
--- /dev/null
@@ -0,0 +1,41 @@
+/**
+ * Sone - StartupHandler.kt - Copyright © 2019–2020 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.notification
+
+import com.google.common.eventbus.*
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.util.notify.*
+import java.util.concurrent.*
+import java.util.concurrent.TimeUnit.*
+import javax.inject.*
+
+/**
+ * Handler for the [Startup] event notification.
+ */
+class StartupHandler @Inject constructor(
+               private val notificationManager: NotificationManager,
+               @Named("startup") private val notification: TemplateNotification,
+               @Named("notification") private val ticker: ScheduledExecutorService) {
+
+       @Subscribe
+       fun startup(@Suppress("UNUSED_PARAMETER") startup: Startup) {
+               notificationManager.addNotification(notification)
+               ticker.schedule({ notificationManager.removeNotification(notification) }, 2, MINUTES)
+       }
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/notification/WebOfTrustHandler.kt b/src/main/kotlin/net/pterodactylus/sone/web/notification/WebOfTrustHandler.kt
new file mode 100644 (file)
index 0000000..900a885
--- /dev/null
@@ -0,0 +1,41 @@
+/**
+ * Sone - WebOfTrustHandler.kt - Copyright © 2019–2020 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.notification
+
+import com.google.common.eventbus.*
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.util.notify.*
+import javax.inject.*
+
+/**
+ * Handler for web of trust-related notifications and the [WebOfTrustAppeared]
+ * and [WebOfTrustDisappeared] events.
+ */
+class WebOfTrustHandler @Inject constructor(private val notificationManager: NotificationManager, @Named("webOfTrust") private val notification: TemplateNotification) {
+
+       @Subscribe
+       fun webOfTrustAppeared(@Suppress("UNUSED_PARAMETER") webOfTrustAppeared: WebOfTrustAppeared) {
+               notificationManager.removeNotification(notification)
+       }
+
+       @Subscribe
+       fun webOfTrustDisappeared(@Suppress("UNUSED_PARAMETER") webOfTrustDisappeared: WebOfTrustDisappeared) {
+               notificationManager.addNotification(notification)
+       }
+
+}
index 2af0eca..68875d6 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - FreenetPage.kt - Copyright © 2011–2019 David Roden
+ * Sone - FreenetPage.kt - Copyright © 2011–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
index 54c8f7f..5729b25 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - FreenetRequest.kt - Copyright © 2011–2019 David Roden
+ * Sone - FreenetRequest.kt - Copyright © 2011–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
 package net.pterodactylus.sone.web.page
 
 import freenet.clients.http.*
-import freenet.clients.http.SessionManager.*
-import freenet.l10n.*
 import freenet.support.api.*
 import net.pterodactylus.util.web.*
 import java.net.*
-import java.util.UUID.*
 
 open class FreenetRequest(uri: URI, method: Method,
                val httpRequest: HTTPRequest,
-               val toadletContext: ToadletContext,
-               val l10n: BaseL10n,
-               val sessionManager: SessionManager
-) : Request(uri, method) {
-
-       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)
index 2dcef51..3912f98 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - FreenetTemplatePage.kt - Copyright © 2010–2019 David Roden
+ * Sone - FreenetTemplatePage.kt - Copyright © 2010–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -98,6 +98,9 @@ open class FreenetTemplatePage(
                /* do nothing. */
        }
 
+       fun redirectTo(target: String?): Nothing =
+                       throw RedirectException(target)
+
        class RedirectException(val target: String?) : Exception() {
                override fun toString(): String = format("RedirectException{target='%s'}", target)
        }
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/page/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 3c84c09..61d8718 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - PageToadletFactory.kt - Copyright © 2010–2019 David Roden
+ * Sone - PageToadletFactory.kt - Copyright © 2010–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
 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 d3eed37..fcfc63d 100644 (file)
@@ -1,16 +1,16 @@
 package net.pterodactylus.sone.web.page
 
 import freenet.clients.http.*
-import freenet.l10n.*
 import freenet.support.api.*
 import net.pterodactylus.sone.core.*
 import net.pterodactylus.sone.web.*
 import net.pterodactylus.util.web.*
 import java.net.*
 
-class SoneRequest(uri: URI, method: Method, httpRequest: HTTPRequest, toadletContext: ToadletContext, l10n: BaseL10n, sessionManager: SessionManager,
-               val core: Core,
-               val webInterface: WebInterface
-) : FreenetRequest(uri, method, httpRequest, toadletContext, l10n, sessionManager)
+class SoneRequest(uri: URI, method: Method, httpRequest: HTTPRequest, toadletContext: ToadletContext,
+                                 val core: Core,
+                                 val webInterface: WebInterface
+) : FreenetRequest(uri, method, httpRequest, toadletContext)
 
-fun FreenetRequest.toSoneRequest(core: Core, webInterface: WebInterface) = SoneRequest(uri, method, httpRequest, toadletContext, l10n, sessionManager, core, webInterface)
+fun FreenetRequest.toSoneRequest(core: Core, webInterface: WebInterface) =
+               SoneRequest(uri, method, httpRequest, toadletContext, core, webInterface)
index 1dc12e4..c03a299 100644 (file)
@@ -21,7 +21,7 @@ class BookmarkPage @Inject constructor(webInterface: WebInterface, loaders: Load
                        soneRequest.core.getPost(postId)?.let {
                                soneRequest.core.bookmarkPost(it)
                        }
-                       throw RedirectException(returnPage)
+                       redirectTo(returnPage)
                }
        }
 
index a00b4bf..b6ce673 100644 (file)
@@ -35,10 +35,10 @@ class CreateAlbumPage @Inject constructor(webInterface: WebInterface, loaders: L
                                        setDescription(TextFilter.filter(soneRequest.httpRequest.getHeader("Host"), description))
                                }.update()
                        } catch (e: AlbumTitleMustNotBeEmpty) {
-                               throw RedirectException("emptyAlbumTitle.html")
+                               redirectTo("emptyAlbumTitle.html")
                        }
                        soneRequest.core.touchConfiguration()
-                       throw RedirectException("imageBrowser.html?album=${album.id}")
+                       redirectTo("imageBrowser.html?album=${album.id}")
                }
        }
 
index b47c2b1..515de60 100644 (file)
@@ -28,8 +28,8 @@ class CreatePostPage @Inject constructor(webInterface: WebInterface, loaders: Lo
                        }
                        val sender = soneRequest.core.getLocalSone(soneRequest.httpRequest.getPartAsStringFailsafe("sender", 43)) ?: currentSone
                        val recipient = soneRequest.core.getSone(soneRequest.httpRequest.getPartAsStringFailsafe("recipient", 43))
-                       soneRequest.core.createPost(sender, recipient.asOptional(), TextFilter.filter(soneRequest.httpRequest.getHeader("Host"), text))
-                       throw RedirectException(returnPage)
+                       soneRequest.core.createPost(sender, recipient, TextFilter.filter(soneRequest.httpRequest.getHeader("Host"), text))
+                       redirectTo(returnPage)
                }
        }
 
index 562e647..7714b77 100644 (file)
@@ -26,10 +26,10 @@ class CreateReplyPage @Inject constructor(webInterface: WebInterface, loaders: L
                                templateContext["errorTextEmpty"] = true
                                return
                        }
-                       val post = soneRequest.core.getPost(postId) ?: throw RedirectException("noPermission.html")
+                       val post = soneRequest.core.getPost(postId) ?: redirectTo("noPermission.html")
                        val sender = soneRequest.core.getLocalSone(soneRequest.httpRequest.getPartAsStringFailsafe("sender", 43)) ?: currentSone
                        soneRequest.core.createReply(sender, post, TextFilter.filter(soneRequest.httpRequest.getHeader("Host"), text))
-                       throw RedirectException(returnPage)
+                       redirectTo(returnPage)
                }
        }
 
index b2056c2..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)
@@ -31,7 +31,7 @@ class CreateSonePage @Inject constructor(webInterface: WebInterface, loaders: Lo
                                        logger.log(Level.SEVERE, "Could not create Sone for OwnIdentity: $ownIdentity")
                                }
                                setCurrentSone(soneRequest.toadletContext, sone)
-                               throw RedirectException("index.html")
+                               redirectTo("index.html")
                        }
                        templateContext["errorNoIdentity"] = true
                }
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/pages/DebugPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/pages/DebugPage.kt
new file mode 100644 (file)
index 0000000..824f6ef
--- /dev/null
@@ -0,0 +1,18 @@
+package net.pterodactylus.sone.web.pages
+
+import net.pterodactylus.sone.main.*
+import net.pterodactylus.sone.web.*
+import net.pterodactylus.sone.web.page.*
+import net.pterodactylus.util.template.*
+import javax.inject.*
+
+@ToadletPath("debug")
+class DebugPage @Inject constructor(webInterface: WebInterface, loaders: Loaders, templateRenderer: TemplateRenderer) :
+               SoneTemplatePage(webInterface, loaders, templateRenderer) {
+
+       override fun handleRequest(soneRequest: SoneRequest, templateContext: TemplateContext) {
+               soneRequest.core.setDebug()
+               redirectTo("./")
+       }
+
+}
index d55c7cb..bd86c95 100644 (file)
@@ -18,18 +18,18 @@ class DeleteAlbumPage @Inject constructor(webInterface: WebInterface, loaders: L
 
        override fun handleRequest(soneRequest: SoneRequest, currentSone: Sone, templateContext: TemplateContext) {
                if (soneRequest.isPOST) {
-                       val album = soneRequest.core.getAlbum(soneRequest.httpRequest.getPartAsStringFailsafe("album", 36)) ?: throw RedirectException("invalid.html")
+                       val album = soneRequest.core.getAlbum(soneRequest.httpRequest.getPartAsStringFailsafe("album", 36)) ?: redirectTo("invalid.html")
                        if (!album.sone.isLocal) {
-                               throw RedirectException("noPermission.html")
+                               redirectTo("noPermission.html")
                        }
                        if (soneRequest.httpRequest.getPartAsStringFailsafe("abortDelete", 4) == "true") {
-                               throw RedirectException("imageBrowser.html?album=${album.id}")
+                               redirectTo("imageBrowser.html?album=${album.id}")
                        }
                        soneRequest.core.deleteAlbum(album)
-                       throw RedirectException(if (album.parent.isRoot) "imageBrowser.html?sone=${album.sone.id}" else "imageBrowser.html?album=${album.parent.id}")
+                       redirectTo(if (album.parent.isRoot) "imageBrowser.html?sone=${album.sone.id}" else "imageBrowser.html?album=${album.parent.id}")
                }
                val album = soneRequest.core.getAlbum(soneRequest.httpRequest.getParam("album"))
-               templateContext["album"] = album ?: throw RedirectException("invalid.html")
+               templateContext["album"] = album ?: redirectTo("invalid.html")
        }
 
 }
index a9d601c..6459bfd 100644 (file)
@@ -18,19 +18,19 @@ class DeleteImagePage @Inject constructor(webInterface: WebInterface, loaders: L
 
        override fun handleRequest(soneRequest: SoneRequest, currentSone: Sone, templateContext: TemplateContext) {
                if (soneRequest.isPOST) {
-                       val image = soneRequest.core.getImage(soneRequest.httpRequest.getPartAsStringFailsafe("image", 36)) ?: throw RedirectException("invalid.html")
+                       val image = soneRequest.core.getImage(soneRequest.httpRequest.getPartAsStringFailsafe("image", 36)) ?: redirectTo("invalid.html")
                        if (!image.sone.isLocal) {
-                               throw RedirectException("noPermission.html")
+                               redirectTo("noPermission.html")
                        }
                        if (soneRequest.httpRequest.isPartSet("abortDelete")) {
-                               throw RedirectException("imageBrowser.html?image=${image.id}")
+                               redirectTo("imageBrowser.html?image=${image.id}")
                        }
                        soneRequest.core.deleteImage(image)
-                       throw RedirectException("imageBrowser.html?album=${image.album.id}")
+                       redirectTo("imageBrowser.html?album=${image.album.id}")
                }
-               val image = soneRequest.core.getImage(soneRequest.httpRequest.getParam("image")) ?: throw RedirectException("invalid.html")
+               val image = soneRequest.core.getImage(soneRequest.httpRequest.getParam("image")) ?: redirectTo("invalid.html")
                if (!image.sone.isLocal) {
-                       throw RedirectException("noPermission.html")
+                       redirectTo("noPermission.html")
                }
                templateContext["image"] = image
        }
index b3749b3..2396370 100644 (file)
@@ -18,22 +18,22 @@ class DeletePostPage @Inject constructor(webInterface: WebInterface, loaders: Lo
 
        override fun handleRequest(soneRequest: SoneRequest, currentSone: Sone, templateContext: TemplateContext) {
                if (soneRequest.isPOST) {
-                       val post = soneRequest.core.getPost(soneRequest.httpRequest.getPartAsStringFailsafe("post", 36)) ?: throw RedirectException("noPermission.html")
+                       val post = soneRequest.core.getPost(soneRequest.httpRequest.getPartAsStringFailsafe("post", 36)) ?: redirectTo("noPermission.html")
                        val returnPage = soneRequest.httpRequest.getPartAsStringFailsafe("returnPage", 256)
                        if (!post.sone.isLocal) {
-                               throw RedirectException("noPermission.html")
+                               redirectTo("noPermission.html")
                        }
                        if (soneRequest.httpRequest.isPartSet("confirmDelete")) {
                                soneRequest.core.deletePost(post)
-                               throw RedirectException(returnPage)
+                               redirectTo(returnPage)
                        } else if (soneRequest.httpRequest.isPartSet("abortDelete")) {
-                               throw RedirectException(returnPage)
+                               redirectTo(returnPage)
                        }
                        templateContext["post"] = post
                        templateContext["returnPage"] = returnPage
                        return
                }
-               templateContext["post"] = soneRequest.core.getPost(soneRequest.httpRequest.getParam("post")) ?: throw RedirectException("noPermission.html")
+               templateContext["post"] = soneRequest.core.getPost(soneRequest.httpRequest.getParam("post")) ?: redirectTo("noPermission.html")
                templateContext["returnPage"] = soneRequest.httpRequest.getParam("returnPage")
        }
 
index 0fabad9..6303b11 100644 (file)
@@ -18,13 +18,13 @@ class DeleteProfileFieldPage @Inject constructor(webInterface: WebInterface, loa
 
        override fun handleRequest(soneRequest: SoneRequest, currentSone: Sone, templateContext: TemplateContext) {
                if (soneRequest.isPOST) {
-                       val field = currentSone.profile.getFieldById(soneRequest.httpRequest.getPartAsStringFailsafe("field", 36)) ?: throw RedirectException("invalid.html")
+                       val field = currentSone.profile.getFieldById(soneRequest.httpRequest.getPartAsStringFailsafe("field", 36)) ?: redirectTo("invalid.html")
                        if (soneRequest.httpRequest.getPartAsStringFailsafe("confirm", 4) == "true") {
                                currentSone.profile = currentSone.profile.apply { removeField(field) }
                        }
-                       throw RedirectException("editProfile.html#profile-fields")
+                       redirectTo("editProfile.html#profile-fields")
                }
-               val field = currentSone.profile.getFieldById(soneRequest.httpRequest.getParam("field")) ?: throw RedirectException("invalid.html")
+               val field = currentSone.profile.getFieldById(soneRequest.httpRequest.getParam("field")) ?: redirectTo("invalid.html")
                templateContext["field"] = field
        }
 
index e3f30ce..e6fbce8 100644 (file)
@@ -19,17 +19,17 @@ class DeleteReplyPage @Inject constructor(webInterface: WebInterface, loaders: L
        override fun handleRequest(soneRequest: SoneRequest, currentSone: Sone, templateContext: TemplateContext) {
                if (soneRequest.isPOST) {
                        val replyId = soneRequest.httpRequest.getPartAsStringFailsafe("reply", 36)
-                       val reply = soneRequest.core.getPostReply(replyId) ?: throw RedirectException("noPermission.html")
+                       val reply = soneRequest.core.getPostReply(replyId) ?: redirectTo("noPermission.html")
                        if (!reply.sone.isLocal) {
-                               throw RedirectException("noPermission.html")
+                               redirectTo("noPermission.html")
                        }
                        val returnPage = soneRequest.httpRequest.getPartAsStringFailsafe("returnPage", 256)
                        if (soneRequest.httpRequest.isPartSet("confirmDelete")) {
                                soneRequest.core.deleteReply(reply)
-                               throw RedirectException(returnPage)
+                               redirectTo(returnPage)
                        }
                        if (soneRequest.httpRequest.isPartSet("abortDelete")) {
-                               throw RedirectException(returnPage)
+                               redirectTo(returnPage)
                        }
                        templateContext["reply"] = replyId
                        templateContext["returnPage"] = returnPage
index f65e378..3f99730 100644 (file)
@@ -24,7 +24,7 @@ class DeleteSonePage @Inject constructor(webInterface: WebInterface, loaders: Lo
                        if (soneRequest.httpRequest.isPartSet("deleteSone")) {
                                soneRequest.core.deleteSone(currentSone)
                        }
-                       throw RedirectException("index.html")
+                       redirectTo("index.html")
                }
        }
 
index 862db67..cd348e7 100644 (file)
@@ -17,7 +17,7 @@ class DismissNotificationPage @Inject constructor(webInterface: WebInterface, lo
                val returnPage = soneRequest.httpRequest.getPartAsStringFailsafe("returnPage", 256)
                val notificationId = soneRequest.httpRequest.getPartAsStringFailsafe("notification", 36)
                soneRequest.webInterface.getNotification(notificationId).orNull()?.takeIf { it.isDismissable }?.dismiss()
-               throw RedirectException(returnPage)
+               redirectTo(returnPage)
        }
 
 }
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/pages/DistrustPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/pages/DistrustPage.kt
deleted file mode 100644 (file)
index f2115e5..0000000
+++ /dev/null
@@ -1,29 +0,0 @@
-package net.pterodactylus.sone.web.pages
-
-import net.pterodactylus.sone.data.*
-import net.pterodactylus.sone.main.*
-import net.pterodactylus.sone.utils.*
-import net.pterodactylus.sone.web.*
-import net.pterodactylus.sone.web.page.*
-import net.pterodactylus.util.template.*
-import javax.inject.*
-
-/**
- * Page that lets the user distrust another Sone. This will assign a
- * configurable (negative) amount of trust to an identity.
- *
- * @see net.pterodactylus.sone.core.Core#distrustSone(Sone, Sone)
- */
-@ToadletPath("distrust.html")
-class DistrustPage @Inject constructor(webInterface: WebInterface, loaders: Loaders, templateRenderer: TemplateRenderer) :
-               LoggedInPage("Page.Distrust.Title", webInterface, loaders, templateRenderer) {
-
-       override fun handleRequest(soneRequest: SoneRequest, currentSone: Sone, templateContext: TemplateContext) {
-               if (soneRequest.isPOST) {
-                       soneRequest.core.getSone(soneRequest.httpRequest.getPartAsStringFailsafe("sone", 44))
-                                       ?.run { soneRequest.core.distrustSone(currentSone, this) }
-                       throw RedirectException(soneRequest.httpRequest.getPartAsStringFailsafe("returnPage", 256))
-               }
-       }
-
-}
index aeaf14e..4569e9a 100644 (file)
@@ -18,16 +18,16 @@ class EditAlbumPage @Inject constructor(webInterface: WebInterface, loaders: Loa
 
        override fun handleRequest(soneRequest: SoneRequest, currentSone: Sone, templateContext: TemplateContext) {
                if (soneRequest.isPOST) {
-                       val album = soneRequest.core.getAlbum(soneRequest.httpRequest.getPartAsStringFailsafe("album", 36)) ?: throw RedirectException("invalid.html")
-                       album.takeUnless { it.sone.isLocal }?.run { throw RedirectException("noPermission.html") }
+                       val album = soneRequest.core.getAlbum(soneRequest.httpRequest.getPartAsStringFailsafe("album", 36)) ?: redirectTo("invalid.html")
+                       album.takeUnless { it.sone.isLocal }?.run { redirectTo("noPermission.html") }
                        if (soneRequest.httpRequest.getPartAsStringFailsafe("moveLeft", 4) == "true") {
                                album.parent?.moveAlbumUp(album)
                                soneRequest.core.touchConfiguration()
-                               throw RedirectException("imageBrowser.html?album=${album.parent?.id}")
+                               redirectTo("imageBrowser.html?album=${album.parent?.id}")
                        } else if (soneRequest.httpRequest.getPartAsStringFailsafe("moveRight", 4) == "true") {
                                album.parent?.moveAlbumDown(album)
                                soneRequest.core.touchConfiguration()
-                               throw RedirectException("imageBrowser.html?album=${album.parent?.id}")
+                               redirectTo("imageBrowser.html?album=${album.parent?.id}")
                        } else {
                                try {
                                        album.modify()
@@ -35,10 +35,10 @@ class EditAlbumPage @Inject constructor(webInterface: WebInterface, loaders: Loa
                                                        .setDescription(soneRequest.httpRequest.getPartAsStringFailsafe("description", 1000))
                                                        .update()
                                } catch (e: AlbumTitleMustNotBeEmpty) {
-                                       throw RedirectException("emptyAlbumTitle.html")
+                                       redirectTo("emptyAlbumTitle.html")
                                }
                                soneRequest.core.touchConfiguration()
-                               throw RedirectException("imageBrowser.html?album=${album.id}")
+                               redirectTo("imageBrowser.html?album=${album.id}")
                        }
                }
        }
index 76f6e23..5443d28 100644 (file)
@@ -19,9 +19,9 @@ class EditImagePage @Inject constructor(webInterface: WebInterface, loaders: Loa
 
        override fun handleRequest(soneRequest: SoneRequest, currentSone: Sone, templateContext: TemplateContext) {
                if (soneRequest.isPOST) {
-                       val image = soneRequest.core.getImage(soneRequest.httpRequest.getPartAsStringFailsafe("image", 36)) ?: throw RedirectException("invalid.html")
+                       val image = soneRequest.core.getImage(soneRequest.httpRequest.getPartAsStringFailsafe("image", 36)) ?: redirectTo("invalid.html")
                        if (!image.sone.isLocal) {
-                               throw RedirectException("noPermission.html")
+                               redirectTo("noPermission.html")
                        }
                        soneRequest.httpRequest.getPartAsStringFailsafe("returnPage", 256).let { returnPage ->
                                if (soneRequest.httpRequest.getPartAsStringFailsafe("moveLeft", 4) == "true") {
@@ -38,10 +38,10 @@ class EditImagePage @Inject constructor(webInterface: WebInterface, loaders: Loa
                                                                .update()
                                                soneRequest.core.touchConfiguration()
                                        } catch (e: ImageTitleMustNotBeEmpty) {
-                                               throw RedirectException("emptyImageTitle.html")
+                                               redirectTo("emptyImageTitle.html")
                                        }
                                }
-                               throw RedirectException(returnPage)
+                               redirectTo(returnPage)
                        }
                }
        }
index 386f66b..be97e80 100644 (file)
@@ -20,23 +20,23 @@ class EditProfileFieldPage @Inject constructor(webInterface: WebInterface, loade
                currentSone.profile.let { profile ->
                        if (soneRequest.isPOST) {
                                if (soneRequest.httpRequest.getPartAsStringFailsafe("cancel", 4) == "true") {
-                                       throw RedirectException("editProfile.html#profile-fields")
+                                       redirectTo("editProfile.html#profile-fields")
                                }
-                               val field = profile.getFieldById(soneRequest.httpRequest.getPartAsStringFailsafe("field", 36)) ?: throw RedirectException("invalid.html")
+                               val field = profile.getFieldById(soneRequest.httpRequest.getPartAsStringFailsafe("field", 36)) ?: redirectTo("invalid.html")
                                soneRequest.httpRequest.getPartAsStringFailsafe("name", 256).let { name ->
                                        try {
                                                if (name != field.name) {
                                                        field.name = name
                                                        currentSone.profile = profile
                                                }
-                                               throw RedirectException("editProfile.html#profile-fields")
+                                               redirectTo("editProfile.html#profile-fields")
                                        } catch (e: IllegalArgumentException) {
                                                templateContext["duplicateFieldName"] = true
                                                return
                                        }
                                }
                        }
-                       templateContext["field"] = profile.getFieldById(soneRequest.httpRequest.getParam("field")) ?: throw RedirectException("invalid.html")
+                       templateContext["field"] = profile.getFieldById(soneRequest.httpRequest.getParam("field")) ?: redirectTo("invalid.html")
                }
        }
 
index 54b7efd..93393ba 100644 (file)
@@ -43,31 +43,31 @@ class EditProfilePage @Inject constructor(webInterface: WebInterface, loaders: L
                                        }
                                        currentSone.profile = profile
                                        soneRequest.core.touchConfiguration()
-                                       throw RedirectException("editProfile.html")
+                                       redirectTo("editProfile.html")
                                } else if (soneRequest.httpRequest.getPartAsStringFailsafe("add-field", 4) == "true") {
                                        val fieldName = soneRequest.httpRequest.getPartAsStringFailsafe("field-name", 100)
                                        try {
                                                profile.addField(fieldName)
                                                currentSone.profile = profile
                                                soneRequest.core.touchConfiguration()
-                                               throw RedirectException("editProfile.html#profile-fields")
+                                               redirectTo("editProfile.html#profile-fields")
                                        } catch (e: DuplicateField) {
                                                templateContext["fieldName"] = fieldName
                                                templateContext["duplicateFieldName"] = true
                                        }
                                } else profile.fields.forEach { field ->
                                        if (soneRequest.httpRequest.getPartAsStringFailsafe("delete-field-${field.id}", 4) == "true") {
-                                               throw RedirectException("deleteProfileField.html?field=${field.id}")
+                                               redirectTo("deleteProfileField.html?field=${field.id}")
                                        } else if (soneRequest.httpRequest.getPartAsStringFailsafe("edit-field-${field.id}", 4) == "true") {
-                                               throw RedirectException("editProfileField.html?field=${field.id}")
+                                               redirectTo("editProfileField.html?field=${field.id}")
                                        } else if (soneRequest.httpRequest.getPartAsStringFailsafe("move-down-field-${field.id}", 4) == "true") {
                                                profile.moveFieldDown(field)
                                                currentSone.profile = profile
-                                               throw RedirectException("editProfile.html#profile-fields")
+                                               redirectTo("editProfile.html#profile-fields")
                                        } else if (soneRequest.httpRequest.getPartAsStringFailsafe("move-up-field-${field.id}", 4) == "true") {
                                                profile.moveFieldUp(field)
                                                currentSone.profile = profile
-                                               throw RedirectException("editProfile.html#profile-fields")
+                                               redirectTo("editProfile.html#profile-fields")
                                        }
                                }
                        }
index 244cb52..5fd06ce 100644 (file)
@@ -24,7 +24,7 @@ class FollowSonePage @Inject constructor(webInterface: WebInterface, loaders: Lo
                                                soneRequest.core.followSone(currentSone, sone.first)
                                                soneRequest.core.markSoneKnown(sone.second)
                                        }
-                       throw RedirectException(soneRequest.httpRequest.getPartAsStringFailsafe("returnPage", 256))
+                       redirectTo(soneRequest.httpRequest.getPartAsStringFailsafe("returnPage", 256))
                }
        }
 
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 cc5043f..c8369e9 100644 (file)
@@ -23,7 +23,7 @@ class LikePage @Inject constructor(webInterface: WebInterface, loaders: Loaders,
                                        "reply" -> currentSone.addLikedReplyId(soneRequest.parameters["reply", 36]!!)
                                }
                        }
-                       throw RedirectException(soneRequest.parameters["returnPage", 256]!!)
+                       redirectTo(soneRequest.parameters["returnPage", 256]!!)
                }
        }
 
index 2a62a7d..9f6f30e 100644 (file)
@@ -19,7 +19,7 @@ class LockSonePage @Inject constructor(webInterface: WebInterface, loaders: Load
                        soneRequest.parameters["sone", 44]!!
                                        .let { soneRequest.core.getLocalSone(it) }
                                        ?.let { soneRequest.core.lockSone(it) }
-                       throw RedirectException(returnPage)
+                       redirectTo(returnPage)
                }
        }
 
index 9e47049..6244325 100644 (file)
@@ -23,10 +23,10 @@ class LoginPage @Inject constructor(webInterface: WebInterface, loaders: Loaders
                        soneRequest.core.getLocalSone(soneId)?.let { sone ->
                                setCurrentSone(soneRequest.toadletContext, sone)
                                val target = soneRequest.httpRequest.getParam("target").emptyToNull ?: "index.html"
-                               throw RedirectException(target)
+                               redirectTo(target)
                        }
                }
-               templateContext["sones"] = soneRequest.core.localSones.sortedWith(Sone.NICE_NAME_COMPARATOR)
+               templateContext["sones"] = soneRequest.core.localSones.sortedWith(niceNameComparator)
                templateContext["identitiesWithoutSone"] = soneRequest.core.identityManager.allOwnIdentities.filterNot { "Sone" in it.contexts }.sortedBy { "${it.nickname}@${it.id}" }
        }
 
index 325d876..a7722d4 100644 (file)
@@ -17,7 +17,7 @@ class LogoutPage @Inject constructor(webInterface: WebInterface, loaders: Loader
 
        override fun handleRequest(soneRequest: SoneRequest, currentSone: Sone, templateContext: TemplateContext) {
                setCurrentSone(soneRequest.toadletContext, null)
-               throw RedirectException("index.html")
+               redirectTo("index.html")
        }
 
        override fun isEnabled(soneRequest: SoneRequest): Boolean =
index 652881d..f919318 100644 (file)
@@ -22,9 +22,9 @@ class MarkAsKnownPage @Inject constructor(webInterface: WebInterface, loaders: L
                        "sone" -> ids.mapNotNull(soneRequest.core::getSone).forEach(soneRequest.core::markSoneKnown)
                        "post" -> ids.mapNotNull(soneRequest.core::getPost).forEach(soneRequest.core::markPostKnown)
                        "reply" -> ids.mapNotNull(soneRequest.core::getPostReply).forEach(soneRequest.core::markReplyKnown)
-                       else -> throw RedirectException("invalid.html")
+                       else -> redirectTo("invalid.html")
                }
-               throw RedirectException(soneRequest.parameters["returnPage", 256]!!)
+               redirectTo(soneRequest.parameters["returnPage", 256]!!)
        }
 
 }
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/pages/MetricsPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/pages/MetricsPage.kt
new file mode 100644 (file)
index 0000000..837e2c8
--- /dev/null
@@ -0,0 +1,19 @@
+package net.pterodactylus.sone.web.pages
+
+import com.codahale.metrics.*
+import net.pterodactylus.sone.main.*
+import net.pterodactylus.sone.web.*
+import net.pterodactylus.sone.web.page.*
+import net.pterodactylus.util.template.*
+import javax.inject.*
+
+@MenuName("Metrics")
+@TemplatePath("/templates/metrics.html")
+@ToadletPath("metrics.html")
+class MetricsPage @Inject constructor(webInterface: WebInterface, loaders: Loaders, templateRenderer: TemplateRenderer, private val metricsRegistry: MetricRegistry) : SoneTemplatePage(webInterface, loaders, templateRenderer, "Page.Metrics.Title") {
+
+       override fun handleRequest(soneRequest: SoneRequest, templateContext: TemplateContext) {
+               templateContext["histograms"] = metricsRegistry.histograms
+       }
+
+}
index 8829030..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()
@@ -50,9 +52,6 @@ class OptionsPage @Inject constructor(webInterface: WebInterface, loaders: Loade
                        val imagesPerPage = soneRequest.parameters["images-per-page"]?.toIntOrNull()
                        val insertionDelay = soneRequest.parameters["insertion-delay"]?.toIntOrNull()
                        val fcpFullAccessRequired = soneRequest.parameters["fcp-full-access-required"]?.toIntOrNull()
-                       val negativeTrust = soneRequest.parameters["negative-trust"]?.toIntOrNull()
-                       val positiveTrust = soneRequest.parameters["positive-trust"]?.toIntOrNull()
-                       val trustComment = soneRequest.parameters["trust-comment"]?.emptyToNull
 
                        if (cantSetOption { soneRequest.core.preferences.newPostsPerPage = postsPerPage }) fieldsWithErrors += "posts-per-page"
                        if (cantSetOption { soneRequest.core.preferences.newCharactersPerPost = charactersPerPost }) fieldsWithErrors += "characters-per-post"
@@ -60,13 +59,10 @@ class OptionsPage @Inject constructor(webInterface: WebInterface, loaders: Loade
                        if (cantSetOption { soneRequest.core.preferences.newImagesPerPage = imagesPerPage }) fieldsWithErrors += "images-per-page"
                        if (cantSetOption { soneRequest.core.preferences.newInsertionDelay = insertionDelay }) fieldsWithErrors += "insertion-delay"
                        fcpFullAccessRequired?.also { if (cantSetOption { soneRequest.core.preferences.newFcpFullAccessRequired = FullAccessRequired.values()[fcpFullAccessRequired] }) fieldsWithErrors += "fcp-full-access-required" }
-                       if (cantSetOption { soneRequest.core.preferences.newNegativeTrust = negativeTrust }) fieldsWithErrors += "negative-trust"
-                       if (cantSetOption { soneRequest.core.preferences.newPositiveTrust = positiveTrust }) fieldsWithErrors += "positive-trust"
-                       if (cantSetOption { soneRequest.core.preferences.newTrustComment = trustComment }) fieldsWithErrors += "trust-comment"
 
                        if (fieldsWithErrors.isEmpty()) {
                                soneRequest.core.touchConfiguration()
-                               throw RedirectException("options.html")
+                               redirectTo("options.html")
                        }
                        templateContext["fieldErrors"] = fieldsWithErrors
                }
@@ -86,11 +82,9 @@ class OptionsPage @Inject constructor(webInterface: WebInterface, loaders: Loade
                        templateContext["images-per-page"] = preferences.imagesPerPage
                        templateContext["fcp-interface-active"] = preferences.fcpInterfaceActive
                        templateContext["require-full-access"] = preferences.requireFullAccess
-                       templateContext["negative-trust"] = preferences.negativeTrust
-                       templateContext["positive-trust"] = preferences.positiveTrust
                        templateContext["post-cut-off-length"] = preferences.postCutOffLength
                        templateContext["posts-per-page"] = preferences.postsPerPage
-                       templateContext["trust-comment"] = preferences.trustComment
+                       templateContext["strict-filtering"] = preferences.strictFiltering
                }
        }
 
index 3c14604..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) =
@@ -125,7 +125,7 @@ class SearchPage(webInterface: WebInterface, loaders: Loaders, templateRenderer:
                                                }
                                        }
 
-       private fun redirect(target: String): Nothing = throw RedirectException(target)
+       private fun redirect(target: String): Nothing = redirectTo(target)
 
        enum class Optionality {
                OPTIONAL,
index c905ed2..86131c3 100644 (file)
@@ -2,6 +2,7 @@ package net.pterodactylus.sone.web.pages
 
 import freenet.clients.http.*
 import net.pterodactylus.sone.data.*
+import net.pterodactylus.sone.freenet.*
 import net.pterodactylus.sone.main.*
 import net.pterodactylus.sone.utils.*
 import net.pterodactylus.sone.web.*
@@ -20,14 +21,15 @@ open class SoneTemplatePage(
                templateRenderer: TemplateRenderer,
                private val pageTitleKey: String? = null,
                private val requiresLogin: Boolean = false,
-               private val pageTitle: (FreenetRequest) -> String = { pageTitleKey?.let(webInterface.l10n::getString) ?: "" }
+               private val pageTitle: (FreenetRequest) -> String = { pageTitleKey?.let(webInterface.translation::translate) ?: "" }
 ) : FreenetTemplatePage(templateRenderer, loaders, "noPermission.html") {
 
        private val core = webInterface.core
        private val sessionProvider: SessionProvider = webInterface
+       protected val translation: Translation = webInterface.translation
 
        protected fun getCurrentSone(toadletContext: ToadletContext, createSession: Boolean = true) =
-                       sessionProvider.getCurrentSone(toadletContext, createSession)
+                       sessionProvider.getCurrentSone(toadletContext)
 
        protected fun setCurrentSone(toadletContext: ToadletContext, sone: Sone?) =
                        sessionProvider.setCurrentSone(toadletContext, sone)
@@ -89,7 +91,7 @@ open class SoneTemplatePage(
        private val String.urlEncode: String get() = URLEncoder.encode(this, "UTF-8")
 
        override fun isEnabled(toadletContext: ToadletContext) =
-                       isEnabled(SoneRequest(toadletContext.uri, Method.GET, HTTPRequestImpl(toadletContext.uri, "GET"), toadletContext, webInterface.l10n, webInterface.sessionManager, core, webInterface))
+                       isEnabled(SoneRequest(toadletContext.uri, Method.GET, HTTPRequestImpl(toadletContext.uri, "GET"), toadletContext, core, webInterface))
 
        open fun isEnabled(soneRequest: SoneRequest) = when {
                requiresLogin && getCurrentSone(soneRequest.toadletContext) == null -> false
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/pages/TrustPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/pages/TrustPage.kt
deleted file mode 100644 (file)
index 9a4c02f..0000000
+++ /dev/null
@@ -1,28 +0,0 @@
-package net.pterodactylus.sone.web.pages
-
-import net.pterodactylus.sone.data.*
-import net.pterodactylus.sone.main.*
-import net.pterodactylus.sone.utils.*
-import net.pterodactylus.sone.web.*
-import net.pterodactylus.sone.web.page.*
-import net.pterodactylus.util.template.*
-import javax.inject.*
-
-/**
- * Page that lets the user trust another Sone. This will assign a configurable
- * amount of trust to an identity.
- */
-@ToadletPath("trust.html")
-class TrustPage @Inject constructor(webInterface: WebInterface, loaders: Loaders, templateRenderer: TemplateRenderer) :
-               LoggedInPage("Page.Trust.Title", webInterface, loaders, templateRenderer) {
-
-       override fun handleRequest(soneRequest: SoneRequest, currentSone: Sone, templateContext: TemplateContext) {
-               if (soneRequest.isPOST) {
-                       soneRequest.core.getSone(soneRequest.parameters["sone"]!!)?.let { sone ->
-                               soneRequest.core.trustSone(currentSone, sone)
-                       }
-                       throw RedirectException(soneRequest.parameters["returnPage", 256])
-               }
-       }
-
-}
index 9c226d4..aa7bbf6 100644 (file)
@@ -21,13 +21,13 @@ class UnbookmarkPage @Inject constructor(webInterface: WebInterface, loaders: Lo
                                soneRequest.core.bookmarkedPosts
                                                .filterNot(Post::isLoaded)
                                                .forEach(soneRequest.core::unbookmarkPost)
-                               throw RedirectException("bookmarks.html")
+                               redirectTo("bookmarks.html")
                        }
                        soneRequest.isPOST -> {
                                soneRequest.parameters["post", 36]
                                                ?.let(soneRequest.core::getPost)
                                                ?.also(soneRequest.core::unbookmarkPost)
-                               throw RedirectException(soneRequest.parameters["returnPage", 256])
+                               redirectTo(soneRequest.parameters["returnPage", 256])
                        }
                }
        }
index 79fdef5..9dbe9f1 100644 (file)
@@ -19,7 +19,7 @@ class UnfollowSonePage @Inject constructor(webInterface: WebInterface, loaders:
                if (soneRequest.isPOST) {
                        soneRequest.parameters["sone"]!!.split(Regex("[ ,]+"))
                                        .forEach { soneRequest.core.unfollowSone(currentSone, it) }
-                       throw RedirectException(soneRequest.parameters["returnPage", 256])
+                       redirectTo(soneRequest.parameters["returnPage", 256])
                }
        }
 
index 82147a9..46bd3ab 100644 (file)
@@ -21,7 +21,7 @@ class UnlikePage @Inject constructor(webInterface: WebInterface, loaders: Loader
                                "post" -> currentSone.removeLikedPostId(soneRequest.parameters["post"]!!)
                                "reply" -> currentSone.removeLikedReplyId(soneRequest.parameters["reply"]!!)
                        }
-                       throw RedirectException(soneRequest.parameters["returnPage", 256])
+                       redirectTo(soneRequest.parameters["returnPage", 256])
                }
        }
 
index 2c5735b..450a466 100644 (file)
@@ -19,7 +19,7 @@ class UnlockSonePage @Inject constructor(webInterface: WebInterface, loaders: Lo
                        soneRequest.parameters["sone", 44]
                                        .let(soneRequest.core::getLocalSone)
                                        ?.also(soneRequest.core::unlockSone)
-                       throw RedirectException(soneRequest.parameters["returnPage", 256])
+                       redirectTo(soneRequest.parameters["returnPage", 256])
                }
        }
 
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/pages/UntrustPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/pages/UntrustPage.kt
deleted file mode 100644 (file)
index 7dd342e..0000000
+++ /dev/null
@@ -1,28 +0,0 @@
-package net.pterodactylus.sone.web.pages
-
-import net.pterodactylus.sone.data.*
-import net.pterodactylus.sone.main.*
-import net.pterodactylus.sone.utils.*
-import net.pterodactylus.sone.web.*
-import net.pterodactylus.sone.web.page.*
-import net.pterodactylus.util.template.*
-import javax.inject.*
-
-/**
- * Page that lets the user untrust another Sone. This will remove all trust
- * assignments for an identity.
- */
-@ToadletPath("untrust.html")
-class UntrustPage @Inject constructor(webInterface: WebInterface, loaders: Loaders, templateRenderer: TemplateRenderer) :
-               LoggedInPage("Page.Untrust.Title", webInterface, loaders, templateRenderer) {
-
-       override fun handleRequest(soneRequest: SoneRequest, currentSone: Sone, templateContext: TemplateContext) {
-               if (soneRequest.isPOST) {
-                       soneRequest.parameters["sone", 44]!!
-                                       .let(soneRequest.core::getSone)
-                                       ?.also { soneRequest.core.untrustSone(currentSone, it) }
-                       throw RedirectException(soneRequest.parameters["returnPage", 256])
-               }
-       }
-
-}
index de61835..0021c6b 100644 (file)
@@ -23,17 +23,17 @@ class UploadImagePage @Inject constructor(webInterface: WebInterface, loaders: L
 
        override fun handleRequest(soneRequest: SoneRequest, currentSone: Sone, templateContext: TemplateContext) {
                if (soneRequest.isPOST) {
-                       val parentAlbum = soneRequest.parameters["parent"]!!.let(soneRequest.core::getAlbum) ?: throw RedirectException("noPermission.html")
+                       val parentAlbum = soneRequest.parameters["parent"]!!.let(soneRequest.core::getAlbum) ?: redirectTo("noPermission.html")
                        if (parentAlbum.sone != currentSone) {
-                               throw RedirectException("noPermission.html")
+                               redirectTo("noPermission.html")
                        }
-                       val title = soneRequest.parameters["title", 200].emptyToNull ?: throw RedirectException("emptyImageTitle.html")
+                       val title = soneRequest.parameters["title", 200].emptyToNull ?: redirectTo("emptyImageTitle.html")
 
                        val uploadedFile = soneRequest.httpRequest.getUploadedFile("image")
                        val bytes = uploadedFile.data.use { it.toByteArray() }
                        val bufferedImage = bytes.toImage()
                        if (bufferedImage == null) {
-                               templateContext["messages"] = soneRequest.l10n.getString("Page.UploadImage.Error.InvalidImage")
+                               templateContext["messages"] = translation.translate("Page.UploadImage.Error.InvalidImage")
                                return
                        }
 
@@ -44,7 +44,7 @@ class UploadImagePage @Inject constructor(webInterface: WebInterface, loaders: L
                                setTitle(title)
                                setDescription(TextFilter.filter(soneRequest.headers["Host"], soneRequest.parameters["description", 4000]))
                        }.update()
-                       throw RedirectException("imageBrowser.html?album=${parentAlbum.id}")
+                       redirectTo("imageBrowser.html?album=${parentAlbum.id}")
                }
        }
 
index a498d24..efaee3f 100644 (file)
@@ -51,7 +51,7 @@ class ViewSonePage @Inject constructor(webInterface: WebInterface, loaders: Load
 
        override fun getPageTitle(soneRequest: SoneRequest): String =
                        soneRequest.parameters["sone"]!!.let(soneRequest.core::getSone)?.let { sone ->
-                               "${SoneAccessor.getNiceName(sone)} - ${soneRequest.l10n.getString("Page.ViewSone.Title")}"
-                       } ?: soneRequest.l10n.getString("Page.ViewSone.Page.TitleWithoutSone")
+                               "${SoneAccessor.getNiceName(sone)} - ${translation.translate("Page.ViewSone.Title")}"
+                       } ?: translation.translate("Page.ViewSone.Page.TitleWithoutSone")
 
 }
index 7e42167..cd42c45 100644 (file)
@@ -26,6 +26,8 @@ Navigation.Menu.Sone.Item.Rescue.Name=Sonerettung
 Navigation.Menu.Sone.Item.Rescue.Tooltip=Rettet Ihre Sone
 Navigation.Menu.Sone.Item.About.Name=Über Sone
 Navigation.Menu.Sone.Item.About.Tooltip=Informationen über Sone
+Navigation.Menu.Sone.Item.Metrics.Name=Metriken
+Navigation.Menu.Sone.Item.Metrics.Tooltip=Von Sone gesammelte Metriken
 
 Page.About.Title=Über  - Sone
 Page.About.Page.Title=Über
@@ -66,16 +68,14 @@ Page.Options.Option.ImagesPerPage.Description=Anzahl der Bilder pro Seite.
 Page.Options.Option.CharactersPerPost.Description=Die Anzahl der Zeichen, die eine Nachricht enthalten muss, damit sie gekürzt angezeigt wird (-1 für „nie kürzen“). Die Anzahl der tatsächlich angezeigten Zeichen wird in der nächsten Option konfiguriert.
 Page.Options.Option.PostCutOffLength.Description=Die Anzahl der Zeichen, die von einer gekürzten Nachricht sichtbar sind (siehe Option hierüber). Wird ignoriert, wenn die Option hierüber deaktiviert ist, bzw. auf -1 steht.
 Page.Options.Option.RequireFullAccess.Description=Zugriff auf Sone für alle Rechner, die keinen vollen Zugriff haben, unterbinden.
-Page.Options.Section.TrustOptions.Title=Vertrauenseinstellungen
-Page.Options.Option.PositiveTrust.Description=Die Menge an positivem Vertrauen, die bei einem Klick auf den Haken unter einer Nachricht zugewiesen werden soll.
-Page.Options.Option.NegativeTrust.Description=Die Menge an negativem Vertrauen, die bei einem Klick auf das rote X unter einer Nachricht zugewiesen werden soll. Dieser Wert sollte negativ sein.
-Page.Options.Option.TrustComment.Description=Der Kommentar, der im Web of Trust für Ihre Zuweisung angezeigt werden soll.
 Page.Options.Section.FcpOptions.Title=FCP-Schnittstellenoptionen
 Page.Options.Option.FcpInterfaceActive.Description=Die FCP-Schnittstelle aktivieren, um anderen Plugins und Programmen den Zugriff auf Ihr Sone-Plugin zu ermöglichen.
 Page.Options.Option.FcpFullAccessRequired.Description=FCP-Verbindungen nur von „erlaubten Hosts“ erlauben (siehe {link}Knoten-Konfiguration, Abschnitt “FCP”{/link}).
 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.
@@ -163,7 +163,7 @@ Page.EditProfileField.Page.Title=Benutzerdefiniertes Feld bearbeiten
 Page.EditProfileField.Text=Bitte geben Sie einen neuen Namen für dieses benutzerdefinierte Feld ein.
 Page.EditProfileField.Error.DuplicateFieldName=Der von Ihnen eingegebene Feldname existiert bereits.
 Page.EditProfileField.Button.Save=Ändern
-Page.EditProfileField.Button.Reset=Aten Namen wiederherstellen
+Page.EditProfileField.Button.Reset=Alten Namen wiederherstellen
 Page.EditProfileField.Button.Cancel=Namen nicht ändern
 
 Page.DeleteProfileField.Title=Benutzerdefiniertes Feld löschen - Sone
@@ -276,12 +276,6 @@ Page.DeleteAlbum.Text.AlbumWillBeGone=Ihr Album „{title}“ wird gelöscht. M
 Page.DeleteAlbum.Button.Yes=Ja, Album löschen.
 Page.DeleteAlbum.Button.No=Nein, Album nicht löschen.
 
-Page.Trust.Title=Sone vertrauen - Sone
-
-Page.Distrust.Title=Sone misstrauen - Sone
-
-Page.Untrust.Title=Sone nicht vertrauen - Sone
-
 Page.MarkAsKnown.Title=Als gelesen markieren - Sone
 
 Page.Bookmark.Title=Als Favorit markieren - Sone
@@ -305,7 +299,7 @@ Page.Rescue.Text.Fetching=Die Sonerettung versucht gerade, Version {0} Ihrer Son
 Page.Rescue.Text.Fetched=Die Sonerettung hat Version {0} Ihrer Sone herunter geladen. Bitte überprüfen Sie Ihre Nachrichten, Antworten und Ihr Profile. Bei Gefallen können Sie die Sone einfach entsperren.
 Page.Rescue.Text.FetchedLast=Die Sonerettung hat die letzte verfügbare Version Ihrer Sone herunter geladen. Wenn bis jetzt keine Version dabei war, die Sie wiederherstellen möchten, haben Sie jetzt kein Glück.
 Page.Rescue.Text.NotFetched=Die Sonerettung konnte Version {0} Ihrer Sone nicht herunter laden. Bitte versuchen Sie erneut, Version {0} herunter zu laden, oder versuchen Sie die nächstältere Version.
-Page.Rescue.Label.NextEdition=Nächste Version:
+Page.Rescue.Label.NextEdition=Nächste Version
 Page.Rescue.Button.Fetch=Version herunter laden
 
 Page.NoPermission.Title=Unberechtigter Zugriff - Sone
@@ -331,6 +325,12 @@ Page.Invalid.Title=Ungültige Aktion ausgeführt - Sone
 Page.Invalid.Page.Title=Ungültige Aktion ausgeführt
 Page.Invalid.Text=Eine ungültige Aktion wurde ausgeführt, oder eine gültige Aktion hatte ungültige Parameter. Bitte kehren Sie zur {link}Hauptseite{/link} zurück und versuchen Sie Ihre Aktion erneut. Wenn der Fehler weiterhin besteht, haben Sie wahrscheinlich einen Programmierfehler gefunden.
 
+Page.Metrics.Title=Metriken
+Page.Metrics.Page.Title=Metriken
+Page.Metrics.SoneInsertDuration.Title=Hochladedauer einer Sone
+Page.Metrics.SoneParseDuration.Title=Parsdauer einer Sone
+Page.Metrics.ConfigurationSaveDuration.Title=Konfigurationsspeicherdauer
+
 View.Search.Button.Search=Suchen
 
 View.CreateSone.Text.WotIdentityRequired=Um eine Sone anzulegen, brauchen Sie eine Identität aus dem {link}„Web of Trust“ Plugin{/link}.
@@ -358,9 +358,10 @@ View.Sone.Status.Downloading=Diese Sone wird gerade herunter geladen.
 View.Sone.Status.Inserting=Diese Sone wird gerade hoch geladen.
 
 View.SoneMenu.Link.AllAlbums=alle Alben
+View.SoneMenu.WebOfTrustLink=„Web of Trust“ Profil
 
 View.Post.UnknownAuthor=(unbekannt)
-View.Post.WebOfTrustLink=„Web of Trust“ Profil
+View.Post.WebOfTrustLink=WoT-Profil
 View.Post.Permalink=Nachrichtenlink
 View.Post.PermalinkAuthor=Autorenlink
 View.Post.Bookmarks.PostIsBookmarked=Nachricht ist ein Favorit, klicken, um Favoritenmarkierung zu entfernen
@@ -377,10 +378,6 @@ View.Post.ShowLess=weniger zeigen
 
 View.UpdateStatus.Text.ChooseSenderIdentity=Absender wählen
 
-View.Trust.Tooltip.Trust=Dieser Sone vertrauen
-View.Trust.Tooltip.Distrust=Dieser Sone misstrauen
-View.Trust.Tooltip.Untrust=Dieser Sone kein Vertrauen zuweisen
-
 View.CreateAlbum.Title=Album anlegen
 View.CreateAlbum.Label.Name=Name:
 View.CreateAlbum.Label.Description=Beschreibung:
@@ -431,9 +428,6 @@ WebInterface.DefaultText.Option.PostsPerPage=Anzahl der Nachrichten pro Seite
 WebInterface.DefaultText.Option.ImagesPerPage=Anzahl der Bilder pro Seite
 WebInterface.DefaultText.Option.CharactersPerPost=Anzahl der Zeichen, die eine Nachricht haben muss, damit er gekürzt wird
 WebInterface.DefaultText.Option.PostCutOffLength=Anzahl der Zeichen, die von einer gekürzten Nachricht angezeigt werden
-WebInterface.DefaultText.Option.PositiveTrust=Der positive Vertrauenswert
-WebInterface.DefaultText.Option.NegativeTrust=Der negative Vertrauenswert
-WebInterface.DefaultText.Option.TrustComment=Der Kommentar für die Vertrauenszuweisung
 WebInterface.Button.Comment=Antworten
 WebInterface.Confirmation.DeletePostButton=Ja, löschen!
 WebInterface.Confirmation.DeleteReplyButton=Ja, löschen!
@@ -471,3 +465,4 @@ Notification.Mention.Text=Sie wurden in diesen Nachrichten erwähnt:
 Notification.SoneIsInserting.Text=Ihre Sone sone://{0} wird jetzt hoch geladen.
 Notification.SoneIsInserted.Text=Ihre Sone sone://{0} wurde in {1,number} {1,choice,0#Sekunden|1#Sekunde|1<Sekunden} hoch geladen.
 Notification.SoneInsertAborted.Text=Ihre Sone sone://{0} konnte nicht hoch geladen werden.
+Notification.SoneLockedOnStartup.Text=Versionen vor v81 hatten einen Fehler, der Sones ohne Inhalte zur Folge hatte. Um zu verhindern, dass solchermaßen defekte Sones hoch geladen werden, wurden diese automatisch gesperrt. Bitte überprüfen Sie Ihre Sones, benutzen Sie den Sonerettungsmodus und entsperren Sie die Sones manuell, wenn Sie mit den Inhalten Ihrer Sones zufrieden sind. Die gesperrten Sones sind:
index 7b62cf1..7c65dcf 100644 (file)
@@ -26,6 +26,8 @@ Navigation.Menu.Sone.Item.Rescue.Name=Rescue
 Navigation.Menu.Sone.Item.Rescue.Tooltip=Rescue Sone
 Navigation.Menu.Sone.Item.About.Name=About
 Navigation.Menu.Sone.Item.About.Tooltip=Information about Sone
+Navigation.Menu.Sone.Item.Metrics.Name=Metrics
+Navigation.Menu.Sone.Item.Metrics.Tooltip=Metrics collected by Sone
 
 Page.About.Title=About - Sone
 Page.About.Page.Title=About
@@ -66,16 +68,14 @@ Page.Options.Option.ImagesPerPage.Description=The number of images to display on
 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.TrustOptions.Title=Trust Settings
-Page.Options.Option.PositiveTrust.Description=The amount of positive trust you want to assign to other Sones by clicking the checkmark below a post or reply.
-Page.Options.Option.NegativeTrust.Description=The amount of trust you want to assign to other Sones by clicking the red X below a post or reply. This value should be negative.
-Page.Options.Option.TrustComment.Description=The comment that will be set in the web of trust for any trust you assign from Sone.
 Page.Options.Section.FcpOptions.Title=FCP Interface Settings
 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=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.
@@ -276,12 +276,6 @@ Page.DeleteAlbum.Text.AlbumWillBeGone=This will remove your album “{title}”.
 Page.DeleteAlbum.Button.Yes=Yes, delete album.
 Page.DeleteAlbum.Button.No=No, don’t delete album.
 
-Page.Trust.Title=Trust Sone - Sone
-
-Page.Distrust.Title=Distrust Sone - Sone
-
-Page.Untrust.Title=Untrust Sone - Sone
-
 Page.MarkAsKnown.Title=Mark as Known - Sone
 
 Page.Bookmark.Title=Bookmark - Sone
@@ -305,7 +299,7 @@ Page.Rescue.Text.Fetching=The Sone Rescuer is currently fetching edition {0} of
 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.Label.NextEdition=Next edition
 Page.Rescue.Button.Fetch=Fetch edition
 
 Page.NoPermission.Title=Unauthorized Access - Sone
@@ -331,6 +325,12 @@ 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}.
@@ -358,9 +358,10 @@ 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=(unknown)
-View.Post.WebOfTrustLink=web of trust profile
+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
@@ -377,10 +378,6 @@ View.Post.ShowLess=show less
 
 View.UpdateStatus.Text.ChooseSenderIdentity=Choose the sender identity
 
-View.Trust.Tooltip.Trust=Trust this person
-View.Trust.Tooltip.Distrust=Assign negative trust to this person
-View.Trust.Tooltip.Untrust=Remove your trust assignment for this person
-
 View.CreateAlbum.Title=Create Album
 View.CreateAlbum.Label.Name=Name:
 View.CreateAlbum.Label.Description=Description:
@@ -431,9 +428,6 @@ 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.DefaultText.Option.PositiveTrust=The positive trust to assign
-WebInterface.DefaultText.Option.NegativeTrust=The negative trust to assign
-WebInterface.DefaultText.Option.TrustComment=The comment to set in the web of trust
 WebInterface.Button.Comment=Comment
 WebInterface.Confirmation.DeletePostButton=Yes, delete!
 WebInterface.Confirmation.DeleteReplyButton=Yes, delete!
@@ -471,3 +465,4 @@ 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 f5072c8..bc90612 100644 (file)
@@ -26,6 +26,8 @@ Navigation.Menu.Sone.Item.Rescue.Name=Rescate
 Navigation.Menu.Sone.Item.Rescue.Tooltip=Rescatar Sone
 Navigation.Menu.Sone.Item.About.Name=Información
 Navigation.Menu.Sone.Item.About.Tooltip=Información sobre Sone
+Navigation.Menu.Sone.Item.Metrics.Name=Metrics
+Navigation.Menu.Sone.Item.Metrics.Tooltip=Metrics collected by Sone
 
 Page.About.Title=Información - Sone
 Page.About.Page.Title=Información
@@ -66,16 +68,14 @@ Page.Options.Option.ImagesPerPage.Description=Número de imágenes a mostrar en
 Page.Options.Option.CharactersPerPost.Description=Número de carácteres a mostrar de una publicación antes de acortarla y mostrar un link para expandirla (-1 para deshabilitar esta opción). La longitud de la acortación está determinada por la siguiente opción.
 Page.Options.Option.PostCutOffLength.Description=Número de carácteres que son mostrados si una publicación es demasiado larga (Mirar la opción anterior).Ignoralo si "Número de carácteres a mostrar" está desactivado (Valor en -1).
 Page.Options.Option.RequireFullAccess.Description=Denegar el acceso a Sone a cualquier host al que no se haya garantizado acceso completo.
-Page.Options.Section.TrustOptions.Title=Opciones de veracidad.
-Page.Options.Option.PositiveTrust.Description=Cantidad de veracidad positiva que quieres asignar a otros Sone al clicar en el tick tras una publicación o respuesta.
-Page.Options.Option.NegativeTrust.Description=Cantidad de veracidad que quieres asignar a otros Sone al clicar la X roja tras una publicación o respuesta. Este valor debería ser negativo.
-Page.Options.Option.TrustComment.Description=El comentario que se mostrara en la Web of Trust para cualquier veracidad que asignes desde Sone.
 Page.Options.Section.FcpOptions.Title=Opciones de la interfaz FCP
 Page.Options.Option.FcpInterfaceActive.Description=Activar la interfaz FCP para permitir a otros plugins y clientes remotos acceder al plugin de Sone.
 Page.Options.Option.FcpFullAccessRequired.Description=Requiere conexión FCP de los hosts permitidos (Comprueva tu {link}configuración del nodo, sección “FCP”{/link})
 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.
@@ -276,12 +276,6 @@ Page.DeleteAlbum.Text.AlbumWillBeGone=Esto eliminará tu álbum “{title}”. R
 Page.DeleteAlbum.Button.Yes=Si, eliminar álbum.
 Page.DeleteAlbum.Button.No=No, no eliminar álbum.
 
-Page.Trust.Title=Dar veracidad al Sone - Sone
-
-Page.Distrust.Title=Retirar veracidad al Sone - Sone
-
-Page.Untrust.Title=Quitar veracidad al Sone - Sone
-
 Page.MarkAsKnown.Title=Marcar como conocido - Sone
 
 Page.Bookmark.Title=Marcar - Sone
@@ -305,7 +299,7 @@ Page.Rescue.Text.Fetching=El modo rescate está obteniendo actualmente la edici
 Page.Rescue.Text.Fetched=El modo rescate ha descargado la edición {0} de tu Sone. Por favor, comprueba tus publicaciones, respuestas y perfil. Si te gusta lo que tiene el Sone actual, desbloquealo.
 Page.Rescue.Text.FetchedLast=El rescatador de Sone ha descargado la  última versión disponibe. Si no ha conseguido restaurar tu Sone probablemente no te queda suerte.
 Page.Rescue.Text.NotFetched=El rescatador de Sone no ha podido descargar la edición {0} de tu Sone. Por favor, vuelve ha intentarlo con la edición {0}, o prueba con la siguiente versión antigua.
-Page.Rescue.Label.NextEdition=Siguiente edición:
+Page.Rescue.Label.NextEdition=Siguiente edición
 Page.Rescue.Button.Fetch=Obtener edición
 
 Page.NoPermission.Title=Acceso desautorizado - Sone
@@ -331,6 +325,12 @@ Page.Invalid.Title=Acción invalida realizada - Sone
 Page.Invalid.Page.Title=Acción invalida realizada
 Page.Invalid.Text=Se ha realizado una acción inválida, o la acción era válida pero los parámetros no. Por favor, vuelve al {link}índice{/link} e intentalo de nuevo. Si el error persiste, probablemente hayas encontrado un 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=Buscar
 
 View.CreateSone.Text.WotIdentityRequired=Para crear un Sone necesitas una identidad del {link}plugin Web of Trust{/link}.
@@ -358,6 +358,7 @@ View.Sone.Status.Downloading=Este Sone está siendo descargado.
 View.Sone.Status.Inserting=Este Sone está siendo insertado.
 
 View.SoneMenu.Link.AllAlbums=todos los albumes
+View.SoneMenu.WebOfTrustLink=perfil de web of trust
 
 View.Post.UnknownAuthor=(desconocido)
 View.Post.WebOfTrustLink=perfil de web of trust
@@ -369,7 +370,7 @@ View.Post.DeleteLink=Eliminar
 View.Post.SendReply=Publicar respuesta!
 View.Post.Reply.DeleteLink=Eliminar
 View.Post.LikeLink=Like
-View.Post.UnlikeLink=Dislike
+View.Post.UnlikeLink=Unlike
 View.Post.ShowSource=Habilitar/Deshabilitar Parser
 View.Post.NotDownloaded=Esta publicación aun no ha sido descargada, o ha sido eliminada.
 View.Post.ShowMore=mostrar más
@@ -377,10 +378,6 @@ View.Post.ShowLess=mostrar menos
 
 View.UpdateStatus.Text.ChooseSenderIdentity=Elegir la identidad del emisario
 
-View.Trust.Tooltip.Trust=Creer a esta persona
-View.Trust.Tooltip.Distrust=Asignar veracidad negativa a esta persona
-View.Trust.Tooltip.Untrust=Eliminar la veracidad asignada a esta persona
-
 View.CreateAlbum.Title=Crear Álbum
 View.CreateAlbum.Label.Name=Nombre:
 View.CreateAlbum.Label.Description=Descripción:
@@ -431,9 +428,6 @@ WebInterface.DefaultText.Option.PostsPerPage=Número de publicaciones a mostrar
 WebInterface.DefaultText.Option.ImagesPerPage=Número de imágenes a mostrar en una página
 WebInterface.DefaultText.Option.CharactersPerPost=Número de carácteres de una publicación antes de acortarla
 WebInterface.DefaultText.Option.PostCutOffLength=Número de carácteres de los fragmentos de la publicación acortada
-WebInterface.DefaultText.Option.PositiveTrust=Veracidad positiva a asignar
-WebInterface.DefaultText.Option.NegativeTrust=Veracidad negativa a asignar
-WebInterface.DefaultText.Option.TrustComment=Comentario a poner en Web of Trust
 WebInterface.Button.Comment=Comentar
 WebInterface.Confirmation.DeletePostButton=Si, eliminar!
 WebInterface.Confirmation.DeleteReplyButton=Si, eliminar!
@@ -471,4 +465,4 @@ Notification.Mention.Text=Has sido mencionado en las siguientes publicaciones:
 Notification.SoneIsInserting.Text=Tu Sone sone://{0} está siendo insertado.
 Notification.SoneIsInserted.Text=Tu Sone sone://{0} ha sido insertado en {1,number} {1,choice,0#segundos|1#segundo|1<segundos}.
 Notification.SoneInsertAborted.Text=Tu Sone sone://{0} no pudo ser insertado.
-# 55-61
+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 86015d0..bf1eccd 100644 (file)
@@ -26,6 +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=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
@@ -52,30 +54,28 @@ Page.Options.Option.ShowAvatars.Followed.Description=Ne montrer que les avatars
 Page.Options.Option.ShowAvatars.ManuallyTrusted.Description=Ne montrer que les avatars des Sones auxquelles vous êtes assignés manuellement une confiance superieure à 0.
 Page.Options.Option.ShowAvatars.Trusted.Description=Ne montrer que les avatars des Sones qui ont une confiance superieure à 0.
 Page.Options.Option.ShowAvatars.Always.Description=Montre tout le temps les avatars personnalisés. Attention: certains avatars peuvent être offensants !
-Page.Options.Section.LoadLinkedImagesOptions.Title=Load Linked Images
-Page.Options.Option.LoadLinkedImages.Description=Sone can automatically try to load images that are linked to in posts and replies. This will only ever load images from Freenet, never from the internet!
-Page.Options.Option.LoadLinkedImages.Never.Description=Never load linked images.
-Page.Options.Option.LoadLinkedImages.Followed.Description=Only load images linked from Sones that you follow.
-Page.Options.Option.LoadLinkedImages.ManuallyTrusted.Description=Only load images linked from Sones that you have manually assigned a trust value larger than 0 to.
-Page.Options.Option.LoadLinkedImages.Trusted.Description=Only load images linked from Sones that have a trust value larger than 0.
-Page.Options.Option.LoadLinkedImages.Always.Description=Always load linked images. Be warned: some images might be disturbing or considered offensive.
-Page.Options.Section.RuntimeOptions.Title=Comportement runtime
+Page.Options.Section.LoadLinkedImagesOptions.Title=Chargement des images liées
+Page.Options.Option.LoadLinkedImages.Description=Sone essaie automatiquement de charger les images liées à des messages ou à des réponses. Tous les chargements sont faits depuis Freenet, jamais depuis internet !
+Page.Options.Option.LoadLinkedImages.Never.Description=Ne jamais charger les images liées.
+Page.Options.Option.LoadLinkedImages.Followed.Description=Charge uniquement les images liées à Sones que vous suivez.
+Page.Options.Option.LoadLinkedImages.ManuallyTrusted.Description=Ne charger que des images liées des Sones auxquelles vous êtes assignés manuellement une confiance superieure à 0.
+Page.Options.Option.LoadLinkedImages.Trusted.Description=Ne charger que des images liées à des Sones qui ont une confiance superieure à 0.
+Page.Options.Option.LoadLinkedImages.Always.Description=Charge tout le temps les images liées. Attention: certains avatars peuvent être offensants !
+Page.Options.Section.RuntimeOptions.Title=Comportement de l'exécution
 Page.Options.Option.InsertionDelay.Description=Le délai d'insertion d'un Sone.
 Page.Options.Option.PostsPerPage.Description=Le nombre de message à afficher par page avant que les boutons de pagination soit affichés.
 Page.Options.Option.ImagesPerPage.Description=Le nombre de message à afficher par page avant que les boutons de pagination soit affichés.
 Page.Options.Option.CharactersPerPost.Description=Le nombre de caractères à afficher par message avant que le lien proposant de voir l'intégralité ne soit proposé (-1 pour désactiver). La taille du composant est déterminée par l'option ci-dessous.
 Page.Options.Option.PostCutOffLength.Description=Le nombre de caractères à afficher si le message est considéré comme trop long (voir option du dessus). Ignoré si "nombre de caractères à afficher" est désactivé (configuré à -1).
 Page.Options.Option.RequireFullAccess.Description=Pour refuser l'accès à Sone à tout hôte à qui un accès complet n'a pas été accordé.
-Page.Options.Section.TrustOptions.Title=Réglages de confiance
-Page.Options.Option.PositiveTrust.Description=La quantité de note de confiance positive que vous voulez assigner à d'autres Sones en cochant la case en dessous d'un message ou d'une réponse.
-Page.Options.Option.NegativeTrust.Description=La quantité de note de confiance que vous voulez assigner à d'autres Sones en cliquant le X rouge en dessous d'un message ou d'une réponse. Cette valeur devrait être négative.
-Page.Options.Option.TrustComment.Description=Le commentaire qui sera mis dans le web of trust pour chaque note de confiance que vous assignez de Sone.
 Page.Options.Section.FcpOptions.Title=Réglages de l'Interface FCP
 Page.Options.Option.FcpInterfaceActive.Description=Activer l'interface FCP afin de permettre à d'autres plugins et clients à distance d'accéder à votre plugin Sone.
 Page.Options.Option.FcpFullAccessRequired.Description=Requière une connexion FCP d'hôtes autorisés (Veuillez voir votre {link}configuration du noeud, section “FCP”{/link})
 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.
@@ -196,7 +196,7 @@ Page.ViewSone.Profile.Title=Profile
 Page.ViewSone.Profile.Label.Name=Nom
 Page.ViewSone.Profile.Label.Albums=Albums
 Page.ViewSone.Profile.Albums.Text.All=Tous les albums
-Page.ViewSone.Profile.Name.WoTLink=Profile Web of trust
+Page.ViewSone.Profile.Name.WoTLink=Profil Web of Trust
 Page.ViewSone.Replies.Title=Messages {sone} a répondu à
 
 Page.ViewPost.Title=Voir message - Sone
@@ -276,12 +276,6 @@ Page.DeleteAlbum.Text.AlbumWillBeGone=Vous allez supprimer l'Album “{title}”
 Page.DeleteAlbum.Button.Yes=Oui, supprimer cet album.
 Page.DeleteAlbum.Button.No=Non, ne pas supprimer cet album.
 
-Page.Trust.Title=Faire confiance à Sone - Sone
-
-Page.Distrust.Title=Se méfier de ce Sone - Sone
-
-Page.Untrust.Title=Ne plus faire confiance en ce Sone - Sone
-
 Page.MarkAsKnown.Title=Marquer comme connu - Sone
 
 Page.Bookmark.Title=Marque-page - Sone
@@ -305,7 +299,7 @@ Page.Rescue.Text.Fetching=Le récupérateur de Sone est en train de restaurer la
 Page.Rescue.Text.Fetched=Le récupérateur de Sone a restauré la version {0} de votre Sone. Merci de vérifier vos messages, réponses et profile. Si les informations vous conviennent, débloquez la version.
 Page.Rescue.Text.FetchedLast=Le récupérateur de Sone a restauré la dernière version disponible. Si vous ne souhaitiez pas récupérer une ancienne version de Sone. Considérez que vous n'avez pas de chance.
 Page.Rescue.Text.NotFetched=Le récupérateur de Sone ne peut pas restaurer la version {0} de votre Sone. Merci de réessayer, ou essayez avec une version plus ancienne.
-Page.Rescue.Label.NextEdition=Prochaine version:
+Page.Rescue.Label.NextEdition=Prochaine version
 Page.Rescue.Button.Fetch=Récupérer la version.
 
 Page.NoPermission.Title=Accès non autorisé - Sone
@@ -322,8 +316,8 @@ Page.EmptyAlbumTitle.Text.EmptyAlbumTitle=Vous devez donner un titre à votre al
 
 Page.DismissNotification.Title=Effacer la notification - Sone
 
-Page.WotPluginMissing.Text.LoadPlugin=Veuillez charger le plugin Web of Trust dans le {link}plugin manager{/link}.
 Page.WotPluginMissing.Text.WotRequired=Parce que Web of Trust est une partie intégrante de Sone, le plugin Web of trust doit être chargé afin de pouvoir faire fontionner Sone.
+Page.WotPluginMissing.Text.LoadPlugin=Veuillez charger le plugin Web of Trust dans le {link}plugin manager{/link}.
 
 Page.Logout.Title=Déconnexion - Sone
 
@@ -331,6 +325,12 @@ Page.Invalid.Title=Action invalide réalisée - Sone
 Page.Invalid.Page.Title=Action invalide réalisée
 Page.Invalid.Text=Une action invalide a été effectuée, ou l'action était valide mes les paramètres ne l'étaient pas. Veuillez retourner à la {link}page d'index{/link} et veuillez réessayer. Si l'erreur persiste, vous avez probablement trouvé un bug.
 
+Page.Metrics.Title=Métriques
+Page.Metrics.Page.Title=Métriques
+Page.Metrics.SoneInsertDuration.Title=Durée d'insertion de Sone
+Page.Metrics.SoneParseDuration.Title=Durée d'analyse de Sone
+Page.Metrics.ConfigurationSaveDuration.Title=Durée de sauvegarde de configuration
+
 View.Search.Button.Search=Recherche
 
 View.CreateSone.Text.WotIdentityRequired=Pour créer un Sone vous avez besoin d'une identité venant du {link}plugin Web of Trust {/link}.
@@ -358,9 +358,10 @@ View.Sone.Status.Downloading=Ce Sone est en train d'être téléchargé.
 View.Sone.Status.Inserting=Ce Sone est en train d'être inséré.
 
 View.SoneMenu.Link.AllAlbums=Tous les Albums
+View.SoneMenu.WebOfTrustLink=Profile web of trust
 
 View.Post.UnknownAuthor=(inconnu)
-View.Post.WebOfTrustLink=Profile web of trust
+View.Post.WebOfTrustLink=Profil Web of Trust
 View.Post.Permalink=Lier le message
 View.Post.PermalinkAuthor=Lier l'auteur
 View.Post.Bookmarks.PostIsBookmarked=Ce message a été ajouté aux marque-pages, cliquer pour retirer des marque-pages
@@ -377,10 +378,6 @@ View.Post.ShowLess=Vois moins
 
 View.UpdateStatus.Text.ChooseSenderIdentity=Choisir l'identité de l'expéditeur
 
-View.Trust.Tooltip.Trust=Faire confiance à cette personne
-View.Trust.Tooltip.Distrust=Assigner une note négative en confiance à cette personne
-View.Trust.Tooltip.Untrust=Retirer votre note de confiance de cette personne
-
 View.CreateAlbum.Title=Créer un Album
 View.CreateAlbum.Label.Name=Nom:
 View.CreateAlbum.Label.Description=Description:
@@ -431,9 +428,6 @@ WebInterface.DefaultText.Option.PostsPerPage=Nombre de messages à afficher par
 WebInterface.DefaultText.Option.ImagesPerPage=Nombre d'images à afficher par page
 WebInterface.DefaultText.Option.CharactersPerPost=Nombre de caractère qu'une publication doit avoir pour être raccourcie
 WebInterface.DefaultText.Option.PostCutOffLength=Nombre de caractère du descriptif de la publication raccourcie
-WebInterface.DefaultText.Option.PositiveTrust=La note de confiance positive à assigner
-WebInterface.DefaultText.Option.NegativeTrust=Une note de confiance négative à assigner
-WebInterface.DefaultText.Option.TrustComment=Le commentaire à mettre dans le "Web of Trust"
 WebInterface.Button.Comment=Commenter
 WebInterface.Confirmation.DeletePostButton=Oui, effacer!
 WebInterface.Confirmation.DeleteReplyButton=Oui, effacer!
@@ -471,4 +465,4 @@ Notification.Mention.Text=Vous avez été mentionné dans les messages suivants:
 Notification.SoneIsInserting.Text=Votre Sone sone://{0} va maintenant être inséré.
 Notification.SoneIsInserted.Text=votre Sone sone://{0} a été inséré dans {1,number} {1,choice,0#seconds|1#second|1<seconds}.
 Notification.SoneInsertAborted.Text=Votre Sone sone://{0} ne peut pas être inséré.
-# 55-61
+Notification.SoneLockedOnStartup.Text=Les versions antérieures à v81 avaient un bug vidant les Sones. Pour éviter d'insérer des Sones vides ils ont été automatiquement vérouillés. Vérifiez vos Sones, utilisez le mode récupération si nécessaire puis dévérouillez vos Sones lorsque vous êtes satisfait du résultat. Les Sones vérouillés sont:
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 bf81762..535140f 100644 (file)
@@ -26,6 +26,8 @@ Navigation.Menu.Sone.Item.Rescue.Name=復帰
 Navigation.Menu.Sone.Item.Rescue.Tooltip=Soneを復帰する
 Navigation.Menu.Sone.Item.About.Name=情報
 Navigation.Menu.Sone.Item.About.Tooltip=Soneについて
+Navigation.Menu.Sone.Item.Metrics.Name=Metrics
+Navigation.Menu.Sone.Item.Metrics.Tooltip=Metrics collected by Sone
 
 Page.About.Title=情報 - Sone
 Page.About.Page.Title=情報
@@ -66,16 +68,14 @@ Page.Options.Option.ImagesPerPage.Description=ページ送りのボタンが表
 Page.Options.Option.CharactersPerPost.Description=投稿を切って全文を見るリンクが表示されるまでの文字数。(-1で無効になります。)文字数は以下の設定により判定されます。
 Page.Options.Option.PostCutOffLength.Description=投稿が長い場合に表示される文字数。(上記の設定も参照してください。)
 Page.Options.Option.RequireFullAccess.Description=完全なアクセスが設定されていないホストに対してSoneへのアクセスを拒否する
-Page.Options.Section.TrustOptions.Title=信用設定
-Page.Options.Option.PositiveTrust.Description=返信のリンクの下に表示されるチェックメークのリンクをクリックした際にSoneに設定されるポジティブな信用値。
-Page.Options.Option.NegativeTrust.Description=返信のリンクの下に表示される赤い☓のリンクをクリックした際にSoneに設定されるネガティブな信用値。
-Page.Options.Option.TrustComment.Description=Soneで信用値を設定した場合に設定されるWoTコメント。
 Page.Options.Section.FcpOptions.Title=FCPインターフェースの設定
 Page.Options.Option.FcpInterfaceActive.Description=FCPインターフェースを有効にし、Soneプラグインに他のプラグインやリモートクライアントからアクセスできるようにする。
 Page.Options.Option.FcpFullAccessRequired.Description=許可されたホストのみFCP接続を許可する。({link}ノード設定の「FCP」内の設定{/link}も確認してください。)
 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}にプラグインの設定を初期化する場合はこの項目も有効にしてください。
@@ -276,12 +276,6 @@ Page.DeleteAlbum.Text.AlbumWillBeGone=これにより“{title}”のアルバ
 Page.DeleteAlbum.Button.Yes=はい、アルバムを削除します。
 Page.DeleteAlbum.Button.No=いいえ、アルバムを削除しません。
 
-Page.Trust.Title=Soneを信用する - Sone
-
-Page.Distrust.Title=Soneを信用しない - Sone
-
-Page.Untrust.Title=Soneの信用を解除する - Sone
-
 Page.MarkAsKnown.Title=既知のマークを付ける - Sone
 
 Page.Bookmark.Title=ブックマーク - Sone
@@ -305,7 +299,7 @@ Page.Rescue.Text.Fetching=Sone復帰モードでは現在{0}の版を読み込
 Page.Rescue.Text.Fetched=Sone復帰モードは{0}の版を読み込みました。投稿、返信、プロフィールなどを確認の上、問題がなければロックを解除してください。
 Page.Rescue.Text.FetchedLast=Sone復帰モードは存在する全ての版を読み込みました。この時点で読み込めていない場合は恐らく復帰は不可能でしょう。
 Page.Rescue.Text.NotFetched=Sone復帰モードは{0}の版を読み込むことはできませんでした。{0}の版を試してみるか、さらに次の版を試してみてください。
-Page.Rescue.Label.NextEdition=次の版:
+Page.Rescue.Label.NextEdition=次の版
 Page.Rescue.Button.Fetch=版を取り出す
 
 Page.NoPermission.Title=不正なアクセス - Sone
@@ -331,6 +325,12 @@ Page.Invalid.Title=不正な操作が行われました - Sone
 Page.Invalid.Page.Title=不正な操作が行われました
 Page.Invalid.Text=不正な操作が行われたか、操作が正常でもパラメーターが不正である可能性があります。{link}インデックスページ{/link}に戻って試してみてください。同じ問題が何度も発生するようであればバグを見つけた可能性があります。
 
+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=検索
 
 View.CreateSone.Text.WotIdentityRequired=Soneを作成するのには{link}Web of Trustプラグイン{/link}のプロフィールが必要です。
@@ -358,6 +358,7 @@ View.Sone.Status.Downloading=このSoneは現在ダウンロード中です。
 View.Sone.Status.Inserting=このSoneは現在インサート中です。
 
 View.SoneMenu.Link.AllAlbums=全てのアルバム
+View.SoneMenu.WebOfTrustLink=Web of Trustプロフィール
 
 View.Post.UnknownAuthor=(不明)
 View.Post.WebOfTrustLink=Web of Trustプロフィール
@@ -377,10 +378,6 @@ View.Post.ShowLess=一部を見る
 
 View.UpdateStatus.Text.ChooseSenderIdentity=送信者のプロフィールを選択してください
 
-View.Trust.Tooltip.Trust=この人を信用する
-View.Trust.Tooltip.Distrust=この人にネガティブな信用を付与する
-View.Trust.Tooltip.Untrust=この人につけた信用を解除する
-
 View.CreateAlbum.Title=アルバムを作成
 View.CreateAlbum.Label.Name=名称:
 View.CreateAlbum.Label.Description=説明:
@@ -431,9 +428,6 @@ WebInterface.DefaultText.Option.PostsPerPage=ページに表示する投稿の
 WebInterface.DefaultText.Option.ImagesPerPage=ページに表示する画像の数
 WebInterface.DefaultText.Option.CharactersPerPost=投稿を短く表示する最低の文字数
 WebInterface.DefaultText.Option.PostCutOffLength=投稿を短く表示する場合の文字数
-WebInterface.DefaultText.Option.PositiveTrust=割り当てるポジティブな信用値
-WebInterface.DefaultText.Option.NegativeTrust=割り当てるネガティブな信用値
-WebInterface.DefaultText.Option.TrustComment=Web of Trustに設定するコメント
 WebInterface.Button.Comment=コメント
 WebInterface.Confirmation.DeletePostButton=本当に消去!
 WebInterface.Confirmation.DeleteReplyButton=本当に消去!
@@ -471,4 +465,4 @@ Notification.Mention.Text=次の投稿でメンションされています:
 Notification.SoneIsInserting.Text=あなたのSone sone://{0}は現在インサート中です。
 Notification.SoneIsInserted.Text=あなたのSone sone://{0}は{1,number}{1,choice,0#秒|1#秒|1<秒}でインサートされました。
 Notification.SoneInsertAborted.Text=あなたのSone sone://{0}のインサートに失敗しました。
-# 55-51, 67, 107, 465
+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 5a13696..967faca 100644 (file)
@@ -26,6 +26,8 @@ Navigation.Menu.Sone.Item.Rescue.Name=Redder
 Navigation.Menu.Sone.Item.Rescue.Tooltip=Redd Sone
 Navigation.Menu.Sone.Item.About.Name=Om
 Navigation.Menu.Sone.Item.About.Tooltip=Informasjon om Sone
+Navigation.Menu.Sone.Item.Metrics.Name=Metrics
+Navigation.Menu.Sone.Item.Metrics.Tooltip=Metrics collected by Sone
 
 Page.About.Title=Om - Sone
 Page.About.Page.Title=Om
@@ -66,16 +68,14 @@ Page.Options.Option.ImagesPerPage.Description=Antall bilder å vise på en side
 Page.Options.Option.CharactersPerPost.Description=Antall tegn å vise fra et innlegg før resten blir skjult og en link blir vist for å utvide til hele innlegget (-1 for å deaktivere). Lengden på den viste teksten kan endres under.
 Page.Options.Option.PostCutOffLength.Description=Antallet tegn som blir vist hvis et innlegg er for langt (Se innstilling over).
 Page.Options.Option.RequireFullAccess.Description=For å avslå tilgang til Sone fra enhver host som ikke har blitt gitt full tilgang.
-Page.Options.Section.TrustOptions.Title=Tillitsinnstillinger
-Page.Options.Option.PositiveTrust.Description=Mengden positiv tillit du ønsker å gi en annen Sone ved å klikke på hake-merket under et innlegg eller ved å skrive et svar.
-Page.Options.Option.NegativeTrust.Description=Mengden tillit du vil gi til andre Soner ved å klikke på den røde X'en nedenfor et innlegg eller svar. Denne verdien burde være negativ.
-Page.Options.Option.TrustComment.Description=Kommentaren som vil bli satt i 'Web Of Trust' for all tillit gitt via Sone.
 Page.Options.Section.FcpOptions.Title=FCP-grensesnitts innstillinger
 Page.Options.Option.FcpInterfaceActive.Description=Aktiver FCP-grensesnittet for å tillate andre tillegg og andre klienter til å aksessere dit Sone-tillegg.
 Page.Options.Option.FcpFullAccessRequired.Description=Påkrev FCP tilkobling fra tillate hoster (se din {link}nodes konfigurasjon, seksjon «FCP»{/link})
 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.
@@ -92,10 +92,10 @@ Page.Login.CreateSone.Title=Lag Sone
 Page.CreateSone.Title=Lag sone - Sone
 
 Page.DeleteSone.Title=Slett Sonen - Sone
-Page.DeleteSone.Button.No=Nei, ikke slett.
-Page.DeleteSone.Button.Yes=Ja, slett.
-Page.DeleteSone.Page.Description=Dette vil ikke slette Sonen fra Freenet (Fordi det er umulig), det vil bare koble fra ditt 'Web Of Trust'-pseudonym fra Sone.
 Page.DeleteSone.Page.Title=Slette Sonen «{sone}»?
+Page.DeleteSone.Page.Description=Dette vil ikke slette Sonen fra Freenet (Fordi det er umulig), det vil bare koble fra ditt 'Web Of Trust'-pseudonym fra Sone.
+Page.DeleteSone.Button.Yes=Ja, slett.
+Page.DeleteSone.Button.No=Nei, ikke slett.
 
 Page.Index.Title=Din Sone - Sone
 Page.Index.Label.Text=Innleggstekst:
@@ -276,12 +276,6 @@ Page.DeleteAlbum.Text.AlbumWillBeGone=Dette vil fjerne albumet ditt, «{title}»
 Page.DeleteAlbum.Button.Yes=Ja, slett album.
 Page.DeleteAlbum.Button.No=Nei, ikke slett album.
 
-Page.Trust.Title=Positiv tillit til Sone - Sone
-
-Page.Distrust.Title=Negativ tillit til Sone - Sone
-
-Page.Untrust.Title=Fjern tillit til Sone - Sone
-
 Page.MarkAsKnown.Title=Merk som kjent - Sone
 
 Page.Bookmark.Title=Bokmerke - Sone
@@ -305,7 +299,7 @@ Page.Rescue.Text.Fetching=Sone-redderen laster foreløpig ned utgave {0} av din
 Page.Rescue.Text.Fetched=Sone-redderen har lastet ned utgave {0} av din Sone. Sjekk dine innlegg, svar og profil. Hvis du er fornøyd med redningen, kan du låse opp Sonen din.
 Page.Rescue.Text.FetchedLast=Sone-redderen har lastet ned den siste tilgjengelige utgaven. Hvis den ikke klarte å redde Sonen din, er det lite annet å gjøre.
 Page.Rescue.Text.NotFetched=Sone-redderen kunne ikke laste ned utgave {0} av din Sone. Enten prøv igjen på nytt med utgave {0}, eller prøv igjen med utgaven før.
-Page.Rescue.Label.NextEdition=Neste utgave:
+Page.Rescue.Label.NextEdition=Neste utgave
 Page.Rescue.Button.Fetch=Hent utgave
 
 Page.NoPermission.Title=Ikke-autorisert tilgang - Sone
@@ -331,6 +325,12 @@ Page.Invalid.Title=Ugyldig handling - Sone
 Page.Invalid.Page.Title=Ugyldig handling
 Page.Invalid.Text=En ugyldig handling ble gjort eller så var handlingens parametere ugyldig. Gå tilbake til {link}indeks{/link} og prøv på ny. Hvis feilmeldingen vedvarer har du sannsynligvis funnet en 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=Søk
 
 View.CreateSone.Text.WotIdentityRequired=For å lage en Sone trenger du et pseudonym fra {link}'Web of Trust'-tillegget{/link}.
@@ -358,6 +358,7 @@ View.Sone.Status.Downloading=Denne Sonen blir for øyeblikket lastet ned.
 View.Sone.Status.Inserting=Denne Sonen blir for øyeblikket innsatt.
 
 View.SoneMenu.Link.AllAlbums=alle album
+View.SoneMenu.WebOfTrustLink='web of trust'-profil
 
 View.Post.UnknownAuthor=(ukjent)
 View.Post.WebOfTrustLink='web of trust'-profil
@@ -377,18 +378,14 @@ View.Post.ShowLess=Vis mindre
 
 View.UpdateStatus.Text.ChooseSenderIdentity=Velg avsender-pseudonym
 
-View.Trust.Tooltip.Trust=Gi tillit til denne personen
-View.Trust.Tooltip.Distrust=Gi negativ tillit til denne personen
-View.Trust.Tooltip.Untrust=Fjern din tillit gitt til denne personen
-
 View.CreateAlbum.Title=Lag album
 View.CreateAlbum.Label.Name=Navn:
 View.CreateAlbum.Label.Description=Beskrivelse:
 
 View.UploadImage.Title=Last opp bilde
-View.UploadImage.Button.UploadImage=Last opp bilde
-View.UploadImage.Label.Description=Beskrivelse:
 View.UploadImage.Label.Title=Tittel:
+View.UploadImage.Label.Description=Beskrivelse:
+View.UploadImage.Button.UploadImage=Last opp bilde
 
 View.Time.InTheFuture=i framtiden
 View.Time.AFewSecondsAgo=noen få sekunder siden
@@ -423,17 +420,14 @@ WebInterface.DefaultText.CreateAlbum.Name=Album tittel
 WebInterface.DefaultText.CreateAlbum.Description=Album beskrivelse
 WebInterface.DefaultText.EditAlbum.Title=Albumtittel
 WebInterface.DefaultText.EditAlbum.Description=Albumbeskrivelse
-WebInterface.DefaultText.UploadImage.Description=Bildebeskrivelse
 WebInterface.DefaultText.UploadImage.Title=Bildetittel
-WebInterface.DefaultText.EditImage.Description=Bildebeskrivelse
+WebInterface.DefaultText.UploadImage.Description=Bildebeskrivelse
 WebInterface.DefaultText.EditImage.Title=Bildetittel
+WebInterface.DefaultText.EditImage.Description=Bildebeskrivelse
 WebInterface.DefaultText.Option.PostsPerPage=Antall innlegg å vise på en side
 WebInterface.DefaultText.Option.ImagesPerPage=Antall bilder å vise per side
 WebInterface.DefaultText.Option.CharactersPerPost=Antall tegn et innlegg må ha for å bli skjult.
 WebInterface.DefaultText.Option.PostCutOffLength=Antall tegn som vises når et innlegg blir skjult
-WebInterface.DefaultText.Option.PositiveTrust=Positiv tillit å gi
-WebInterface.DefaultText.Option.NegativeTrust=Negativ tillit å gi
-WebInterface.DefaultText.Option.TrustComment=Kommentaren til 'Web Of Trust'
 WebInterface.Button.Comment=Kommenter
 WebInterface.Confirmation.DeletePostButton=Ja, slett!
 WebInterface.Confirmation.DeleteReplyButton=Ja, slett!
@@ -471,4 +465,4 @@ Notification.Mention.Text=Du har blitt nevnt i følgende innlegg:
 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.
-# 55-61, 67, 107, 127-128, 315-317, 319-321, 465, 471-473
+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 e9e460b..253497b 100644 (file)
@@ -1,5 +1,5 @@
 Navigation.Menu.Sone.Name=Sone
-Navigation.Menu.Sone.Tooltip= Wtyczka Sieci Freenet
+Navigation.Menu.Sone.Tooltip=Wtyczka Sieci Freenet
 Navigation.Menu.Sone.Item.Login.Name=Login
 Navigation.Menu.Sone.Item.Login.Tooltip=Login do twojego Sone
 Navigation.Menu.Sone.Item.Index.Name=Twój Sone
@@ -26,6 +26,8 @@ Navigation.Menu.Sone.Item.Rescue.Name=Pomoc
 Navigation.Menu.Sone.Item.Rescue.Tooltip=Sone Pomoc
 Navigation.Menu.Sone.Item.About.Name=O Sone
 Navigation.Menu.Sone.Item.About.Tooltip=Informacje o Sone
+Navigation.Menu.Sone.Item.Metrics.Name=Metrics
+Navigation.Menu.Sone.Item.Metrics.Tooltip=Metrics collected by Sone
 
 Page.About.Title=O Sone - Sone
 Page.About.Page.Title=O Sone
@@ -66,16 +68,14 @@ Page.Options.Option.ImagesPerPage.Description=Ilość obrazków wyświetlanych n
 Page.Options.Option.CharactersPerPost.Description=Ilość znaków pokazywanych w poście zanim zostanie on obcięty i pojawi się link do jego rozszerzenia (-1 powoduje wyłączenie). Długość fragmentu zależy od poniższej opcji.
 Page.Options.Option.PostCutOffLength.Description=Ilość znaków które są pokazywane gdy post uznajemy za zbyt długi (zobacz opcję powyżej). Ignorowane jeżeli opcja “ilość znaków do pokazania” jest wyłączona (jest ustawiona na -1).
 Page.Options.Option.RequireFullAccess.Description=Opcja odmowy dostępu do Sone hostom bez przyznanego pełnego dostępu.
-Page.Options.Section.TrustOptions.Title=Ustawienia Zaufania
-Page.Options.Option.PositiveTrust.Description=Punkty pozytywnego zaufania, które chcesz przyznać innym użytkownikom Sone klikając na ikonę pod postem lub odpowiedzią.
-Page.Options.Option.NegativeTrust.Description=Punkty zaufania, które chcesz przyznać innym użytkownikom Sone klikając na czerwony krzyżyk pod postem lub odpowiedzią. Wartosć powinna być negatywna.
-Page.Options.Option.TrustComment.Description=Komentarz, który wyświetli się w sieci zaufania w momencie przyznawania punktów zaufania w obrębie Sone.
 Page.Options.Section.FcpOptions.Title=Ustawienia Interfejsu FCP
 Page.Options.Option.FcpInterfaceActive.Description=Uruchom interfejs FCP, aby umożliwić innym wtyczkom i klientom zdalnym dostęp do twojej wtyczki Sone.
 Page.Options.Option.FcpFullAccessRequired.Description=Wymagane połączenie FCP dla hostów z dostępem (patrz twoja {link}konfiguracja Freenet, sekcja “FCP”{/link})
 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.
@@ -93,7 +93,7 @@ Page.CreateSone.Title=Utwórz Sone - Sone
 
 Page.DeleteSone.Title=Usuń Sone - Sone
 Page.DeleteSone.Page.Title=Usuń Sone “{zone}”?
-Page.DeleteSone.Page.Description= Sone nie zostanie usunięte z Freenet (nie ma takiej możliwości), nastąpi jedynie wykluczenie tożsamości z siecii zaufania Sone.
+Page.DeleteSone.Page.Description=Sone nie zostanie usunięte z Freenet (nie ma takiej możliwości), nastąpi jedynie wykluczenie tożsamości z siecii zaufania Sone.
 Page.DeleteSone.Button.Yes=Tak, usuń.
 Page.DeleteSone.Button.No=Nie usuwaj.
 
@@ -108,7 +108,7 @@ Page.Index.PostList.Text.AutoFollowOption=Masz też możliwość aby automatyczn
 
 Page.New.Title=Nowe Posty i Odpowiedzi - Sone
 Page.New.Page.Title=Nowe Posty i Odpowiedzi
-Page.New.NothingNew= Brak nowych postów i odpowiedzi.
+Page.New.NothingNew=Brak nowych postów i odpowiedzi.
 
 Page.KnownSones.Title=Znane Sone - Sone
 Page.KnownSones.Page.Title=Znane Sone
@@ -176,7 +176,7 @@ Page.CreatePost.Title=Napisz post - Sone
 Page.CreatePost.Page.Title=Napisz post
 Page.CreatePost.Label.Text=Treść postu:
 Page.CreatePost.Button.Post=Wyślij!
-Page.CreatePost.Error.EmptyText= Nic nie napisałeś, szkoda. Sprobój napisać coś więcej!
+Page.CreatePost.Error.EmptyText=Nic nie napisałeś, szkoda. Sprobój napisać coś więcej!
 
 Page.CreateReply.Title=Napisz odpowiedź - Sone
 Page.CreateReply.Page.Title=Napisz odpowiedź
@@ -186,7 +186,7 @@ Page.CreateReply.Button.Post=Wyślij odpowiedź!
 
 Page.ViewSone.Title=Zobacz Sone - Sone
 Page.ViewSone.Page.TitleWithoutSone=Zobacz nieznany Sone
-Page.ViewSone.NoSone.Description= Nie ma obecnie Sone z tym ID {sone}. Jesli szukasz konkretnego użytkownika to upewnij się, że jest widoczny w twojej sieci zaufania.
+Page.ViewSone.NoSone.Description=Nie ma obecnie Sone z tym ID {sone}. Jesli szukasz konkretnego użytkownika to upewnij się, że jest widoczny w twojej sieci zaufania.
 Page.ViewSone.UnknownSone.Description=Ten Sone nie został jeszcze odzyskany. Proszę sprawdzić za chwilę.
 Page.ViewSone.UnknownSone.LinkToWebOfTrust=Ten Sone nie jest jeszcze znany, ale jego profil w Sieci Zaufania może być już dostępny.
 Page.ViewSone.WriteAMessage=Tu możesz napisać wiadomość do tego Sone. Każdy będzie mógł zobaczyć tą wiadomość!
@@ -215,7 +215,7 @@ Page.DeletePost.Button.No=Nie, nie usuwaj.
 
 Page.DeleteReply.Title=Usuń odpowiedź - Sone
 Page.DeleteReply.Page.Title=Usuń odpowiedź
-Page.DeleteReply.Text.PostWillBeGone= Usunięta odpowiedź nie będzie widoczna na twoim Sone. Nie zostanie jednak usunięta z Freenet, ponieważ nie ma takiej możlowość. Starsze wersje twojego Sone zawsze będa zawierały usunięte odpowiedzi.
+Page.DeleteReply.Text.PostWillBeGone=Usunięta odpowiedź nie będzie widoczna na twoim Sone. Nie zostanie jednak usunięta z Freenet, ponieważ nie ma takiej możlowość. Starsze wersje twojego Sone zawsze będa zawierały usunięte odpowiedzi.
 Page.DeleteReply.Button.Yes=Tak, usuń.
 Page.DeleteReply.Button.No=Nie, nie usuwaj.
 
@@ -276,19 +276,13 @@ Page.DeleteAlbum.Text.AlbumWillBeGone=Twój album zostanie usunięty “{title}
 Page.DeleteAlbum.Button.Yes=Tak, usuń album.
 Page.DeleteAlbum.Button.No=Nie, nie usuwaj albumu.
 
-Page.Trust.Title=Zaufaj Sone - Sone
-
-Page.Distrust.Title=Nie ufaj Sone - Sone
-
-Page.Untrust.Title=Przestań ufać Sone - Sone
-
 Page.MarkAsKnown.Title=Oznacz jako znany - Sone
 
 Page.Bookmark.Title=Zakładka - Sone
 Page.Unbookmark.Title=Usuń zakładkę - Sone
 Page.Bookmarks.Title=Zakładka - Sone
 Page.Bookmarks.Page.Title=Zakładki
-Page.Bookmarks.Text.NoBookmarks= Nie masz obecnie żadnych zakładek. Możesz dodać posty do zakładek klikając gwiazdkę poniżej postu.
+Page.Bookmarks.Text.NoBookmarks=Nie masz obecnie żadnych zakładek. Możesz dodać posty do zakładek klikając gwiazdkę poniżej postu.
 Page.Bookmarks.Text.PostsNotLoaded=Niektóre z zaznaczonych przez ciebie postów nie zostały wyświetlone, ponieważ wystąpił problem z ich załadowaniem. Dzieje się tak, jeśli Sone było niedawno restartowane, lub gdy posty zostały usunięte. Jeśli jesteś pewien, że posty zostały usunięte, to możesz je {link}odznaczyć{/link}.
 
 Page.Search.Title=Szukaj - Sone
@@ -303,9 +297,9 @@ Page.Rescue.Text.Description=Tryb Ratunkowy pozwala przywrócić poprzednią wer
 Page.Rescue.Text.Procedure=Tryb Rarunkowy polega na pobraniu ostatniej wprowadzonej edycji twojego Sone. Jeśli edycja zostanie poprawnie pobrana wówczas zostanie załadowana na twój Sone, co umożliwi zarządzanie twoimi postami, profilem oraz innymi ustawieniami (można to zrobić w nowej zakładce lub oknie przegladarki). Jeśli pobrana edycja różni się od tej, którą chcesz przywrócić, wówczas ustaw Tryb Ratunkowy tak, aby poprał jeszcze wcześniejszą edycję.
 Page.Rescue.Text.Fetching=Tryb Ratunkowy Sone pobiera właśnie edycję {0} twojego Sone.
 Page.Rescue.Text.Fetched=Tryb Ratunkowy Sone pobrał edycję {0} twojego Sone. Sprawdź swoje posty, odpowiedzi oraz profil. Jeśli podoba ci się zawartość aktualnego Sone, to mozesz go odblokować.
-Page.Rescue.Text.FetchedLast= Tryb Ratunkowy Sone pobrał ostatnią dostępną edycję. Jeśli nie udało się przywrócić twojego Sone, to nie masz teraz szczęścia.
+Page.Rescue.Text.FetchedLast=Tryb Ratunkowy Sone pobrał ostatnią dostępną edycję. Jeśli nie udało się przywrócić twojego Sone, to nie masz teraz szczęścia.
 Page.Rescue.Text.NotFetched=Tryb Ratunkowy Sone nie mógł sćiągnąć edycji {0} twojego Sone. Spróbuj pobrać ponownie edycję {0}, albo pobierz kolejną starszą edycję.
-Page.Rescue.Label.NextEdition=Następna edycja:
+Page.Rescue.Label.NextEdition=Następna edycja
 Page.Rescue.Button.Fetch=Pobierz edycję
 
 Page.NoPermission.Title=Nieupoważniony dostęp- Sone
@@ -331,6 +325,12 @@ Page.Invalid.Title=Niepoprawne działanie
 Page.Invalid.Page.Title=Niepoprawne działanie
 Page.Invalid.Text=Podjęto niepoprawne działanie, lub też działanie było poprawne, ale parametry nie. Wróć do{link}strony głównej{/link} i spróbuj ponownie. Jeśli problem będzie się utrzymywać to najprawdopodobniej znalazłeś błąd.
 
+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=Szukaj
 
 View.CreateSone.Text.WotIdentityRequired=Żeby utworzyć Sone potrzebujesz tożsamości z {link} wtyczki Sieć Zaufania{/link}.
@@ -358,6 +358,7 @@ View.Sone.Status.Downloading=Ten Sone jest właśnie ściągany.
 View.Sone.Status.Inserting=Ten Sone jest właśnie ładowany.
 
 View.SoneMenu.Link.AllAlbums=wszystkie albumy
+View.SoneMenu.WebOfTrustLink=Profil sieci zaufania
 
 View.Post.UnknownAuthor=(nieznany)
 View.Post.WebOfTrustLink=Profil sieci zaufania
@@ -377,10 +378,6 @@ View.Post.ShowLess=pokaż mniej
 
 View.UpdateStatus.Text.ChooseSenderIdentity=Wybierz tożsamość nadawcy
 
-View.Trust.Tooltip.Trust=Zaufaj tej osobie
-View.Trust.Tooltip.Distrust=Przypisz tej osobie nagatywny poziom zaufania
-View.Trust.Tooltip.Untrust=Cofnij swoje zaufanie dla tej osoby
-
 View.CreateAlbum.Title=Utwórz Album
 View.CreateAlbum.Label.Name=Nazwa:
 View.CreateAlbum.Label.Description=Opis:
@@ -431,9 +428,6 @@ WebInterface.DefaultText.Option.PostsPerPage=Ilość postów wyświetlanych na j
 WebInterface.DefaultText.Option.ImagesPerPage=Ilość obrazków na stronie
 WebInterface.DefaultText.Option.CharactersPerPost=Ilość znaków, które ma zawierać post, aby zostać skrócony
 WebInterface.DefaultText.Option.PostCutOffLength=Ilość znaków w skróconym poście
-WebInterface.DefaultText.Option.PositiveTrust=Pozytywny poziom zaufania
-WebInterface.DefaultText.Option.NegativeTrust=Negatywny poziom zaufania
-WebInterface.DefaultText.Option.TrustComment=Komentarz, który zostanie ustawiony w Sieci Zaufania
 WebInterface.Button.Comment=Komentuj
 WebInterface.Confirmation.DeletePostButton=Tak, usuń!
 WebInterface.Confirmation.DeleteReplyButton=Tak, usuń!
@@ -469,6 +463,6 @@ Notification.ImageInsertFailed.Text=Nie można załadowac następujących obraz
 Notification.Mention.ShortText=Zostałeś oznaczony.
 Notification.Mention.Text=Zostałeś oznaczony w następujących postach:
 Notification.SoneIsInserting.Text=Twoje Sone sone://{0} jest w tej chili wysyłane.
-Notification.SoneIsInserted.Text=Twoje sone://{0} zostało wysłane w {1,number} {1,choice,0#seconds|1#second|1<seconds}.
+Notification.SoneIsInserted.Text=Your Sone sone://{0} has been inserted in {1,number} {1,choice,0#seconds|1#second|1<seconds}.
 Notification.SoneInsertAborted.Text=Twoje Sone sone://{0} nie mogło zostać wysłane.
-# 55-61, 465
+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 bfc7301..354c0d2 100644 (file)
@@ -26,6 +26,8 @@ Navigation.Menu.Sone.Item.Rescue.Name=Восстановление
 Navigation.Menu.Sone.Item.Rescue.Tooltip=Восстановить Sone
 Navigation.Menu.Sone.Item.About.Name=О дополнении
 Navigation.Menu.Sone.Item.About.Tooltip=Информация о Sone
+Navigation.Menu.Sone.Item.Metrics.Name=Metrics
+Navigation.Menu.Sone.Item.Metrics.Tooltip=Metrics collected by Sone
 
 Page.About.Title=О дополнении - Sone
 Page.About.Page.Title=О дополнении
@@ -66,16 +68,14 @@ Page.Options.Option.ImagesPerPage.Description=Количество изобра
 Page.Options.Option.CharactersPerPost.Description=Количество символов сообщения, которые должны быть показаны до того, как оно будет обрезано и будет показана ссылка для его раскрытия (-1 для отключения). Фактическая длина обрезанного сообщения задается нижеследующей настройкой.
 Page.Options.Option.PostCutOffLength.Description=Количество символов, которые показываются, если сообщение посчитано слишком длинным (см. настройку выше).
 Page.Options.Option.RequireFullAccess.Description=Запрещать доступ к Sone любому хосту, которому не был дан полный доступ.
-Page.Options.Section.TrustOptions.Title=Настройки доверия
-Page.Options.Option.PositiveTrust.Description=Количество положительного доверия, которое вы хотите назначать другим Sone, нажимая галочку под сообщением или ответом.
-Page.Options.Option.NegativeTrust.Description=Количество доверия, которое вы хотите назначить другим Sone, нажимая красный X под сообщением или ответом. Это значение должно быть отрицательным.
-Page.Options.Option.TrustComment.Description=Комментарий, который будет установлен в web of trust для любого доверия, назначенного из Sone.
 Page.Options.Section.FcpOptions.Title=Настройка интерфейса FCP
 Page.Options.Option.FcpInterfaceActive.Description=Активировать интерфейс FCP, чтобы позволить другим дополнениям и удаленным клиентам получать доступ к вашему дополнению Sone.
 Page.Options.Option.FcpFullAccessRequired.Description=Требовать соединение FCP от разрешенных хостов(см. {link}конфигурацию узла, секция "FCP"{/link}).
 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} хотите очистить настройки дополнения во время следующего перезапуска.
@@ -276,12 +276,6 @@ Page.DeleteAlbum.Text.AlbumWillBeGone=Это удалит альбом "{title}"
 Page.DeleteAlbum.Button.Yes=Да, удалить альбом.
 Page.DeleteAlbum.Button.No=Нет, не удалять альбом.
 
-Page.Trust.Title=Доверять Sone - Sone
-
-Page.Distrust.Title=Не доверять Sone - Sone
-
-Page.Untrust.Title=Отменить доверие к Sone - Sone
-
 Page.MarkAsKnown.Title=Отметить как известный - Sone
 
 Page.Bookmark.Title=Добавить в закладки - Sone
@@ -305,7 +299,7 @@ Page.Rescue.Text.Fetching=Восстановитель Sone в данный мо
 Page.Rescue.Text.Fetched=Восстановитель Sone скачал редакцию {0} вашего Sone. Пожалуйста проверьте свои сообщения, ответы и профиль.
 Page.Rescue.Text.FetchedLast=Восстановитель Sone скачал последнюю доступную редакцию. Если ему не удалось восстановить ваш Sone, у вас кончились варианты действий.
 Page.Rescue.Text.NotFetched=Восстановитель Sone не смог скачать редакцию {0} вашего Sone. Пожалуйста, либо попытайтесь снова с редакцией {0}, либо попробуйте более старую редакцию.
-Page.Rescue.Label.NextEdition=Следующая редакция:
+Page.Rescue.Label.NextEdition=Следующая редакция
 Page.Rescue.Button.Fetch=Загрузить редакцию
 
 Page.NoPermission.Title=Неавторизованный доступ - Sone
@@ -331,6 +325,12 @@ Page.Invalid.Title=Произведено неверное действие - So
 Page.Invalid.Page.Title=Произведено неверное действие
 Page.Invalid.Text=Было произведено неверное действие, или верное действие с неверными параметрами. Пожалуйста, вернитесь на {link}главную страницу{/link} и попытайтесь снова. Если ошибка повторяется, вы, вероятно, обнаружили баг.
 
+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=Поиск
 
 View.CreateSone.Text.WotIdentityRequired=Чтобы создать Sone, вам необходима личность от дополнения {link}Web of Trust{/link}.
@@ -358,6 +358,7 @@ View.Sone.Status.Downloading=Этот Sone сейчас загружается.
 View.Sone.Status.Inserting=Этот Sone сейчас выгружается.
 
 View.SoneMenu.Link.AllAlbums=все альбомы
+View.SoneMenu.WebOfTrustLink=профиль web of trust
 
 View.Post.UnknownAuthor=(неизвестно)
 View.Post.WebOfTrustLink=профиль web of trust
@@ -377,10 +378,6 @@ View.Post.ShowLess=показать меньше
 
 View.UpdateStatus.Text.ChooseSenderIdentity=выбрать личность отправителя
 
-View.Trust.Tooltip.Trust=Доверять этому человека
-View.Trust.Tooltip.Distrust=Назначить этому человеку отрицательное доверие
-View.Trust.Tooltip.Untrust=Отменить доверие к этому человеку
-
 View.CreateAlbum.Title=Создать альбом
 View.CreateAlbum.Label.Name=Название:
 View.CreateAlbum.Label.Description=Описание:
@@ -431,9 +428,6 @@ WebInterface.DefaultText.Option.PostsPerPage=Количество сообщен
 WebInterface.DefaultText.Option.ImagesPerPage=Количество изображений, показываемых на странице
 WebInterface.DefaultText.Option.CharactersPerPost=Количество символов, которое должно быть у сообщения, чтобы оно было сокращено
 WebInterface.DefaultText.Option.PostCutOffLength=Количество символов в сокращенном варианте сообщения
-WebInterface.DefaultText.Option.PositiveTrust=Положительное доверие для назначения
-WebInterface.DefaultText.Option.NegativeTrust=Отрицательное доверие для назначения
-WebInterface.DefaultText.Option.TrustComment=Комментарий для установки в web of trust
 WebInterface.Button.Comment=Комментировать
 WebInterface.Confirmation.DeletePostButton=Да, удалить!
 WebInterface.Confirmation.DeleteReplyButton=Да, удалить!
@@ -471,4 +465,4 @@ Notification.Mention.Text=Вас упомянули в следующих соо
 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.
-# 55-61, 67, 107, 127-128, 315-317, 319-321, 465, 471-473
+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 5ea5f64..26d74e4 100644 (file)
@@ -254,6 +254,10 @@ textarea {
        text-align: right;
 }
 
+#sone td.numeric {
+       text-align: right;
+}
+
 #sone .post {
        padding: 1ex 0px;
        border-bottom: solid 1px #ccc;
@@ -387,7 +391,7 @@ textarea {
        color: #666;
 }
 
-#sone .post .delete, #sone .post .likes, #sone .post .like, #sone .post .unlike, #sone .post .trust, #sone .post .distrust, #sone .post .untrust {
+#sone .post .delete, #sone .post .likes, #sone .post .like, #sone .post .unlike, #sone .post .wot-link {
        display: inline;
        font: inherit;
        margin: 0px;
@@ -397,11 +401,11 @@ textarea {
        display: none;
 }
 
-#sone .post .like.hidden, #sone .post .unlike.hidden, #sone .post .trust.hidden, #sone .post .distrust.hidden, #sone .post .untrust.hidden, #sone .post .bookmark.hidden, #sone .post .unbookmark.hidden {
+#sone .post .like.hidden, #sone .post .unlike.hidden, #sone .post .bookmark.hidden, #sone .post .unbookmark.hidden {
        display: none;
 }
 
-#sone .post .delete button, #sone .post .like button, #sone .post .unlike button, #sone .post .trust button, #sone .post .distrust button, #sone .post .untrust button, #sone .post .bookmark button, #sone .post .unbookmark button {
+#sone .post .delete button, #sone .post .like button, #sone .post .unlike button, #sone .post .bookmark button, #sone .post .unbookmark button {
        border: 0px;
        background: none;
        padding: 0px;
@@ -415,19 +419,7 @@ textarea {
        font-weight: bold;
 }
 
-#sone .post .trust button {
-       color: rgb(0, 128, 0);
-}
-
-#sone .post .distrust button {
-       color: rgb(255, 0, 0);
-}
-
-#sone .post .untrust button {
-       color: rgb(64, 64, 64);
-}
-
-#sone .post .delete button:hover, #sone .post .like button:hover, #sone .post .unlike button:hover, #sone .post .trust button:hover, #sone .post .distrust button:hover, #sone .post .untrust button:hover, #sone .post .bookmark button:hover, #sone .post .unbookmark button:hover {
+#sone .post .delete button:hover, #sone .post .like button:hover, #sone .post .unlike button:hover, #sone .post .bookmark button:hover, #sone .post .unbookmark button:hover {
        border: 0px;
        background: none;
        padding: 0px;
@@ -933,3 +925,7 @@ textarea {
 #sone form#options li {
        list-style-type: none;
 }
+
+#sone table thead tr {
+       font-weight: bold;
+}
diff --git a/src/main/resources/static/javascript/jquery-1.4.2.js b/src/main/resources/static/javascript/jquery-1.4.2.js
deleted file mode 100644 (file)
index fff6776..0000000
+++ /dev/null
@@ -1,6240 +0,0 @@
-/*!
- * jQuery JavaScript Library v1.4.2
- * http://jquery.com/
- *
- * Copyright 2010, John Resig
- * Dual licensed under the MIT or GPL Version 2 licenses.
- * http://jquery.org/license
- *
- * Includes Sizzle.js
- * http://sizzlejs.com/
- * Copyright 2010, The Dojo Foundation
- * Released under the MIT, BSD, and GPL Licenses.
- *
- * Date: Sat Feb 13 22:33:48 2010 -0500
- */
-(function( window, undefined ) {
-
-// Define a local copy of jQuery
-var jQuery = function( selector, context ) {
-               // The jQuery object is actually just the init constructor 'enhanced'
-               return new jQuery.fn.init( selector, context );
-       },
-
-       // Map over jQuery in case of overwrite
-       _jQuery = window.jQuery,
-
-       // Map over the $ in case of overwrite
-       _$ = window.$,
-
-       // Use the correct document accordingly with window argument (sandbox)
-       document = window.document,
-
-       // A central reference to the root jQuery(document)
-       rootjQuery,
-
-       // A simple way to check for HTML strings or ID strings
-       // (both of which we optimize for)
-       quickExpr = /^[^<]*(<[\w\W]+>)[^>]*$|^#([\w-]+)$/,
-
-       // Is it a simple selector
-       isSimple = /^.[^:#\[\.,]*$/,
-
-       // Check if a string has a non-whitespace character in it
-       rnotwhite = /\S/,
-
-       // Used for trimming whitespace
-       rtrim = /^(\s|\u00A0)+|(\s|\u00A0)+$/g,
-
-       // Match a standalone tag
-       rsingleTag = /^<(\w+)\s*\/?>(?:<\/\1>)?$/,
-
-       // Keep a UserAgent string for use with jQuery.browser
-       userAgent = navigator.userAgent,
-
-       // For matching the engine and version of the browser
-       browserMatch,
-       
-       // Has the ready events already been bound?
-       readyBound = false,
-       
-       // The functions to execute on DOM ready
-       readyList = [],
-
-       // The ready event handler
-       DOMContentLoaded,
-
-       // Save a reference to some core methods
-       toString = Object.prototype.toString,
-       hasOwnProperty = Object.prototype.hasOwnProperty,
-       push = Array.prototype.push,
-       slice = Array.prototype.slice,
-       indexOf = Array.prototype.indexOf;
-
-jQuery.fn = jQuery.prototype = {
-       init: function( selector, context ) {
-               var match, elem, ret, doc;
-
-               // Handle $(""), $(null), or $(undefined)
-               if ( !selector ) {
-                       return this;
-               }
-
-               // Handle $(DOMElement)
-               if ( selector.nodeType ) {
-                       this.context = this[0] = selector;
-                       this.length = 1;
-                       return this;
-               }
-               
-               // The body element only exists once, optimize finding it
-               if ( selector === "body" && !context ) {
-                       this.context = document;
-                       this[0] = document.body;
-                       this.selector = "body";
-                       this.length = 1;
-                       return this;
-               }
-
-               // Handle HTML strings
-               if ( typeof selector === "string" ) {
-                       // Are we dealing with HTML string or an ID?
-                       match = quickExpr.exec( selector );
-
-                       // Verify a match, and that no context was specified for #id
-                       if ( match && (match[1] || !context) ) {
-
-                               // HANDLE: $(html) -> $(array)
-                               if ( match[1] ) {
-                                       doc = (context ? context.ownerDocument || context : document);
-
-                                       // If a single string is passed in and it's a single tag
-                                       // just do a createElement and skip the rest
-                                       ret = rsingleTag.exec( selector );
-
-                                       if ( ret ) {
-                                               if ( jQuery.isPlainObject( context ) ) {
-                                                       selector = [ document.createElement( ret[1] ) ];
-                                                       jQuery.fn.attr.call( selector, context, true );
-
-                                               } else {
-                                                       selector = [ doc.createElement( ret[1] ) ];
-                                               }
-
-                                       } else {
-                                               ret = buildFragment( [ match[1] ], [ doc ] );
-                                               selector = (ret.cacheable ? ret.fragment.cloneNode(true) : ret.fragment).childNodes;
-                                       }
-                                       
-                                       return jQuery.merge( this, selector );
-                                       
-                               // HANDLE: $("#id")
-                               } else {
-                                       elem = document.getElementById( match[2] );
-
-                                       if ( elem ) {
-                                               // Handle the case where IE and Opera return items
-                                               // by name instead of ID
-                                               if ( elem.id !== match[2] ) {
-                                                       return rootjQuery.find( selector );
-                                               }
-
-                                               // Otherwise, we inject the element directly into the jQuery object
-                                               this.length = 1;
-                                               this[0] = elem;
-                                       }
-
-                                       this.context = document;
-                                       this.selector = selector;
-                                       return this;
-                               }
-
-                       // HANDLE: $("TAG")
-                       } else if ( !context && /^\w+$/.test( selector ) ) {
-                               this.selector = selector;
-                               this.context = document;
-                               selector = document.getElementsByTagName( selector );
-                               return jQuery.merge( this, selector );
-
-                       // HANDLE: $(expr, $(...))
-                       } else if ( !context || context.jquery ) {
-                               return (context || rootjQuery).find( selector );
-
-                       // HANDLE: $(expr, context)
-                       // (which is just equivalent to: $(context).find(expr)
-                       } else {
-                               return jQuery( context ).find( selector );
-                       }
-
-               // HANDLE: $(function)
-               // Shortcut for document ready
-               } else if ( jQuery.isFunction( selector ) ) {
-                       return rootjQuery.ready( selector );
-               }
-
-               if (selector.selector !== undefined) {
-                       this.selector = selector.selector;
-                       this.context = selector.context;
-               }
-
-               return jQuery.makeArray( selector, this );
-       },
-
-       // Start with an empty selector
-       selector: "",
-
-       // The current version of jQuery being used
-       jquery: "1.4.2",
-
-       // The default length of a jQuery object is 0
-       length: 0,
-
-       // The number of elements contained in the matched element set
-       size: function() {
-               return this.length;
-       },
-
-       toArray: function() {
-               return slice.call( this, 0 );
-       },
-
-       // Get the Nth element in the matched element set OR
-       // Get the whole matched element set as a clean array
-       get: function( num ) {
-               return num == null ?
-
-                       // Return a 'clean' array
-                       this.toArray() :
-
-                       // Return just the object
-                       ( num < 0 ? this.slice(num)[ 0 ] : this[ num ] );
-       },
-
-       // Take an array of elements and push it onto the stack
-       // (returning the new matched element set)
-       pushStack: function( elems, name, selector ) {
-               // Build a new jQuery matched element set
-               var ret = jQuery();
-
-               if ( jQuery.isArray( elems ) ) {
-                       push.apply( ret, elems );
-               
-               } else {
-                       jQuery.merge( ret, elems );
-               }
-
-               // Add the old object onto the stack (as a reference)
-               ret.prevObject = this;
-
-               ret.context = this.context;
-
-               if ( name === "find" ) {
-                       ret.selector = this.selector + (this.selector ? " " : "") + selector;
-               } else if ( name ) {
-                       ret.selector = this.selector + "." + name + "(" + selector + ")";
-               }
-
-               // Return the newly-formed element set
-               return ret;
-       },
-
-       // Execute a callback for every element in the matched set.
-       // (You can seed the arguments with an array of args, but this is
-       // only used internally.)
-       each: function( callback, args ) {
-               return jQuery.each( this, callback, args );
-       },
-       
-       ready: function( fn ) {
-               // Attach the listeners
-               jQuery.bindReady();
-
-               // If the DOM is already ready
-               if ( jQuery.isReady ) {
-                       // Execute the function immediately
-                       fn.call( document, jQuery );
-
-               // Otherwise, remember the function for later
-               } else if ( readyList ) {
-                       // Add the function to the wait list
-                       readyList.push( fn );
-               }
-
-               return this;
-       },
-       
-       eq: function( i ) {
-               return i === -1 ?
-                       this.slice( i ) :
-                       this.slice( i, +i + 1 );
-       },
-
-       first: function() {
-               return this.eq( 0 );
-       },
-
-       last: function() {
-               return this.eq( -1 );
-       },
-
-       slice: function() {
-               return this.pushStack( slice.apply( this, arguments ),
-                       "slice", slice.call(arguments).join(",") );
-       },
-
-       map: function( callback ) {
-               return this.pushStack( jQuery.map(this, function( elem, i ) {
-                       return callback.call( elem, i, elem );
-               }));
-       },
-       
-       end: function() {
-               return this.prevObject || jQuery(null);
-       },
-
-       // For internal use only.
-       // Behaves like an Array's method, not like a jQuery method.
-       push: push,
-       sort: [].sort,
-       splice: [].splice
-};
-
-// Give the init function the jQuery prototype for later instantiation
-jQuery.fn.init.prototype = jQuery.fn;
-
-jQuery.extend = jQuery.fn.extend = function() {
-       // copy reference to target object
-       var target = arguments[0] || {}, i = 1, length = arguments.length, deep = false, options, name, src, copy;
-
-       // Handle a deep copy situation
-       if ( typeof target === "boolean" ) {
-               deep = target;
-               target = arguments[1] || {};
-               // skip the boolean and the target
-               i = 2;
-       }
-
-       // Handle case when target is a string or something (possible in deep copy)
-       if ( typeof target !== "object" && !jQuery.isFunction(target) ) {
-               target = {};
-       }
-
-       // extend jQuery itself if only one argument is passed
-       if ( length === i ) {
-               target = this;
-               --i;
-       }
-
-       for ( ; i < length; i++ ) {
-               // Only deal with non-null/undefined values
-               if ( (options = arguments[ i ]) != null ) {
-                       // Extend the base object
-                       for ( name in options ) {
-                               src = target[ name ];
-                               copy = options[ name ];
-
-                               // Prevent never-ending loop
-                               if ( target === copy ) {
-                                       continue;
-                               }
-
-                               // Recurse if we're merging object literal values or arrays
-                               if ( deep && copy && ( jQuery.isPlainObject(copy) || jQuery.isArray(copy) ) ) {
-                                       var clone = src && ( jQuery.isPlainObject(src) || jQuery.isArray(src) ) ? src
-                                               : jQuery.isArray(copy) ? [] : {};
-
-                                       // Never move original objects, clone them
-                                       target[ name ] = jQuery.extend( deep, clone, copy );
-
-                               // Don't bring in undefined values
-                               } else if ( copy !== undefined ) {
-                                       target[ name ] = copy;
-                               }
-                       }
-               }
-       }
-
-       // Return the modified object
-       return target;
-};
-
-jQuery.extend({
-       noConflict: function( deep ) {
-               window.$ = _$;
-
-               if ( deep ) {
-                       window.jQuery = _jQuery;
-               }
-
-               return jQuery;
-       },
-       
-       // Is the DOM ready to be used? Set to true once it occurs.
-       isReady: false,
-       
-       // Handle when the DOM is ready
-       ready: function() {
-               // Make sure that the DOM is not already loaded
-               if ( !jQuery.isReady ) {
-                       // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443).
-                       if ( !document.body ) {
-                               return setTimeout( jQuery.ready, 13 );
-                       }
-
-                       // Remember that the DOM is ready
-                       jQuery.isReady = true;
-
-                       // If there are functions bound, to execute
-                       if ( readyList ) {
-                               // Execute all of them
-                               var fn, i = 0;
-                               while ( (fn = readyList[ i++ ]) ) {
-                                       fn.call( document, jQuery );
-                               }
-
-                               // Reset the list of functions
-                               readyList = null;
-                       }
-
-                       // Trigger any bound ready events
-                       if ( jQuery.fn.triggerHandler ) {
-                               jQuery( document ).triggerHandler( "ready" );
-                       }
-               }
-       },
-       
-       bindReady: function() {
-               if ( readyBound ) {
-                       return;
-               }
-
-               readyBound = true;
-
-               // Catch cases where $(document).ready() is called after the
-               // browser event has already occurred.
-               if ( document.readyState === "complete" ) {
-                       return jQuery.ready();
-               }
-
-               // Mozilla, Opera and webkit nightlies currently support this event
-               if ( document.addEventListener ) {
-                       // Use the handy event callback
-                       document.addEventListener( "DOMContentLoaded", DOMContentLoaded, false );
-                       
-                       // A fallback to window.onload, that will always work
-                       window.addEventListener( "load", jQuery.ready, false );
-
-               // If IE event model is used
-               } else if ( document.attachEvent ) {
-                       // ensure firing before onload,
-                       // maybe late but safe also for iframes
-                       document.attachEvent("onreadystatechange", DOMContentLoaded);
-                       
-                       // A fallback to window.onload, that will always work
-                       window.attachEvent( "onload", jQuery.ready );
-
-                       // If IE and not a frame
-                       // continually check to see if the document is ready
-                       var toplevel = false;
-
-                       try {
-                               toplevel = window.frameElement == null;
-                       } catch(e) {}
-
-                       if ( document.documentElement.doScroll && toplevel ) {
-                               doScrollCheck();
-                       }
-               }
-       },
-
-       // See test/unit/core.js for details concerning isFunction.
-       // Since version 1.3, DOM methods and functions like alert
-       // aren't supported. They return false on IE (#2968).
-       isFunction: function( obj ) {
-               return toString.call(obj) === "[object Function]";
-       },
-
-       isArray: function( obj ) {
-               return toString.call(obj) === "[object Array]";
-       },
-
-       isPlainObject: function( obj ) {
-               // Must be an Object.
-               // Because of IE, we also have to check the presence of the constructor property.
-               // Make sure that DOM nodes and window objects don't pass through, as well
-               if ( !obj || toString.call(obj) !== "[object Object]" || obj.nodeType || obj.setInterval ) {
-                       return false;
-               }
-               
-               // Not own constructor property must be Object
-               if ( obj.constructor
-                       && !hasOwnProperty.call(obj, "constructor")
-                       && !hasOwnProperty.call(obj.constructor.prototype, "isPrototypeOf") ) {
-                       return false;
-               }
-               
-               // Own properties are enumerated firstly, so to speed up,
-               // if last one is own, then all properties are own.
-       
-               var key;
-               for ( key in obj ) {}
-               
-               return key === undefined || hasOwnProperty.call( obj, key );
-       },
-
-       isEmptyObject: function( obj ) {
-               for ( var name in obj ) {
-                       return false;
-               }
-               return true;
-       },
-       
-       error: function( msg ) {
-               throw msg;
-       },
-       
-       parseJSON: function( data ) {
-               if ( typeof data !== "string" || !data ) {
-                       return null;
-               }
-
-               // Make sure leading/trailing whitespace is removed (IE can't handle it)
-               data = jQuery.trim( data );
-               
-               // Make sure the incoming data is actual JSON
-               // Logic borrowed from http://json.org/json2.js
-               if ( /^[\],:{}\s]*$/.test(data.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, "@")
-                       .replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, "]")
-                       .replace(/(?:^|:|,)(?:\s*\[)+/g, "")) ) {
-
-                       // Try to use the native JSON parser first
-                       return window.JSON && window.JSON.parse ?
-                               window.JSON.parse( data ) :
-                               (new Function("return " + data))();
-
-               } else {
-                       jQuery.error( "Invalid JSON: " + data );
-               }
-       },
-
-       noop: function() {},
-
-       // Evalulates a script in a global context
-       globalEval: function( data ) {
-               if ( data && rnotwhite.test(data) ) {
-                       // Inspired by code by Andrea Giammarchi
-                       // http://webreflection.blogspot.com/2007/08/global-scope-evaluation-and-dom.html
-                       var head = document.getElementsByTagName("head")[0] || document.documentElement,
-                               script = document.createElement("script");
-
-                       script.type = "text/javascript";
-
-                       if ( jQuery.support.scriptEval ) {
-                               script.appendChild( document.createTextNode( data ) );
-                       } else {
-                               script.text = data;
-                       }
-
-                       // Use insertBefore instead of appendChild to circumvent an IE6 bug.
-                       // This arises when a base node is used (#2709).
-                       head.insertBefore( script, head.firstChild );
-                       head.removeChild( script );
-               }
-       },
-
-       nodeName: function( elem, name ) {
-               return elem.nodeName && elem.nodeName.toUpperCase() === name.toUpperCase();
-       },
-
-       // args is for internal usage only
-       each: function( object, callback, args ) {
-               var name, i = 0,
-                       length = object.length,
-                       isObj = length === undefined || jQuery.isFunction(object);
-
-               if ( args ) {
-                       if ( isObj ) {
-                               for ( name in object ) {
-                                       if ( callback.apply( object[ name ], args ) === false ) {
-                                               break;
-                                       }
-                               }
-                       } else {
-                               for ( ; i < length; ) {
-                                       if ( callback.apply( object[ i++ ], args ) === false ) {
-                                               break;
-                                       }
-                               }
-                       }
-
-               // A special, fast, case for the most common use of each
-               } else {
-                       if ( isObj ) {
-                               for ( name in object ) {
-                                       if ( callback.call( object[ name ], name, object[ name ] ) === false ) {
-                                               break;
-                                       }
-                               }
-                       } else {
-                               for ( var value = object[0];
-                                       i < length && callback.call( value, i, value ) !== false; value = object[++i] ) {}
-                       }
-               }
-
-               return object;
-       },
-
-       trim: function( text ) {
-               return (text || "").replace( rtrim, "" );
-       },
-
-       // results is for internal usage only
-       makeArray: function( array, results ) {
-               var ret = results || [];
-
-               if ( array != null ) {
-                       // The window, strings (and functions) also have 'length'
-                       // The extra typeof function check is to prevent crashes
-                       // in Safari 2 (See: #3039)
-                       if ( array.length == null || typeof array === "string" || jQuery.isFunction(array) || (typeof array !== "function" && array.setInterval) ) {
-                               push.call( ret, array );
-                       } else {
-                               jQuery.merge( ret, array );
-                       }
-               }
-
-               return ret;
-       },
-
-       inArray: function( elem, array ) {
-               if ( array.indexOf ) {
-                       return array.indexOf( elem );
-               }
-
-               for ( var i = 0, length = array.length; i < length; i++ ) {
-                       if ( array[ i ] === elem ) {
-                               return i;
-                       }
-               }
-
-               return -1;
-       },
-
-       merge: function( first, second ) {
-               var i = first.length, j = 0;
-
-               if ( typeof second.length === "number" ) {
-                       for ( var l = second.length; j < l; j++ ) {
-                               first[ i++ ] = second[ j ];
-                       }
-               
-               } else {
-                       while ( second[j] !== undefined ) {
-                               first[ i++ ] = second[ j++ ];
-                       }
-               }
-
-               first.length = i;
-
-               return first;
-       },
-
-       grep: function( elems, callback, inv ) {
-               var ret = [];
-
-               // Go through the array, only saving the items
-               // that pass the validator function
-               for ( var i = 0, length = elems.length; i < length; i++ ) {
-                       if ( !inv !== !callback( elems[ i ], i ) ) {
-                               ret.push( elems[ i ] );
-                       }
-               }
-
-               return ret;
-       },
-
-       // arg is for internal usage only
-       map: function( elems, callback, arg ) {
-               var ret = [], value;
-
-               // Go through the array, translating each of the items to their
-               // new value (or values).
-               for ( var i = 0, length = elems.length; i < length; i++ ) {
-                       value = callback( elems[ i ], i, arg );
-
-                       if ( value != null ) {
-                               ret[ ret.length ] = value;
-                       }
-               }
-
-               return ret.concat.apply( [], ret );
-       },
-
-       // A global GUID counter for objects
-       guid: 1,
-
-       proxy: function( fn, proxy, thisObject ) {
-               if ( arguments.length === 2 ) {
-                       if ( typeof proxy === "string" ) {
-                               thisObject = fn;
-                               fn = thisObject[ proxy ];
-                               proxy = undefined;
-
-                       } else if ( proxy && !jQuery.isFunction( proxy ) ) {
-                               thisObject = proxy;
-                               proxy = undefined;
-                       }
-               }
-
-               if ( !proxy && fn ) {
-                       proxy = function() {
-                               return fn.apply( thisObject || this, arguments );
-                       };
-               }
-
-               // Set the guid of unique handler to the same of original handler, so it can be removed
-               if ( fn ) {
-                       proxy.guid = fn.guid = fn.guid || proxy.guid || jQuery.guid++;
-               }
-
-               // So proxy can be declared as an argument
-               return proxy;
-       },
-
-       // Use of jQuery.browser is frowned upon.
-       // More details: http://docs.jquery.com/Utilities/jQuery.browser
-       uaMatch: function( ua ) {
-               ua = ua.toLowerCase();
-
-               var match = /(webkit)[ \/]([\w.]+)/.exec( ua ) ||
-                       /(opera)(?:.*version)?[ \/]([\w.]+)/.exec( ua ) ||
-                       /(msie) ([\w.]+)/.exec( ua ) ||
-                       !/compatible/.test( ua ) && /(mozilla)(?:.*? rv:([\w.]+))?/.exec( ua ) ||
-                       [];
-
-               return { browser: match[1] || "", version: match[2] || "0" };
-       },
-
-       browser: {}
-});
-
-browserMatch = jQuery.uaMatch( userAgent );
-if ( browserMatch.browser ) {
-       jQuery.browser[ browserMatch.browser ] = true;
-       jQuery.browser.version = browserMatch.version;
-}
-
-// Deprecated, use jQuery.browser.webkit instead
-if ( jQuery.browser.webkit ) {
-       jQuery.browser.safari = true;
-}
-
-if ( indexOf ) {
-       jQuery.inArray = function( elem, array ) {
-               return indexOf.call( array, elem );
-       };
-}
-
-// All jQuery objects should point back to these
-rootjQuery = jQuery(document);
-
-// Cleanup functions for the document ready method
-if ( document.addEventListener ) {
-       DOMContentLoaded = function() {
-               document.removeEventListener( "DOMContentLoaded", DOMContentLoaded, false );
-               jQuery.ready();
-       };
-
-} else if ( document.attachEvent ) {
-       DOMContentLoaded = function() {
-               // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443).
-               if ( document.readyState === "complete" ) {
-                       document.detachEvent( "onreadystatechange", DOMContentLoaded );
-                       jQuery.ready();
-               }
-       };
-}
-
-// The DOM ready check for Internet Explorer
-function doScrollCheck() {
-       if ( jQuery.isReady ) {
-               return;
-       }
-
-       try {
-               // If IE is used, use the trick by Diego Perini
-               // http://javascript.nwbox.com/IEContentLoaded/
-               document.documentElement.doScroll("left");
-       } catch( error ) {
-               setTimeout( doScrollCheck, 1 );
-               return;
-       }
-
-       // and execute any waiting functions
-       jQuery.ready();
-}
-
-function evalScript( i, elem ) {
-       if ( elem.src ) {
-               jQuery.ajax({
-                       url: elem.src,
-                       async: false,
-                       dataType: "script"
-               });
-       } else {
-               jQuery.globalEval( elem.text || elem.textContent || elem.innerHTML || "" );
-       }
-
-       if ( elem.parentNode ) {
-               elem.parentNode.removeChild( elem );
-       }
-}
-
-// Mutifunctional method to get and set values to a collection
-// The value/s can be optionally by executed if its a function
-function access( elems, key, value, exec, fn, pass ) {
-       var length = elems.length;
-       
-       // Setting many attributes
-       if ( typeof key === "object" ) {
-               for ( var k in key ) {
-                       access( elems, k, key[k], exec, fn, value );
-               }
-               return elems;
-       }
-       
-       // Setting one attribute
-       if ( value !== undefined ) {
-               // Optionally, function values get executed if exec is true
-               exec = !pass && exec && jQuery.isFunction(value);
-               
-               for ( var i = 0; i < length; i++ ) {
-                       fn( elems[i], key, exec ? value.call( elems[i], i, fn( elems[i], key ) ) : value, pass );
-               }
-               
-               return elems;
-       }
-       
-       // Getting an attribute
-       return length ? fn( elems[0], key ) : undefined;
-}
-
-function now() {
-       return (new Date).getTime();
-}
-(function() {
-
-       jQuery.support = {};
-
-       var root = document.documentElement,
-               script = document.createElement("script"),
-               div = document.createElement("div"),
-               id = "script" + now();
-
-       div.style.display = "none";
-       div.innerHTML = "   <link/><table></table><a href='/a' style='color:red;float:left;opacity:.55;'>a</a><input type='checkbox'/>";
-
-       var all = div.getElementsByTagName("*"),
-               a = div.getElementsByTagName("a")[0];
-
-       // Can't get basic test support
-       if ( !all || !all.length || !a ) {
-               return;
-       }
-
-       jQuery.support = {
-               // IE strips leading whitespace when .innerHTML is used
-               leadingWhitespace: div.firstChild.nodeType === 3,
-
-               // Make sure that tbody elements aren't automatically inserted
-               // IE will insert them into empty tables
-               tbody: !div.getElementsByTagName("tbody").length,
-
-               // Make sure that link elements get serialized correctly by innerHTML
-               // This requires a wrapper element in IE
-               htmlSerialize: !!div.getElementsByTagName("link").length,
-
-               // Get the style information from getAttribute
-               // (IE uses .cssText insted)
-               style: /red/.test( a.getAttribute("style") ),
-
-               // Make sure that URLs aren't manipulated
-               // (IE normalizes it by default)
-               hrefNormalized: a.getAttribute("href") === "/a",
-
-               // Make sure that element opacity exists
-               // (IE uses filter instead)
-               // Use a regex to work around a WebKit issue. See #5145
-               opacity: /^0.55$/.test( a.style.opacity ),
-
-               // Verify style float existence
-               // (IE uses styleFloat instead of cssFloat)
-               cssFloat: !!a.style.cssFloat,
-
-               // Make sure that if no value is specified for a checkbox
-               // that it defaults to "on".
-               // (WebKit defaults to "" instead)
-               checkOn: div.getElementsByTagName("input")[0].value === "on",
-
-               // Make sure that a selected-by-default option has a working selected property.
-               // (WebKit defaults to false instead of true, IE too, if it's in an optgroup)
-               optSelected: document.createElement("select").appendChild( document.createElement("option") ).selected,
-
-               parentNode: div.removeChild( div.appendChild( document.createElement("div") ) ).parentNode === null,
-
-               // Will be defined later
-               deleteExpando: true,
-               checkClone: false,
-               scriptEval: false,
-               noCloneEvent: true,
-               boxModel: null
-       };
-
-       script.type = "text/javascript";
-       try {
-               script.appendChild( document.createTextNode( "window." + id + "=1;" ) );
-       } catch(e) {}
-
-       root.insertBefore( script, root.firstChild );
-
-       // Make sure that the execution of code works by injecting a script
-       // tag with appendChild/createTextNode
-       // (IE doesn't support this, fails, and uses .text instead)
-       if ( window[ id ] ) {
-               jQuery.support.scriptEval = true;
-               delete window[ id ];
-       }
-
-       // Test to see if it's possible to delete an expando from an element
-       // Fails in Internet Explorer
-       try {
-               delete script.test;
-       
-       } catch(e) {
-               jQuery.support.deleteExpando = false;
-       }
-
-       root.removeChild( script );
-
-       if ( div.attachEvent && div.fireEvent ) {
-               div.attachEvent("onclick", function click() {
-                       // Cloning a node shouldn't copy over any
-                       // bound event handlers (IE does this)
-                       jQuery.support.noCloneEvent = false;
-                       div.detachEvent("onclick", click);
-               });
-               div.cloneNode(true).fireEvent("onclick");
-       }
-
-       div = document.createElement("div");
-       div.innerHTML = "<input type='radio' name='radiotest' checked='checked'/>";
-
-       var fragment = document.createDocumentFragment();
-       fragment.appendChild( div.firstChild );
-
-       // WebKit doesn't clone checked state correctly in fragments
-       jQuery.support.checkClone = fragment.cloneNode(true).cloneNode(true).lastChild.checked;
-
-       // Figure out if the W3C box model works as expected
-       // document.body must exist before we can do this
-       jQuery(function() {
-               var div = document.createElement("div");
-               div.style.width = div.style.paddingLeft = "1px";
-
-               document.body.appendChild( div );
-               jQuery.boxModel = jQuery.support.boxModel = div.offsetWidth === 2;
-               document.body.removeChild( div ).style.display = 'none';
-
-               div = null;
-       });
-
-       // Technique from Juriy Zaytsev
-       // http://thinkweb2.com/projects/prototype/detecting-event-support-without-browser-sniffing/
-       var eventSupported = function( eventName ) { 
-               var el = document.createElement("div"); 
-               eventName = "on" + eventName; 
-
-               var isSupported = (eventName in el); 
-               if ( !isSupported ) { 
-                       el.setAttribute(eventName, "return;"); 
-                       isSupported = typeof el[eventName] === "function"; 
-               } 
-               el = null; 
-
-               return isSupported; 
-       };
-       
-       jQuery.support.submitBubbles = eventSupported("submit");
-       jQuery.support.changeBubbles = eventSupported("change");
-
-       // release memory in IE
-       root = script = div = all = a = null;
-})();
-
-jQuery.props = {
-       "for": "htmlFor",
-       "class": "className",
-       readonly: "readOnly",
-       maxlength: "maxLength",
-       cellspacing: "cellSpacing",
-       rowspan: "rowSpan",
-       colspan: "colSpan",
-       tabindex: "tabIndex",
-       usemap: "useMap",
-       frameborder: "frameBorder"
-};
-var expando = "jQuery" + now(), uuid = 0, windowData = {};
-
-jQuery.extend({
-       cache: {},
-       
-       expando:expando,
-
-       // The following elements throw uncatchable exceptions if you
-       // attempt to add expando properties to them.
-       noData: {
-               "embed": true,
-               "object": true,
-               "applet": true
-       },
-
-       data: function( elem, name, data ) {
-               if ( elem.nodeName && jQuery.noData[elem.nodeName.toLowerCase()] ) {
-                       return;
-               }
-
-               elem = elem == window ?
-                       windowData :
-                       elem;
-
-               var id = elem[ expando ], cache = jQuery.cache, thisCache;
-
-               if ( !id && typeof name === "string" && data === undefined ) {
-                       return null;
-               }
-
-               // Compute a unique ID for the element
-               if ( !id ) { 
-                       id = ++uuid;
-               }
-
-               // Avoid generating a new cache unless none exists and we
-               // want to manipulate it.
-               if ( typeof name === "object" ) {
-                       elem[ expando ] = id;
-                       thisCache = cache[ id ] = jQuery.extend(true, {}, name);
-
-               } else if ( !cache[ id ] ) {
-                       elem[ expando ] = id;
-                       cache[ id ] = {};
-               }
-
-               thisCache = cache[ id ];
-
-               // Prevent overriding the named cache with undefined values
-               if ( data !== undefined ) {
-                       thisCache[ name ] = data;
-               }
-
-               return typeof name === "string" ? thisCache[ name ] : thisCache;
-       },
-
-       removeData: function( elem, name ) {
-               if ( elem.nodeName && jQuery.noData[elem.nodeName.toLowerCase()] ) {
-                       return;
-               }
-
-               elem = elem == window ?
-                       windowData :
-                       elem;
-
-               var id = elem[ expando ], cache = jQuery.cache, thisCache = cache[ id ];
-
-               // If we want to remove a specific section of the element's data
-               if ( name ) {
-                       if ( thisCache ) {
-                               // Remove the section of cache data
-                               delete thisCache[ name ];
-
-                               // If we've removed all the data, remove the element's cache
-                               if ( jQuery.isEmptyObject(thisCache) ) {
-                                       jQuery.removeData( elem );
-                               }
-                       }
-
-               // Otherwise, we want to remove all of the element's data
-               } else {
-                       if ( jQuery.support.deleteExpando ) {
-                               delete elem[ jQuery.expando ];
-
-                       } else if ( elem.removeAttribute ) {
-                               elem.removeAttribute( jQuery.expando );
-                       }
-
-                       // Completely remove the data cache
-                       delete cache[ id ];
-               }
-       }
-});
-
-jQuery.fn.extend({
-       data: function( key, value ) {
-               if ( typeof key === "undefined" && this.length ) {
-                       return jQuery.data( this[0] );
-
-               } else if ( typeof key === "object" ) {
-                       return this.each(function() {
-                               jQuery.data( this, key );
-                       });
-               }
-
-               var parts = key.split(".");
-               parts[1] = parts[1] ? "." + parts[1] : "";
-
-               if ( value === undefined ) {
-                       var data = this.triggerHandler("getData" + parts[1] + "!", [parts[0]]);
-
-                       if ( data === undefined && this.length ) {
-                               data = jQuery.data( this[0], key );
-                       }
-                       return data === undefined && parts[1] ?
-                               this.data( parts[0] ) :
-                               data;
-               } else {
-                       return this.trigger("setData" + parts[1] + "!", [parts[0], value]).each(function() {
-                               jQuery.data( this, key, value );
-                       });
-               }
-       },
-
-       removeData: function( key ) {
-               return this.each(function() {
-                       jQuery.removeData( this, key );
-               });
-       }
-});
-jQuery.extend({
-       queue: function( elem, type, data ) {
-               if ( !elem ) {
-                       return;
-               }
-
-               type = (type || "fx") + "queue";
-               var q = jQuery.data( elem, type );
-
-               // Speed up dequeue by getting out quickly if this is just a lookup
-               if ( !data ) {
-                       return q || [];
-               }
-
-               if ( !q || jQuery.isArray(data) ) {
-                       q = jQuery.data( elem, type, jQuery.makeArray(data) );
-
-               } else {
-                       q.push( data );
-               }
-
-               return q;
-       },
-
-       dequeue: function( elem, type ) {
-               type = type || "fx";
-
-               var queue = jQuery.queue( elem, type ), fn = queue.shift();
-
-               // If the fx queue is dequeued, always remove the progress sentinel
-               if ( fn === "inprogress" ) {
-                       fn = queue.shift();
-               }
-
-               if ( fn ) {
-                       // Add a progress sentinel to prevent the fx queue from being
-                       // automatically dequeued
-                       if ( type === "fx" ) {
-                               queue.unshift("inprogress");
-                       }
-
-                       fn.call(elem, function() {
-                               jQuery.dequeue(elem, type);
-                       });
-               }
-       }
-});
-
-jQuery.fn.extend({
-       queue: function( type, data ) {
-               if ( typeof type !== "string" ) {
-                       data = type;
-                       type = "fx";
-               }
-
-               if ( data === undefined ) {
-                       return jQuery.queue( this[0], type );
-               }
-               return this.each(function( i, elem ) {
-                       var queue = jQuery.queue( this, type, data );
-
-                       if ( type === "fx" && queue[0] !== "inprogress" ) {
-                               jQuery.dequeue( this, type );
-                       }
-               });
-       },
-       dequeue: function( type ) {
-               return this.each(function() {
-                       jQuery.dequeue( this, type );
-               });
-       },
-
-       // Based off of the plugin by Clint Helfers, with permission.
-       // http://blindsignals.com/index.php/2009/07/jquery-delay/
-       delay: function( time, type ) {
-               time = jQuery.fx ? jQuery.fx.speeds[time] || time : time;
-               type = type || "fx";
-
-               return this.queue( type, function() {
-                       var elem = this;
-                       setTimeout(function() {
-                               jQuery.dequeue( elem, type );
-                       }, time );
-               });
-       },
-
-       clearQueue: function( type ) {
-               return this.queue( type || "fx", [] );
-       }
-});
-var rclass = /[\n\t]/g,
-       rspace = /\s+/,
-       rreturn = /\r/g,
-       rspecialurl = /href|src|style/,
-       rtype = /(button|input)/i,
-       rfocusable = /(button|input|object|select|textarea)/i,
-       rclickable = /^(a|area)$/i,
-       rradiocheck = /radio|checkbox/;
-
-jQuery.fn.extend({
-       attr: function( name, value ) {
-               return access( this, name, value, true, jQuery.attr );
-       },
-
-       removeAttr: function( name, fn ) {
-               return this.each(function(){
-                       jQuery.attr( this, name, "" );
-                       if ( this.nodeType === 1 ) {
-                               this.removeAttribute( name );
-                       }
-               });
-       },
-
-       addClass: function( value ) {
-               if ( jQuery.isFunction(value) ) {
-                       return this.each(function(i) {
-                               var self = jQuery(this);
-                               self.addClass( value.call(this, i, self.attr("class")) );
-                       });
-               }
-
-               if ( value && typeof value === "string" ) {
-                       var classNames = (value || "").split( rspace );
-
-                       for ( var i = 0, l = this.length; i < l; i++ ) {
-                               var elem = this[i];
-
-                               if ( elem.nodeType === 1 ) {
-                                       if ( !elem.className ) {
-                                               elem.className = value;
-
-                                       } else {
-                                               var className = " " + elem.className + " ", setClass = elem.className;
-                                               for ( var c = 0, cl = classNames.length; c < cl; c++ ) {
-                                                       if ( className.indexOf( " " + classNames[c] + " " ) < 0 ) {
-                                                               setClass += " " + classNames[c];
-                                                       }
-                                               }
-                                               elem.className = jQuery.trim( setClass );
-                                       }
-                               }
-                       }
-               }
-
-               return this;
-       },
-
-       removeClass: function( value ) {
-               if ( jQuery.isFunction(value) ) {
-                       return this.each(function(i) {
-                               var self = jQuery(this);
-                               self.removeClass( value.call(this, i, self.attr("class")) );
-                       });
-               }
-
-               if ( (value && typeof value === "string") || value === undefined ) {
-                       var classNames = (value || "").split(rspace);
-
-                       for ( var i = 0, l = this.length; i < l; i++ ) {
-                               var elem = this[i];
-
-                               if ( elem.nodeType === 1 && elem.className ) {
-                                       if ( value ) {
-                                               var className = (" " + elem.className + " ").replace(rclass, " ");
-                                               for ( var c = 0, cl = classNames.length; c < cl; c++ ) {
-                                                       className = className.replace(" " + classNames[c] + " ", " ");
-                                               }
-                                               elem.className = jQuery.trim( className );
-
-                                       } else {
-                                               elem.className = "";
-                                       }
-                               }
-                       }
-               }
-
-               return this;
-       },
-
-       toggleClass: function( value, stateVal ) {
-               var type = typeof value, isBool = typeof stateVal === "boolean";
-
-               if ( jQuery.isFunction( value ) ) {
-                       return this.each(function(i) {
-                               var self = jQuery(this);
-                               self.toggleClass( value.call(this, i, self.attr("class"), stateVal), stateVal );
-                       });
-               }
-
-               return this.each(function() {
-                       if ( type === "string" ) {
-                               // toggle individual class names
-                               var className, i = 0, self = jQuery(this),
-                                       state = stateVal,
-                                       classNames = value.split( rspace );
-
-                               while ( (className = classNames[ i++ ]) ) {
-                                       // check each className given, space seperated list
-                                       state = isBool ? state : !self.hasClass( className );
-                                       self[ state ? "addClass" : "removeClass" ]( className );
-                               }
-
-                       } else if ( type === "undefined" || type === "boolean" ) {
-                               if ( this.className ) {
-                                       // store className if set
-                                       jQuery.data( this, "__className__", this.className );
-                               }
-
-                               // toggle whole className
-                               this.className = this.className || value === false ? "" : jQuery.data( this, "__className__" ) || "";
-                       }
-               });
-       },
-
-       hasClass: function( selector ) {
-               var className = " " + selector + " ";
-               for ( var i = 0, l = this.length; i < l; i++ ) {
-                       if ( (" " + this[i].className + " ").replace(rclass, " ").indexOf( className ) > -1 ) {
-                               return true;
-                       }
-               }
-
-               return false;
-       },
-
-       val: function( value ) {
-               if ( value === undefined ) {
-                       var elem = this[0];
-
-                       if ( elem ) {
-                               if ( jQuery.nodeName( elem, "option" ) ) {
-                                       return (elem.attributes.value || {}).specified ? elem.value : elem.text;
-                               }
-
-                               // We need to handle select boxes special
-                               if ( jQuery.nodeName( elem, "select" ) ) {
-                                       var index = elem.selectedIndex,
-                                               values = [],
-                                               options = elem.options,
-                                               one = elem.type === "select-one";
-
-                                       // Nothing was selected
-                                       if ( index < 0 ) {
-                                               return null;
-                                       }
-
-                                       // Loop through all the selected options
-                                       for ( var i = one ? index : 0, max = one ? index + 1 : options.length; i < max; i++ ) {
-                                               var option = options[ i ];
-
-                                               if ( option.selected ) {
-                                                       // Get the specifc value for the option
-                                                       value = jQuery(option).val();
-
-                                                       // We don't need an array for one selects
-                                                       if ( one ) {
-                                                               return value;
-                                                       }
-
-                                                       // Multi-Selects return an array
-                                                       values.push( value );
-                                               }
-                                       }
-
-                                       return values;
-                               }
-
-                               // Handle the case where in Webkit "" is returned instead of "on" if a value isn't specified
-                               if ( rradiocheck.test( elem.type ) && !jQuery.support.checkOn ) {
-                                       return elem.getAttribute("value") === null ? "on" : elem.value;
-                               }
-                               
-
-                               // Everything else, we just grab the value
-                               return (elem.value || "").replace(rreturn, "");
-
-                       }
-
-                       return undefined;
-               }
-
-               var isFunction = jQuery.isFunction(value);
-
-               return this.each(function(i) {
-                       var self = jQuery(this), val = value;
-
-                       if ( this.nodeType !== 1 ) {
-                               return;
-                       }
-
-                       if ( isFunction ) {
-                               val = value.call(this, i, self.val());
-                       }
-
-                       // Typecast each time if the value is a Function and the appended
-                       // value is therefore different each time.
-                       if ( typeof val === "number" ) {
-                               val += "";
-                       }
-
-                       if ( jQuery.isArray(val) && rradiocheck.test( this.type ) ) {
-                               this.checked = jQuery.inArray( self.val(), val ) >= 0;
-
-                       } else if ( jQuery.nodeName( this, "select" ) ) {
-                               var values = jQuery.makeArray(val);
-
-                               jQuery( "option", this ).each(function() {
-                                       this.selected = jQuery.inArray( jQuery(this).val(), values ) >= 0;
-                               });
-
-                               if ( !values.length ) {
-                                       this.selectedIndex = -1;
-                               }
-
-                       } else {
-                               this.value = val;
-                       }
-               });
-       }
-});
-
-jQuery.extend({
-       attrFn: {
-               val: true,
-               css: true,
-               html: true,
-               text: true,
-               data: true,
-               width: true,
-               height: true,
-               offset: true
-       },
-               
-       attr: function( elem, name, value, pass ) {
-               // don't set attributes on text and comment nodes
-               if ( !elem || elem.nodeType === 3 || elem.nodeType === 8 ) {
-                       return undefined;
-               }
-
-               if ( pass && name in jQuery.attrFn ) {
-                       return jQuery(elem)[name](value);
-               }
-
-               var notxml = elem.nodeType !== 1 || !jQuery.isXMLDoc( elem ),
-                       // Whether we are setting (or getting)
-                       set = value !== undefined;
-
-               // Try to normalize/fix the name
-               name = notxml && jQuery.props[ name ] || name;
-
-               // Only do all the following if this is a node (faster for style)
-               if ( elem.nodeType === 1 ) {
-                       // These attributes require special treatment
-                       var special = rspecialurl.test( name );
-
-                       // Safari mis-reports the default selected property of an option
-                       // Accessing the parent's selectedIndex property fixes it
-                       if ( name === "selected" && !jQuery.support.optSelected ) {
-                               var parent = elem.parentNode;
-                               if ( parent ) {
-                                       parent.selectedIndex;
-       
-                                       // Make sure that it also works with optgroups, see #5701
-                                       if ( parent.parentNode ) {
-                                               parent.parentNode.selectedIndex;
-                                       }
-                               }
-                       }
-
-                       // If applicable, access the attribute via the DOM 0 way
-                       if ( name in elem && notxml && !special ) {
-                               if ( set ) {
-                                       // We can't allow the type property to be changed (since it causes problems in IE)
-                                       if ( name === "type" && rtype.test( elem.nodeName ) && elem.parentNode ) {
-                                               jQuery.error( "type property can't be changed" );
-                                       }
-
-                                       elem[ name ] = value;
-                               }
-
-                               // browsers index elements by id/name on forms, give priority to attributes.
-                               if ( jQuery.nodeName( elem, "form" ) && elem.getAttributeNode(name) ) {
-                                       return elem.getAttributeNode( name ).nodeValue;
-                               }
-
-                               // elem.tabIndex doesn't always return the correct value when it hasn't been explicitly set
-                               // http://fluidproject.org/blog/2008/01/09/getting-setting-and-removing-tabindex-values-with-javascript/
-                               if ( name === "tabIndex" ) {
-                                       var attributeNode = elem.getAttributeNode( "tabIndex" );
-
-                                       return attributeNode && attributeNode.specified ?
-                                               attributeNode.value :
-                                               rfocusable.test( elem.nodeName ) || rclickable.test( elem.nodeName ) && elem.href ?
-                                                       0 :
-                                                       undefined;
-                               }
-
-                               return elem[ name ];
-                       }
-
-                       if ( !jQuery.support.style && notxml && name === "style" ) {
-                               if ( set ) {
-                                       elem.style.cssText = "" + value;
-                               }
-
-                               return elem.style.cssText;
-                       }
-
-                       if ( set ) {
-                               // convert the value to a string (all browsers do this but IE) see #1070
-                               elem.setAttribute( name, "" + value );
-                       }
-
-                       var attr = !jQuery.support.hrefNormalized && notxml && special ?
-                                       // Some attributes require a special call on IE
-                                       elem.getAttribute( name, 2 ) :
-                                       elem.getAttribute( name );
-
-                       // Non-existent attributes return null, we normalize to undefined
-                       return attr === null ? undefined : attr;
-               }
-
-               // elem is actually elem.style ... set the style
-               // Using attr for specific style information is now deprecated. Use style instead.
-               return jQuery.style( elem, name, value );
-       }
-});
-var rnamespaces = /\.(.*)$/,
-       fcleanup = function( nm ) {
-               return nm.replace(/[^\w\s\.\|`]/g, function( ch ) {
-                       return "\\" + ch;
-               });
-       };
-
-/*
- * A number of helper functions used for managing events.
- * Many of the ideas behind this code originated from
- * Dean Edwards' addEvent library.
- */
-jQuery.event = {
-
-       // Bind an event to an element
-       // Original by Dean Edwards
-       add: function( elem, types, handler, data ) {
-               if ( elem.nodeType === 3 || elem.nodeType === 8 ) {
-                       return;
-               }
-
-               // For whatever reason, IE has trouble passing the window object
-               // around, causing it to be cloned in the process
-               if ( elem.setInterval && ( elem !== window && !elem.frameElement ) ) {
-                       elem = window;
-               }
-
-               var handleObjIn, handleObj;
-
-               if ( handler.handler ) {
-                       handleObjIn = handler;
-                       handler = handleObjIn.handler;
-               }
-
-               // Make sure that the function being executed has a unique ID
-               if ( !handler.guid ) {
-                       handler.guid = jQuery.guid++;
-               }
-
-               // Init the element's event structure
-               var elemData = jQuery.data( elem );
-
-               // If no elemData is found then we must be trying to bind to one of the
-               // banned noData elements
-               if ( !elemData ) {
-                       return;
-               }
-
-               var events = elemData.events = elemData.events || {},
-                       eventHandle = elemData.handle, eventHandle;
-
-               if ( !eventHandle ) {
-                       elemData.handle = eventHandle = function() {
-                               // Handle the second event of a trigger and when
-                               // an event is called after a page has unloaded
-                               return typeof jQuery !== "undefined" && !jQuery.event.triggered ?
-                                       jQuery.event.handle.apply( eventHandle.elem, arguments ) :
-                                       undefined;
-                       };
-               }
-
-               // Add elem as a property of the handle function
-               // This is to prevent a memory leak with non-native events in IE.
-               eventHandle.elem = elem;
-
-               // Handle multiple events separated by a space
-               // jQuery(...).bind("mouseover mouseout", fn);
-               types = types.split(" ");
-
-               var type, i = 0, namespaces;
-
-               while ( (type = types[ i++ ]) ) {
-                       handleObj = handleObjIn ?
-                               jQuery.extend({}, handleObjIn) :
-                               { handler: handler, data: data };
-
-                       // Namespaced event handlers
-                       if ( type.indexOf(".") > -1 ) {
-                               namespaces = type.split(".");
-                               type = namespaces.shift();
-                               handleObj.namespace = namespaces.slice(0).sort().join(".");
-
-                       } else {
-                               namespaces = [];
-                               handleObj.namespace = "";
-                       }
-
-                       handleObj.type = type;
-                       handleObj.guid = handler.guid;
-
-                       // Get the current list of functions bound to this event
-                       var handlers = events[ type ],
-                               special = jQuery.event.special[ type ] || {};
-
-                       // Init the event handler queue
-                       if ( !handlers ) {
-                               handlers = events[ type ] = [];
-
-                               // Check for a special event handler
-                               // Only use addEventListener/attachEvent if the special
-                               // events handler returns false
-                               if ( !special.setup || special.setup.call( elem, data, namespaces, eventHandle ) === false ) {
-                                       // Bind the global event handler to the element
-                                       if ( elem.addEventListener ) {
-                                               elem.addEventListener( type, eventHandle, false );
-
-                                       } else if ( elem.attachEvent ) {
-                                               elem.attachEvent( "on" + type, eventHandle );
-                                       }
-                               }
-                       }
-                       
-                       if ( special.add ) { 
-                               special.add.call( elem, handleObj ); 
-
-                               if ( !handleObj.handler.guid ) {
-                                       handleObj.handler.guid = handler.guid;
-                               }
-                       }
-
-                       // Add the function to the element's handler list
-                       handlers.push( handleObj );
-
-                       // Keep track of which events have been used, for global triggering
-                       jQuery.event.global[ type ] = true;
-               }
-
-               // Nullify elem to prevent memory leaks in IE
-               elem = null;
-       },
-
-       global: {},
-
-       // Detach an event or set of events from an element
-       remove: function( elem, types, handler, pos ) {
-               // don't do events on text and comment nodes
-               if ( elem.nodeType === 3 || elem.nodeType === 8 ) {
-                       return;
-               }
-
-               var ret, type, fn, i = 0, all, namespaces, namespace, special, eventType, handleObj, origType,
-                       elemData = jQuery.data( elem ),
-                       events = elemData && elemData.events;
-
-               if ( !elemData || !events ) {
-                       return;
-               }
-
-               // types is actually an event object here
-               if ( types && types.type ) {
-                       handler = types.handler;
-                       types = types.type;
-               }
-
-               // Unbind all events for the element
-               if ( !types || typeof types === "string" && types.charAt(0) === "." ) {
-                       types = types || "";
-
-                       for ( type in events ) {
-                               jQuery.event.remove( elem, type + types );
-                       }
-
-                       return;
-               }
-
-               // Handle multiple events separated by a space
-               // jQuery(...).unbind("mouseover mouseout", fn);
-               types = types.split(" ");
-
-               while ( (type = types[ i++ ]) ) {
-                       origType = type;
-                       handleObj = null;
-                       all = type.indexOf(".") < 0;
-                       namespaces = [];
-
-                       if ( !all ) {
-                               // Namespaced event handlers
-                               namespaces = type.split(".");
-                               type = namespaces.shift();
-
-                               namespace = new RegExp("(^|\\.)" + 
-                                       jQuery.map( namespaces.slice(0).sort(), fcleanup ).join("\\.(?:.*\\.)?") + "(\\.|$)")
-                       }
-
-                       eventType = events[ type ];
-
-                       if ( !eventType ) {
-                               continue;
-                       }
-
-                       if ( !handler ) {
-                               for ( var j = 0; j < eventType.length; j++ ) {
-                                       handleObj = eventType[ j ];
-
-                                       if ( all || namespace.test( handleObj.namespace ) ) {
-                                               jQuery.event.remove( elem, origType, handleObj.handler, j );
-                                               eventType.splice( j--, 1 );
-                                       }
-                               }
-
-                               continue;
-                       }
-
-                       special = jQuery.event.special[ type ] || {};
-
-                       for ( var j = pos || 0; j < eventType.length; j++ ) {
-                               handleObj = eventType[ j ];
-
-                               if ( handler.guid === handleObj.guid ) {
-                                       // remove the given handler for the given type
-                                       if ( all || namespace.test( handleObj.namespace ) ) {
-                                               if ( pos == null ) {
-                                                       eventType.splice( j--, 1 );
-                                               }
-
-                                               if ( special.remove ) {
-                                                       special.remove.call( elem, handleObj );
-                                               }
-                                       }
-
-                                       if ( pos != null ) {
-                                               break;
-                                       }
-                               }
-                       }
-
-                       // remove generic event handler if no more handlers exist
-                       if ( eventType.length === 0 || pos != null && eventType.length === 1 ) {
-                               if ( !special.teardown || special.teardown.call( elem, namespaces ) === false ) {
-                                       removeEvent( elem, type, elemData.handle );
-                               }
-
-                               ret = null;
-                               delete events[ type ];
-                       }
-               }
-
-               // Remove the expando if it's no longer used
-               if ( jQuery.isEmptyObject( events ) ) {
-                       var handle = elemData.handle;
-                       if ( handle ) {
-                               handle.elem = null;
-                       }
-
-                       delete elemData.events;
-                       delete elemData.handle;
-
-                       if ( jQuery.isEmptyObject( elemData ) ) {
-                               jQuery.removeData( elem );
-                       }
-               }
-       },
-
-       // bubbling is internal
-       trigger: function( event, data, elem /*, bubbling */ ) {
-               // Event object or event type
-               var type = event.type || event,
-                       bubbling = arguments[3];
-
-               if ( !bubbling ) {
-                       event = typeof event === "object" ?
-                               // jQuery.Event object
-                               event[expando] ? event :
-                               // Object literal
-                               jQuery.extend( jQuery.Event(type), event ) :
-                               // Just the event type (string)
-                               jQuery.Event(type);
-
-                       if ( type.indexOf("!") >= 0 ) {
-                               event.type = type = type.slice(0, -1);
-                               event.exclusive = true;
-                       }
-
-                       // Handle a global trigger
-                       if ( !elem ) {
-                               // Don't bubble custom events when global (to avoid too much overhead)
-                               event.stopPropagation();
-
-                               // Only trigger if we've ever bound an event for it
-                               if ( jQuery.event.global[ type ] ) {
-                                       jQuery.each( jQuery.cache, function() {
-                                               if ( this.events && this.events[type] ) {
-                                                       jQuery.event.trigger( event, data, this.handle.elem );
-                                               }
-                                       });
-                               }
-                       }
-
-                       // Handle triggering a single element
-
-                       // don't do events on text and comment nodes
-                       if ( !elem || elem.nodeType === 3 || elem.nodeType === 8 ) {
-                               return undefined;
-                       }
-
-                       // Clean up in case it is reused
-                       event.result = undefined;
-                       event.target = elem;
-
-                       // Clone the incoming data, if any
-                       data = jQuery.makeArray( data );
-                       data.unshift( event );
-               }
-
-               event.currentTarget = elem;
-
-               // Trigger the event, it is assumed that "handle" is a function
-               var handle = jQuery.data( elem, "handle" );
-               if ( handle ) {
-                       handle.apply( elem, data );
-               }
-
-               var parent = elem.parentNode || elem.ownerDocument;
-
-               // Trigger an inline bound script
-               try {
-                       if ( !(elem && elem.nodeName && jQuery.noData[elem.nodeName.toLowerCase()]) ) {
-                               if ( elem[ "on" + type ] && elem[ "on" + type ].apply( elem, data ) === false ) {
-                                       event.result = false;
-                               }
-                       }
-
-               // prevent IE from throwing an error for some elements with some event types, see #3533
-               } catch (e) {}
-
-               if ( !event.isPropagationStopped() && parent ) {
-                       jQuery.event.trigger( event, data, parent, true );
-
-               } else if ( !event.isDefaultPrevented() ) {
-                       var target = event.target, old,
-                               isClick = jQuery.nodeName(target, "a") && type === "click",
-                               special = jQuery.event.special[ type ] || {};
-
-                       if ( (!special._default || special._default.call( elem, event ) === false) && 
-                               !isClick && !(target && target.nodeName && jQuery.noData[target.nodeName.toLowerCase()]) ) {
-
-                               try {
-                                       if ( target[ type ] ) {
-                                               // Make sure that we don't accidentally re-trigger the onFOO events
-                                               old = target[ "on" + type ];
-
-                                               if ( old ) {
-                                                       target[ "on" + type ] = null;
-                                               }
-
-                                               jQuery.event.triggered = true;
-                                               target[ type ]();
-                                       }
-
-                               // prevent IE from throwing an error for some elements with some event types, see #3533
-                               } catch (e) {}
-
-                               if ( old ) {
-                                       target[ "on" + type ] = old;
-                               }
-
-                               jQuery.event.triggered = false;
-                       }
-               }
-       },
-
-       handle: function( event ) {
-               var all, handlers, namespaces, namespace, events;
-
-               event = arguments[0] = jQuery.event.fix( event || window.event );
-               event.currentTarget = this;
-
-               // Namespaced event handlers
-               all = event.type.indexOf(".") < 0 && !event.exclusive;
-
-               if ( !all ) {
-                       namespaces = event.type.split(".");
-                       event.type = namespaces.shift();
-                       namespace = new RegExp("(^|\\.)" + namespaces.slice(0).sort().join("\\.(?:.*\\.)?") + "(\\.|$)");
-               }
-
-               var events = jQuery.data(this, "events"), handlers = events[ event.type ];
-
-               if ( events && handlers ) {
-                       // Clone the handlers to prevent manipulation
-                       handlers = handlers.slice(0);
-
-                       for ( var j = 0, l = handlers.length; j < l; j++ ) {
-                               var handleObj = handlers[ j ];
-
-                               // Filter the functions by class
-                               if ( all || namespace.test( handleObj.namespace ) ) {
-                                       // Pass in a reference to the handler function itself
-                                       // So that we can later remove it
-                                       event.handler = handleObj.handler;
-                                       event.data = handleObj.data;
-                                       event.handleObj = handleObj;
-       
-                                       var ret = handleObj.handler.apply( this, arguments );
-
-                                       if ( ret !== undefined ) {
-                                               event.result = ret;
-                                               if ( ret === false ) {
-                                                       event.preventDefault();
-                                                       event.stopPropagation();
-                                               }
-                                       }
-
-                                       if ( event.isImmediatePropagationStopped() ) {
-                                               break;
-                                       }
-                               }
-                       }
-               }
-
-               return event.result;
-       },
-
-       props: "altKey attrChange attrName bubbles button cancelable charCode clientX clientY ctrlKey currentTarget data detail eventPhase fromElement handler keyCode layerX layerY metaKey newValue offsetX offsetY originalTarget pageX pageY prevValue relatedNode relatedTarget screenX screenY shiftKey srcElement target toElement view wheelDelta which".split(" "),
-
-       fix: function( event ) {
-               if ( event[ expando ] ) {
-                       return event;
-               }
-
-               // store a copy of the original event object
-               // and "clone" to set read-only properties
-               var originalEvent = event;
-               event = jQuery.Event( originalEvent );
-
-               for ( var i = this.props.length, prop; i; ) {
-                       prop = this.props[ --i ];
-                       event[ prop ] = originalEvent[ prop ];
-               }
-
-               // Fix target property, if necessary
-               if ( !event.target ) {
-                       event.target = event.srcElement || document; // Fixes #1925 where srcElement might not be defined either
-               }
-
-               // check if target is a textnode (safari)
-               if ( event.target.nodeType === 3 ) {
-                       event.target = event.target.parentNode;
-               }
-
-               // Add relatedTarget, if necessary
-               if ( !event.relatedTarget && event.fromElement ) {
-                       event.relatedTarget = event.fromElement === event.target ? event.toElement : event.fromElement;
-               }
-
-               // Calculate pageX/Y if missing and clientX/Y available
-               if ( event.pageX == null && event.clientX != null ) {
-                       var doc = document.documentElement, body = document.body;
-                       event.pageX = event.clientX + (doc && doc.scrollLeft || body && body.scrollLeft || 0) - (doc && doc.clientLeft || body && body.clientLeft || 0);
-                       event.pageY = event.clientY + (doc && doc.scrollTop  || body && body.scrollTop  || 0) - (doc && doc.clientTop  || body && body.clientTop  || 0);
-               }
-
-               // Add which for key events
-               if ( !event.which && ((event.charCode || event.charCode === 0) ? event.charCode : event.keyCode) ) {
-                       event.which = event.charCode || event.keyCode;
-               }
-
-               // Add metaKey to non-Mac browsers (use ctrl for PC's and Meta for Macs)
-               if ( !event.metaKey && event.ctrlKey ) {
-                       event.metaKey = event.ctrlKey;
-               }
-
-               // Add which for click: 1 === left; 2 === middle; 3 === right
-               // Note: button is not normalized, so don't use it
-               if ( !event.which && event.button !== undefined ) {
-                       event.which = (event.button & 1 ? 1 : ( event.button & 2 ? 3 : ( event.button & 4 ? 2 : 0 ) ));
-               }
-
-               return event;
-       },
-
-       // Deprecated, use jQuery.guid instead
-       guid: 1E8,
-
-       // Deprecated, use jQuery.proxy instead
-       proxy: jQuery.proxy,
-
-       special: {
-               ready: {
-                       // Make sure the ready event is setup
-                       setup: jQuery.bindReady,
-                       teardown: jQuery.noop
-               },
-
-               live: {
-                       add: function( handleObj ) {
-                               jQuery.event.add( this, handleObj.origType, jQuery.extend({}, handleObj, {handler: liveHandler}) ); 
-                       },
-
-                       remove: function( handleObj ) {
-                               var remove = true,
-                                       type = handleObj.origType.replace(rnamespaces, "");
-                               
-                               jQuery.each( jQuery.data(this, "events").live || [], function() {
-                                       if ( type === this.origType.replace(rnamespaces, "") ) {
-                                               remove = false;
-                                               return false;
-                                       }
-                               });
-
-                               if ( remove ) {
-                                       jQuery.event.remove( this, handleObj.origType, liveHandler );
-                               }
-                       }
-
-               },
-
-               beforeunload: {
-                       setup: function( data, namespaces, eventHandle ) {
-                               // We only want to do this special case on windows
-                               if ( this.setInterval ) {
-                                       this.onbeforeunload = eventHandle;
-                               }
-
-                               return false;
-                       },
-                       teardown: function( namespaces, eventHandle ) {
-                               if ( this.onbeforeunload === eventHandle ) {
-                                       this.onbeforeunload = null;
-                               }
-                       }
-               }
-       }
-};
-
-var removeEvent = document.removeEventListener ?
-       function( elem, type, handle ) {
-               elem.removeEventListener( type, handle, false );
-       } : 
-       function( elem, type, handle ) {
-               elem.detachEvent( "on" + type, handle );
-       };
-
-jQuery.Event = function( src ) {
-       // Allow instantiation without the 'new' keyword
-       if ( !this.preventDefault ) {
-               return new jQuery.Event( src );
-       }
-
-       // Event object
-       if ( src && src.type ) {
-               this.originalEvent = src;
-               this.type = src.type;
-       // Event type
-       } else {
-               this.type = src;
-       }
-
-       // timeStamp is buggy for some events on Firefox(#3843)
-       // So we won't rely on the native value
-       this.timeStamp = now();
-
-       // Mark it as fixed
-       this[ expando ] = true;
-};
-
-function returnFalse() {
-       return false;
-}
-function returnTrue() {
-       return true;
-}
-
-// jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding
-// http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html
-jQuery.Event.prototype = {
-       preventDefault: function() {
-               this.isDefaultPrevented = returnTrue;
-
-               var e = this.originalEvent;
-               if ( !e ) {
-                       return;
-               }
-               
-               // if preventDefault exists run it on the original event
-               if ( e.preventDefault ) {
-                       e.preventDefault();
-               }
-               // otherwise set the returnValue property of the original event to false (IE)
-               e.returnValue = false;
-       },
-       stopPropagation: function() {
-               this.isPropagationStopped = returnTrue;
-
-               var e = this.originalEvent;
-               if ( !e ) {
-                       return;
-               }
-               // if stopPropagation exists run it on the original event
-               if ( e.stopPropagation ) {
-                       e.stopPropagation();
-               }
-               // otherwise set the cancelBubble property of the original event to true (IE)
-               e.cancelBubble = true;
-       },
-       stopImmediatePropagation: function() {
-               this.isImmediatePropagationStopped = returnTrue;
-               this.stopPropagation();
-       },
-       isDefaultPrevented: returnFalse,
-       isPropagationStopped: returnFalse,
-       isImmediatePropagationStopped: returnFalse
-};
-
-// Checks if an event happened on an element within another element
-// Used in jQuery.event.special.mouseenter and mouseleave handlers
-var withinElement = function( event ) {
-       // Check if mouse(over|out) are still within the same parent element
-       var parent = event.relatedTarget;
-
-       // Firefox sometimes assigns relatedTarget a XUL element
-       // which we cannot access the parentNode property of
-       try {
-               // Traverse up the tree
-               while ( parent && parent !== this ) {
-                       parent = parent.parentNode;
-               }
-
-               if ( parent !== this ) {
-                       // set the correct event type
-                       event.type = event.data;
-
-                       // handle event if we actually just moused on to a non sub-element
-                       jQuery.event.handle.apply( this, arguments );
-               }
-
-       // assuming we've left the element since we most likely mousedover a xul element
-       } catch(e) { }
-},
-
-// In case of event delegation, we only need to rename the event.type,
-// liveHandler will take care of the rest.
-delegate = function( event ) {
-       event.type = event.data;
-       jQuery.event.handle.apply( this, arguments );
-};
-
-// Create mouseenter and mouseleave events
-jQuery.each({
-       mouseenter: "mouseover",
-       mouseleave: "mouseout"
-}, function( orig, fix ) {
-       jQuery.event.special[ orig ] = {
-               setup: function( data ) {
-                       jQuery.event.add( this, fix, data && data.selector ? delegate : withinElement, orig );
-               },
-               teardown: function( data ) {
-                       jQuery.event.remove( this, fix, data && data.selector ? delegate : withinElement );
-               }
-       };
-});
-
-// submit delegation
-if ( !jQuery.support.submitBubbles ) {
-
-       jQuery.event.special.submit = {
-               setup: function( data, namespaces ) {
-                       if ( this.nodeName.toLowerCase() !== "form" ) {
-                               jQuery.event.add(this, "click.specialSubmit", function( e ) {
-                                       var elem = e.target, type = elem.type;
-
-                                       if ( (type === "submit" || type === "image") && jQuery( elem ).closest("form").length ) {
-                                               return trigger( "submit", this, arguments );
-                                       }
-                               });
-        
-                               jQuery.event.add(this, "keypress.specialSubmit", function( e ) {
-                                       var elem = e.target, type = elem.type;
-
-                                       if ( (type === "text" || type === "password") && jQuery( elem ).closest("form").length && e.keyCode === 13 ) {
-                                               return trigger( "submit", this, arguments );
-                                       }
-                               });
-
-                       } else {
-                               return false;
-                       }
-               },
-
-               teardown: function( namespaces ) {
-                       jQuery.event.remove( this, ".specialSubmit" );
-               }
-       };
-
-}
-
-// change delegation, happens here so we have bind.
-if ( !jQuery.support.changeBubbles ) {
-
-       var formElems = /textarea|input|select/i,
-
-       changeFilters,
-
-       getVal = function( elem ) {
-               var type = elem.type, val = elem.value;
-
-               if ( type === "radio" || type === "checkbox" ) {
-                       val = elem.checked;
-
-               } else if ( type === "select-multiple" ) {
-                       val = elem.selectedIndex > -1 ?
-                               jQuery.map( elem.options, function( elem ) {
-                                       return elem.selected;
-                               }).join("-") :
-                               "";
-
-               } else if ( elem.nodeName.toLowerCase() === "select" ) {
-                       val = elem.selectedIndex;
-               }
-
-               return val;
-       },
-
-       testChange = function testChange( e ) {
-               var elem = e.target, data, val;
-
-               if ( !formElems.test( elem.nodeName ) || elem.readOnly ) {
-                       return;
-               }
-
-               data = jQuery.data( elem, "_change_data" );
-               val = getVal(elem);
-
-               // the current data will be also retrieved by beforeactivate
-               if ( e.type !== "focusout" || elem.type !== "radio" ) {
-                       jQuery.data( elem, "_change_data", val );
-               }
-               
-               if ( data === undefined || val === data ) {
-                       return;
-               }
-
-               if ( data != null || val ) {
-                       e.type = "change";
-                       return jQuery.event.trigger( e, arguments[1], elem );
-               }
-       };
-
-       jQuery.event.special.change = {
-               filters: {
-                       focusout: testChange, 
-
-                       click: function( e ) {
-                               var elem = e.target, type = elem.type;
-
-                               if ( type === "radio" || type === "checkbox" || elem.nodeName.toLowerCase() === "select" ) {
-                                       return testChange.call( this, e );
-                               }
-                       },
-
-                       // Change has to be called before submit
-                       // Keydown will be called before keypress, which is used in submit-event delegation
-                       keydown: function( e ) {
-                               var elem = e.target, type = elem.type;
-
-                               if ( (e.keyCode === 13 && elem.nodeName.toLowerCase() !== "textarea") ||
-                                       (e.keyCode === 32 && (type === "checkbox" || type === "radio")) ||
-                                       type === "select-multiple" ) {
-                                       return testChange.call( this, e );
-                               }
-                       },
-
-                       // Beforeactivate happens also before the previous element is blurred
-                       // with this event you can't trigger a change event, but you can store
-                       // information/focus[in] is not needed anymore
-                       beforeactivate: function( e ) {
-                               var elem = e.target;
-                               jQuery.data( elem, "_change_data", getVal(elem) );
-                       }
-               },
-
-               setup: function( data, namespaces ) {
-                       if ( this.type === "file" ) {
-                               return false;
-                       }
-
-                       for ( var type in changeFilters ) {
-                               jQuery.event.add( this, type + ".specialChange", changeFilters[type] );
-                       }
-
-                       return formElems.test( this.nodeName );
-               },
-
-               teardown: function( namespaces ) {
-                       jQuery.event.remove( this, ".specialChange" );
-
-                       return formElems.test( this.nodeName );
-               }
-       };
-
-       changeFilters = jQuery.event.special.change.filters;
-}
-
-function trigger( type, elem, args ) {
-       args[0].type = type;
-       return jQuery.event.handle.apply( elem, args );
-}
-
-// Create "bubbling" focus and blur events
-if ( document.addEventListener ) {
-       jQuery.each({ focus: "focusin", blur: "focusout" }, function( orig, fix ) {
-               jQuery.event.special[ fix ] = {
-                       setup: function() {
-                               this.addEventListener( orig, handler, true );
-                       }, 
-                       teardown: function() { 
-                               this.removeEventListener( orig, handler, true );
-                       }
-               };
-
-               function handler( e ) { 
-                       e = jQuery.event.fix( e );
-                       e.type = fix;
-                       return jQuery.event.handle.call( this, e );
-               }
-       });
-}
-
-jQuery.each(["bind", "one"], function( i, name ) {
-       jQuery.fn[ name ] = function( type, data, fn ) {
-               // Handle object literals
-               if ( typeof type === "object" ) {
-                       for ( var key in type ) {
-                               this[ name ](key, data, type[key], fn);
-                       }
-                       return this;
-               }
-               
-               if ( jQuery.isFunction( data ) ) {
-                       fn = data;
-                       data = undefined;
-               }
-
-               var handler = name === "one" ? jQuery.proxy( fn, function( event ) {
-                       jQuery( this ).unbind( event, handler );
-                       return fn.apply( this, arguments );
-               }) : fn;
-
-               if ( type === "unload" && name !== "one" ) {
-                       this.one( type, data, fn );
-
-               } else {
-                       for ( var i = 0, l = this.length; i < l; i++ ) {
-                               jQuery.event.add( this[i], type, handler, data );
-                       }
-               }
-
-               return this;
-       };
-});
-
-jQuery.fn.extend({
-       unbind: function( type, fn ) {
-               // Handle object literals
-               if ( typeof type === "object" && !type.preventDefault ) {
-                       for ( var key in type ) {
-                               this.unbind(key, type[key]);
-                       }
-
-               } else {
-                       for ( var i = 0, l = this.length; i < l; i++ ) {
-                               jQuery.event.remove( this[i], type, fn );
-                       }
-               }
-
-               return this;
-       },
-       
-       delegate: function( selector, types, data, fn ) {
-               return this.live( types, data, fn, selector );
-       },
-       
-       undelegate: function( selector, types, fn ) {
-               if ( arguments.length === 0 ) {
-                               return this.unbind( "live" );
-               
-               } else {
-                       return this.die( types, null, fn, selector );
-               }
-       },
-       
-       trigger: function( type, data ) {
-               return this.each(function() {
-                       jQuery.event.trigger( type, data, this );
-               });
-       },
-
-       triggerHandler: function( type, data ) {
-               if ( this[0] ) {
-                       var event = jQuery.Event( type );
-                       event.preventDefault();
-                       event.stopPropagation();
-                       jQuery.event.trigger( event, data, this[0] );
-                       return event.result;
-               }
-       },
-
-       toggle: function( fn ) {
-               // Save reference to arguments for access in closure
-               var args = arguments, i = 1;
-
-               // link all the functions, so any of them can unbind this click handler
-               while ( i < args.length ) {
-                       jQuery.proxy( fn, args[ i++ ] );
-               }
-
-               return this.click( jQuery.proxy( fn, function( event ) {
-                       // Figure out which function to execute
-                       var lastToggle = ( jQuery.data( this, "lastToggle" + fn.guid ) || 0 ) % i;
-                       jQuery.data( this, "lastToggle" + fn.guid, lastToggle + 1 );
-
-                       // Make sure that clicks stop
-                       event.preventDefault();
-
-                       // and execute the function
-                       return args[ lastToggle ].apply( this, arguments ) || false;
-               }));
-       },
-
-       hover: function( fnOver, fnOut ) {
-               return this.mouseenter( fnOver ).mouseleave( fnOut || fnOver );
-       }
-});
-
-var liveMap = {
-       focus: "focusin",
-       blur: "focusout",
-       mouseenter: "mouseover",
-       mouseleave: "mouseout"
-};
-
-jQuery.each(["live", "die"], function( i, name ) {
-       jQuery.fn[ name ] = function( types, data, fn, origSelector /* Internal Use Only */ ) {
-               var type, i = 0, match, namespaces, preType,
-                       selector = origSelector || this.selector,
-                       context = origSelector ? this : jQuery( this.context );
-
-               if ( jQuery.isFunction( data ) ) {
-                       fn = data;
-                       data = undefined;
-               }
-
-               types = (types || "").split(" ");
-
-               while ( (type = types[ i++ ]) != null ) {
-                       match = rnamespaces.exec( type );
-                       namespaces = "";
-
-                       if ( match )  {
-                               namespaces = match[0];
-                               type = type.replace( rnamespaces, "" );
-                       }
-
-                       if ( type === "hover" ) {
-                               types.push( "mouseenter" + namespaces, "mouseleave" + namespaces );
-                               continue;
-                       }
-
-                       preType = type;
-
-                       if ( type === "focus" || type === "blur" ) {
-                               types.push( liveMap[ type ] + namespaces );
-                               type = type + namespaces;
-
-                       } else {
-                               type = (liveMap[ type ] || type) + namespaces;
-                       }
-
-                       if ( name === "live" ) {
-                               // bind live handler
-                               context.each(function(){
-                                       jQuery.event.add( this, liveConvert( type, selector ),
-                                               { data: data, selector: selector, handler: fn, origType: type, origHandler: fn, preType: preType } );
-                               });
-
-                       } else {
-                               // unbind live handler
-                               context.unbind( liveConvert( type, selector ), fn );
-                       }
-               }
-               
-               return this;
-       }
-});
-
-function liveHandler( event ) {
-       var stop, elems = [], selectors = [], args = arguments,
-               related, match, handleObj, elem, j, i, l, data,
-               events = jQuery.data( this, "events" );
-
-       // Make sure we avoid non-left-click bubbling in Firefox (#3861)
-       if ( event.liveFired === this || !events || !events.live || event.button && event.type === "click" ) {
-               return;
-       }
-
-       event.liveFired = this;
-
-       var live = events.live.slice(0);
-
-       for ( j = 0; j < live.length; j++ ) {
-               handleObj = live[j];
-
-               if ( handleObj.origType.replace( rnamespaces, "" ) === event.type ) {
-                       selectors.push( handleObj.selector );
-
-               } else {
-                       live.splice( j--, 1 );
-               }
-       }
-
-       match = jQuery( event.target ).closest( selectors, event.currentTarget );
-
-       for ( i = 0, l = match.length; i < l; i++ ) {
-               for ( j = 0; j < live.length; j++ ) {
-                       handleObj = live[j];
-
-                       if ( match[i].selector === handleObj.selector ) {
-                               elem = match[i].elem;
-                               related = null;
-
-                               // Those two events require additional checking
-                               if ( handleObj.preType === "mouseenter" || handleObj.preType === "mouseleave" ) {
-                                       related = jQuery( event.relatedTarget ).closest( handleObj.selector )[0];
-                               }
-
-                               if ( !related || related !== elem ) {
-                                       elems.push({ elem: elem, handleObj: handleObj });
-                               }
-                       }
-               }
-       }
-
-       for ( i = 0, l = elems.length; i < l; i++ ) {
-               match = elems[i];
-               event.currentTarget = match.elem;
-               event.data = match.handleObj.data;
-               event.handleObj = match.handleObj;
-
-               if ( match.handleObj.origHandler.apply( match.elem, args ) === false ) {
-                       stop = false;
-                       break;
-               }
-       }
-
-       return stop;
-}
-
-function liveConvert( type, selector ) {
-       return "live." + (type && type !== "*" ? type + "." : "") + selector.replace(/\./g, "`").replace(/ /g, "&");
-}
-
-jQuery.each( ("blur focus focusin focusout load resize scroll unload click dblclick " +
-       "mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave " +
-       "change select submit keydown keypress keyup error").split(" "), function( i, name ) {
-
-       // Handle event binding
-       jQuery.fn[ name ] = function( fn ) {
-               return fn ? this.bind( name, fn ) : this.trigger( name );
-       };
-
-       if ( jQuery.attrFn ) {
-               jQuery.attrFn[ name ] = true;
-       }
-});
-
-// Prevent memory leaks in IE
-// Window isn't included so as not to unbind existing unload events
-// More info:
-//  - http://isaacschlueter.com/2006/10/msie-memory-leaks/
-if ( window.attachEvent && !window.addEventListener ) {
-       window.attachEvent("onunload", function() {
-               for ( var id in jQuery.cache ) {
-                       if ( jQuery.cache[ id ].handle ) {
-                               // Try/Catch is to handle iframes being unloaded, see #4280
-                               try {
-                                       jQuery.event.remove( jQuery.cache[ id ].handle.elem );
-                               } catch(e) {}
-                       }
-               }
-       });
-}
-/*!
- * Sizzle CSS Selector Engine - v1.0
- *  Copyright 2009, The Dojo Foundation
- *  Released under the MIT, BSD, and GPL Licenses.
- *  More information: http://sizzlejs.com/
- */
-(function(){
-
-var chunker = /((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^[\]]*\]|['"][^'"]*['"]|[^[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g,
-       done = 0,
-       toString = Object.prototype.toString,
-       hasDuplicate = false,
-       baseHasDuplicate = true;
-
-// Here we check if the JavaScript engine is using some sort of
-// optimization where it does not always call our comparision
-// function. If that is the case, discard the hasDuplicate value.
-//   Thus far that includes Google Chrome.
-[0, 0].sort(function(){
-       baseHasDuplicate = false;
-       return 0;
-});
-
-var Sizzle = function(selector, context, results, seed) {
-       results = results || [];
-       var origContext = context = context || document;
-
-       if ( context.nodeType !== 1 && context.nodeType !== 9 ) {
-               return [];
-       }
-       
-       if ( !selector || typeof selector !== "string" ) {
-               return results;
-       }
-
-       var parts = [], m, set, checkSet, extra, prune = true, contextXML = isXML(context),
-               soFar = selector;
-       
-       // Reset the position of the chunker regexp (start from head)
-       while ( (chunker.exec(""), m = chunker.exec(soFar)) !== null ) {
-               soFar = m[3];
-               
-               parts.push( m[1] );
-               
-               if ( m[2] ) {
-                       extra = m[3];
-                       break;
-               }
-       }
-
-       if ( parts.length > 1 && origPOS.exec( selector ) ) {
-               if ( parts.length === 2 && Expr.relative[ parts[0] ] ) {
-                       set = posProcess( parts[0] + parts[1], context );
-               } else {
-                       set = Expr.relative[ parts[0] ] ?
-                               [ context ] :
-                               Sizzle( parts.shift(), context );
-
-                       while ( parts.length ) {
-                               selector = parts.shift();
-
-                               if ( Expr.relative[ selector ] ) {
-                                       selector += parts.shift();
-                               }
-                               
-                               set = posProcess( selector, set );
-                       }
-               }
-       } else {
-               // Take a shortcut and set the context if the root selector is an ID
-               // (but not if it'll be faster if the inner selector is an ID)
-               if ( !seed && parts.length > 1 && context.nodeType === 9 && !contextXML &&
-                               Expr.match.ID.test(parts[0]) && !Expr.match.ID.test(parts[parts.length - 1]) ) {
-                       var ret = Sizzle.find( parts.shift(), context, contextXML );
-                       context = ret.expr ? Sizzle.filter( ret.expr, ret.set )[0] : ret.set[0];
-               }
-
-               if ( context ) {
-                       var ret = seed ?
-                               { expr: parts.pop(), set: makeArray(seed) } :
-                               Sizzle.find( parts.pop(), parts.length === 1 && (parts[0] === "~" || parts[0] === "+") && context.parentNode ? context.parentNode : context, contextXML );
-                       set = ret.expr ? Sizzle.filter( ret.expr, ret.set ) : ret.set;
-
-                       if ( parts.length > 0 ) {
-                               checkSet = makeArray(set);
-                       } else {
-                               prune = false;
-                       }
-
-                       while ( parts.length ) {
-                               var cur = parts.pop(), pop = cur;
-
-                               if ( !Expr.relative[ cur ] ) {
-                                       cur = "";
-                               } else {
-                                       pop = parts.pop();
-                               }
-
-                               if ( pop == null ) {
-                                       pop = context;
-                               }
-
-                               Expr.relative[ cur ]( checkSet, pop, contextXML );
-                       }
-               } else {
-                       checkSet = parts = [];
-               }
-       }
-
-       if ( !checkSet ) {
-               checkSet = set;
-       }
-
-       if ( !checkSet ) {
-               Sizzle.error( cur || selector );
-       }
-
-       if ( toString.call(checkSet) === "[object Array]" ) {
-               if ( !prune ) {
-                       results.push.apply( results, checkSet );
-               } else if ( context && context.nodeType === 1 ) {
-                       for ( var i = 0; checkSet[i] != null; i++ ) {
-                               if ( checkSet[i] && (checkSet[i] === true || checkSet[i].nodeType === 1 && contains(context, checkSet[i])) ) {
-                                       results.push( set[i] );
-                               }
-                       }
-               } else {
-                       for ( var i = 0; checkSet[i] != null; i++ ) {
-                               if ( checkSet[i] && checkSet[i].nodeType === 1 ) {
-                                       results.push( set[i] );
-                               }
-                       }
-               }
-       } else {
-               makeArray( checkSet, results );
-       }
-
-       if ( extra ) {
-               Sizzle( extra, origContext, results, seed );
-               Sizzle.uniqueSort( results );
-       }
-
-       return results;
-};
-
-Sizzle.uniqueSort = function(results){
-       if ( sortOrder ) {
-               hasDuplicate = baseHasDuplicate;
-               results.sort(sortOrder);
-
-               if ( hasDuplicate ) {
-                       for ( var i = 1; i < results.length; i++ ) {
-                               if ( results[i] === results[i-1] ) {
-                                       results.splice(i--, 1);
-                               }
-                       }
-               }
-       }
-
-       return results;
-};
-
-Sizzle.matches = function(expr, set){
-       return Sizzle(expr, null, null, set);
-};
-
-Sizzle.find = function(expr, context, isXML){
-       var set, match;
-
-       if ( !expr ) {
-               return [];
-       }
-
-       for ( var i = 0, l = Expr.order.length; i < l; i++ ) {
-               var type = Expr.order[i], match;
-               
-               if ( (match = Expr.leftMatch[ type ].exec( expr )) ) {
-                       var left = match[1];
-                       match.splice(1,1);
-
-                       if ( left.substr( left.length - 1 ) !== "\\" ) {
-                               match[1] = (match[1] || "").replace(/\\/g, "");
-                               set = Expr.find[ type ]( match, context, isXML );
-                               if ( set != null ) {
-                                       expr = expr.replace( Expr.match[ type ], "" );
-                                       break;
-                               }
-                       }
-               }
-       }
-
-       if ( !set ) {
-               set = context.getElementsByTagName("*");
-       }
-
-       return {set: set, expr: expr};
-};
-
-Sizzle.filter = function(expr, set, inplace, not){
-       var old = expr, result = [], curLoop = set, match, anyFound,
-               isXMLFilter = set && set[0] && isXML(set[0]);
-
-       while ( expr && set.length ) {
-               for ( var type in Expr.filter ) {
-                       if ( (match = Expr.leftMatch[ type ].exec( expr )) != null && match[2] ) {
-                               var filter = Expr.filter[ type ], found, item, left = match[1];
-                               anyFound = false;
-
-                               match.splice(1,1);
-
-                               if ( left.substr( left.length - 1 ) === "\\" ) {
-                                       continue;
-                               }
-
-                               if ( curLoop === result ) {
-                                       result = [];
-                               }
-
-                               if ( Expr.preFilter[ type ] ) {
-                                       match = Expr.preFilter[ type ]( match, curLoop, inplace, result, not, isXMLFilter );
-
-                                       if ( !match ) {
-                                               anyFound = found = true;
-                                       } else if ( match === true ) {
-                                               continue;
-                                       }
-                               }
-
-                               if ( match ) {
-                                       for ( var i = 0; (item = curLoop[i]) != null; i++ ) {
-                                               if ( item ) {
-                                                       found = filter( item, match, i, curLoop );
-                                                       var pass = not ^ !!found;
-
-                                                       if ( inplace && found != null ) {
-                                                               if ( pass ) {
-                                                                       anyFound = true;
-                                                               } else {
-                                                                       curLoop[i] = false;
-                                                               }
-                                                       } else if ( pass ) {
-                                                               result.push( item );
-                                                               anyFound = true;
-                                                       }
-                                               }
-                                       }
-                               }
-
-                               if ( found !== undefined ) {
-                                       if ( !inplace ) {
-                                               curLoop = result;
-                                       }
-
-                                       expr = expr.replace( Expr.match[ type ], "" );
-
-                                       if ( !anyFound ) {
-                                               return [];
-                                       }
-
-                                       break;
-                               }
-                       }
-               }
-
-               // Improper expression
-               if ( expr === old ) {
-                       if ( anyFound == null ) {
-                               Sizzle.error( expr );
-                       } else {
-                               break;
-                       }
-               }
-
-               old = expr;
-       }
-
-       return curLoop;
-};
-
-Sizzle.error = function( msg ) {
-       throw "Syntax error, unrecognized expression: " + msg;
-};
-
-var Expr = Sizzle.selectors = {
-       order: [ "ID", "NAME", "TAG" ],
-       match: {
-               ID: /#((?:[\w\u00c0-\uFFFF-]|\\.)+)/,
-               CLASS: /\.((?:[\w\u00c0-\uFFFF-]|\\.)+)/,
-               NAME: /\[name=['"]*((?:[\w\u00c0-\uFFFF-]|\\.)+)['"]*\]/,
-               ATTR: /\[\s*((?:[\w\u00c0-\uFFFF-]|\\.)+)\s*(?:(\S?=)\s*(['"]*)(.*?)\3|)\s*\]/,
-               TAG: /^((?:[\w\u00c0-\uFFFF\*-]|\\.)+)/,
-               CHILD: /:(only|nth|last|first)-child(?:\((even|odd|[\dn+-]*)\))?/,
-               POS: /:(nth|eq|gt|lt|first|last|even|odd)(?:\((\d*)\))?(?=[^-]|$)/,
-               PSEUDO: /:((?:[\w\u00c0-\uFFFF-]|\\.)+)(?:\((['"]?)((?:\([^\)]+\)|[^\(\)]*)+)\2\))?/
-       },
-       leftMatch: {},
-       attrMap: {
-               "class": "className",
-               "for": "htmlFor"
-       },
-       attrHandle: {
-               href: function(elem){
-                       return elem.getAttribute("href");
-               }
-       },
-       relative: {
-               "+": function(checkSet, part){
-                       var isPartStr = typeof part === "string",
-                               isTag = isPartStr && !/\W/.test(part),
-                               isPartStrNotTag = isPartStr && !isTag;
-
-                       if ( isTag ) {
-                               part = part.toLowerCase();
-                       }
-
-                       for ( var i = 0, l = checkSet.length, elem; i < l; i++ ) {
-                               if ( (elem = checkSet[i]) ) {
-                                       while ( (elem = elem.previousSibling) && elem.nodeType !== 1 ) {}
-
-                                       checkSet[i] = isPartStrNotTag || elem && elem.nodeName.toLowerCase() === part ?
-                                               elem || false :
-                                               elem === part;
-                               }
-                       }
-
-                       if ( isPartStrNotTag ) {
-                               Sizzle.filter( part, checkSet, true );
-                       }
-               },
-               ">": function(checkSet, part){
-                       var isPartStr = typeof part === "string";
-
-                       if ( isPartStr && !/\W/.test(part) ) {
-                               part = part.toLowerCase();
-
-                               for ( var i = 0, l = checkSet.length; i < l; i++ ) {
-                                       var elem = checkSet[i];
-                                       if ( elem ) {
-                                               var parent = elem.parentNode;
-                                               checkSet[i] = parent.nodeName.toLowerCase() === part ? parent : false;
-                                       }
-                               }
-                       } else {
-                               for ( var i = 0, l = checkSet.length; i < l; i++ ) {
-                                       var elem = checkSet[i];
-                                       if ( elem ) {
-                                               checkSet[i] = isPartStr ?
-                                                       elem.parentNode :
-                                                       elem.parentNode === part;
-                                       }
-                               }
-
-                               if ( isPartStr ) {
-                                       Sizzle.filter( part, checkSet, true );
-                               }
-                       }
-               },
-               "": function(checkSet, part, isXML){
-                       var doneName = done++, checkFn = dirCheck;
-
-                       if ( typeof part === "string" && !/\W/.test(part) ) {
-                               var nodeCheck = part = part.toLowerCase();
-                               checkFn = dirNodeCheck;
-                       }
-
-                       checkFn("parentNode", part, doneName, checkSet, nodeCheck, isXML);
-               },
-               "~": function(checkSet, part, isXML){
-                       var doneName = done++, checkFn = dirCheck;
-
-                       if ( typeof part === "string" && !/\W/.test(part) ) {
-                               var nodeCheck = part = part.toLowerCase();
-                               checkFn = dirNodeCheck;
-                       }
-
-                       checkFn("previousSibling", part, doneName, checkSet, nodeCheck, isXML);
-               }
-       },
-       find: {
-               ID: function(match, context, isXML){
-                       if ( typeof context.getElementById !== "undefined" && !isXML ) {
-                               var m = context.getElementById(match[1]);
-                               return m ? [m] : [];
-                       }
-               },
-               NAME: function(match, context){
-                       if ( typeof context.getElementsByName !== "undefined" ) {
-                               var ret = [], results = context.getElementsByName(match[1]);
-
-                               for ( var i = 0, l = results.length; i < l; i++ ) {
-                                       if ( results[i].getAttribute("name") === match[1] ) {
-                                               ret.push( results[i] );
-                                       }
-                               }
-
-                               return ret.length === 0 ? null : ret;
-                       }
-               },
-               TAG: function(match, context){
-                       return context.getElementsByTagName(match[1]);
-               }
-       },
-       preFilter: {
-               CLASS: function(match, curLoop, inplace, result, not, isXML){
-                       match = " " + match[1].replace(/\\/g, "") + " ";
-
-                       if ( isXML ) {
-                               return match;
-                       }
-
-                       for ( var i = 0, elem; (elem = curLoop[i]) != null; i++ ) {
-                               if ( elem ) {
-                                       if ( not ^ (elem.className && (" " + elem.className + " ").replace(/[\t\n]/g, " ").indexOf(match) >= 0) ) {
-                                               if ( !inplace ) {
-                                                       result.push( elem );
-                                               }
-                                       } else if ( inplace ) {
-                                               curLoop[i] = false;
-                                       }
-                               }
-                       }
-
-                       return false;
-               },
-               ID: function(match){
-                       return match[1].replace(/\\/g, "");
-               },
-               TAG: function(match, curLoop){
-                       return match[1].toLowerCase();
-               },
-               CHILD: function(match){
-                       if ( match[1] === "nth" ) {
-                               // parse equations like 'even', 'odd', '5', '2n', '3n+2', '4n-1', '-n+6'
-                               var test = /(-?)(\d*)n((?:\+|-)?\d*)/.exec(
-                                       match[2] === "even" && "2n" || match[2] === "odd" && "2n+1" ||
-                                       !/\D/.test( match[2] ) && "0n+" + match[2] || match[2]);
-
-                               // calculate the numbers (first)n+(last) including if they are negative
-                               match[2] = (test[1] + (test[2] || 1)) - 0;
-                               match[3] = test[3] - 0;
-                       }
-
-                       // TODO: Move to normal caching system
-                       match[0] = done++;
-
-                       return match;
-               },
-               ATTR: function(match, curLoop, inplace, result, not, isXML){
-                       var name = match[1].replace(/\\/g, "");
-                       
-                       if ( !isXML && Expr.attrMap[name] ) {
-                               match[1] = Expr.attrMap[name];
-                       }
-
-                       if ( match[2] === "~=" ) {
-                               match[4] = " " + match[4] + " ";
-                       }
-
-                       return match;
-               },
-               PSEUDO: function(match, curLoop, inplace, result, not){
-                       if ( match[1] === "not" ) {
-                               // If we're dealing with a complex expression, or a simple one
-                               if ( ( chunker.exec(match[3]) || "" ).length > 1 || /^\w/.test(match[3]) ) {
-                                       match[3] = Sizzle(match[3], null, null, curLoop);
-                               } else {
-                                       var ret = Sizzle.filter(match[3], curLoop, inplace, true ^ not);
-                                       if ( !inplace ) {
-                                               result.push.apply( result, ret );
-                                       }
-                                       return false;
-                               }
-                       } else if ( Expr.match.POS.test( match[0] ) || Expr.match.CHILD.test( match[0] ) ) {
-                               return true;
-                       }
-                       
-                       return match;
-               },
-               POS: function(match){
-                       match.unshift( true );
-                       return match;
-               }
-       },
-       filters: {
-               enabled: function(elem){
-                       return elem.disabled === false && elem.type !== "hidden";
-               },
-               disabled: function(elem){
-                       return elem.disabled === true;
-               },
-               checked: function(elem){
-                       return elem.checked === true;
-               },
-               selected: function(elem){
-                       // Accessing this property makes selected-by-default
-                       // options in Safari work properly
-                       elem.parentNode.selectedIndex;
-                       return elem.selected === true;
-               },
-               parent: function(elem){
-                       return !!elem.firstChild;
-               },
-               empty: function(elem){
-                       return !elem.firstChild;
-               },
-               has: function(elem, i, match){
-                       return !!Sizzle( match[3], elem ).length;
-               },
-               header: function(elem){
-                       return /h\d/i.test( elem.nodeName );
-               },
-               text: function(elem){
-                       return "text" === elem.type;
-               },
-               radio: function(elem){
-                       return "radio" === elem.type;
-               },
-               checkbox: function(elem){
-                       return "checkbox" === elem.type;
-               },
-               file: function(elem){
-                       return "file" === elem.type;
-               },
-               password: function(elem){
-                       return "password" === elem.type;
-               },
-               submit: function(elem){
-                       return "submit" === elem.type;
-               },
-               image: function(elem){
-                       return "image" === elem.type;
-               },
-               reset: function(elem){
-                       return "reset" === elem.type;
-               },
-               button: function(elem){
-                       return "button" === elem.type || elem.nodeName.toLowerCase() === "button";
-               },
-               input: function(elem){
-                       return /input|select|textarea|button/i.test(elem.nodeName);
-               }
-       },
-       setFilters: {
-               first: function(elem, i){
-                       return i === 0;
-               },
-               last: function(elem, i, match, array){
-                       return i === array.length - 1;
-               },
-               even: function(elem, i){
-                       return i % 2 === 0;
-               },
-               odd: function(elem, i){
-                       return i % 2 === 1;
-               },
-               lt: function(elem, i, match){
-                       return i < match[3] - 0;
-               },
-               gt: function(elem, i, match){
-                       return i > match[3] - 0;
-               },
-               nth: function(elem, i, match){
-                       return match[3] - 0 === i;
-               },
-               eq: function(elem, i, match){
-                       return match[3] - 0 === i;
-               }
-       },
-       filter: {
-               PSEUDO: function(elem, match, i, array){
-                       var name = match[1], filter = Expr.filters[ name ];
-
-                       if ( filter ) {
-                               return filter( elem, i, match, array );
-                       } else if ( name === "contains" ) {
-                               return (elem.textContent || elem.innerText || getText([ elem ]) || "").indexOf(match[3]) >= 0;
-                       } else if ( name === "not" ) {
-                               var not = match[3];
-
-                               for ( var i = 0, l = not.length; i < l; i++ ) {
-                                       if ( not[i] === elem ) {
-                                               return false;
-                                       }
-                               }
-
-                               return true;
-                       } else {
-                               Sizzle.error( "Syntax error, unrecognized expression: " + name );
-                       }
-               },
-               CHILD: function(elem, match){
-                       var type = match[1], node = elem;
-                       switch (type) {
-                               case 'only':
-                               case 'first':
-                                       while ( (node = node.previousSibling) )  {
-                                               if ( node.nodeType === 1 ) { 
-                                                       return false; 
-                                               }
-                                       }
-                                       if ( type === "first" ) { 
-                                               return true; 
-                                       }
-                                       node = elem;
-                               case 'last':
-                                       while ( (node = node.nextSibling) )      {
-                                               if ( node.nodeType === 1 ) { 
-                                                       return false; 
-                                               }
-                                       }
-                                       return true;
-                               case 'nth':
-                                       var first = match[2], last = match[3];
-
-                                       if ( first === 1 && last === 0 ) {
-                                               return true;
-                                       }
-                                       
-                                       var doneName = match[0],
-                                               parent = elem.parentNode;
-       
-                                       if ( parent && (parent.sizcache !== doneName || !elem.nodeIndex) ) {
-                                               var count = 0;
-                                               for ( node = parent.firstChild; node; node = node.nextSibling ) {
-                                                       if ( node.nodeType === 1 ) {
-                                                               node.nodeIndex = ++count;
-                                                       }
-                                               } 
-                                               parent.sizcache = doneName;
-                                       }
-                                       
-                                       var diff = elem.nodeIndex - last;
-                                       if ( first === 0 ) {
-                                               return diff === 0;
-                                       } else {
-                                               return ( diff % first === 0 && diff / first >= 0 );
-                                       }
-                       }
-               },
-               ID: function(elem, match){
-                       return elem.nodeType === 1 && elem.getAttribute("id") === match;
-               },
-               TAG: function(elem, match){
-                       return (match === "*" && elem.nodeType === 1) || elem.nodeName.toLowerCase() === match;
-               },
-               CLASS: function(elem, match){
-                       return (" " + (elem.className || elem.getAttribute("class")) + " ")
-                               .indexOf( match ) > -1;
-               },
-               ATTR: function(elem, match){
-                       var name = match[1],
-                               result = Expr.attrHandle[ name ] ?
-                                       Expr.attrHandle[ name ]( elem ) :
-                                       elem[ name ] != null ?
-                                               elem[ name ] :
-                                               elem.getAttribute( name ),
-                               value = result + "",
-                               type = match[2],
-                               check = match[4];
-
-                       return result == null ?
-                               type === "!=" :
-                               type === "=" ?
-                               value === check :
-                               type === "*=" ?
-                               value.indexOf(check) >= 0 :
-                               type === "~=" ?
-                               (" " + value + " ").indexOf(check) >= 0 :
-                               !check ?
-                               value && result !== false :
-                               type === "!=" ?
-                               value !== check :
-                               type === "^=" ?
-                               value.indexOf(check) === 0 :
-                               type === "$=" ?
-                               value.substr(value.length - check.length) === check :
-                               type === "|=" ?
-                               value === check || value.substr(0, check.length + 1) === check + "-" :
-                               false;
-               },
-               POS: function(elem, match, i, array){
-                       var name = match[2], filter = Expr.setFilters[ name ];
-
-                       if ( filter ) {
-                               return filter( elem, i, match, array );
-                       }
-               }
-       }
-};
-
-var origPOS = Expr.match.POS;
-
-for ( var type in Expr.match ) {
-       Expr.match[ type ] = new RegExp( Expr.match[ type ].source + /(?![^\[]*\])(?![^\(]*\))/.source );
-       Expr.leftMatch[ type ] = new RegExp( /(^(?:.|\r|\n)*?)/.source + Expr.match[ type ].source.replace(/\\(\d+)/g, function(all, num){
-               return "\\" + (num - 0 + 1);
-       }));
-}
-
-var makeArray = function(array, results) {
-       array = Array.prototype.slice.call( array, 0 );
-
-       if ( results ) {
-               results.push.apply( results, array );
-               return results;
-       }
-       
-       return array;
-};
-
-// Perform a simple check to determine if the browser is capable of
-// converting a NodeList to an array using builtin methods.
-// Also verifies that the returned array holds DOM nodes
-// (which is not the case in the Blackberry browser)
-try {
-       Array.prototype.slice.call( document.documentElement.childNodes, 0 )[0].nodeType;
-
-// Provide a fallback method if it does not work
-} catch(e){
-       makeArray = function(array, results) {
-               var ret = results || [];
-
-               if ( toString.call(array) === "[object Array]" ) {
-                       Array.prototype.push.apply( ret, array );
-               } else {
-                       if ( typeof array.length === "number" ) {
-                               for ( var i = 0, l = array.length; i < l; i++ ) {
-                                       ret.push( array[i] );
-                               }
-                       } else {
-                               for ( var i = 0; array[i]; i++ ) {
-                                       ret.push( array[i] );
-                               }
-                       }
-               }
-
-               return ret;
-       };
-}
-
-var sortOrder;
-
-if ( document.documentElement.compareDocumentPosition ) {
-       sortOrder = function( a, b ) {
-               if ( !a.compareDocumentPosition || !b.compareDocumentPosition ) {
-                       if ( a == b ) {
-                               hasDuplicate = true;
-                       }
-                       return a.compareDocumentPosition ? -1 : 1;
-               }
-
-               var ret = a.compareDocumentPosition(b) & 4 ? -1 : a === b ? 0 : 1;
-               if ( ret === 0 ) {
-                       hasDuplicate = true;
-               }
-               return ret;
-       };
-} else if ( "sourceIndex" in document.documentElement ) {
-       sortOrder = function( a, b ) {
-               if ( !a.sourceIndex || !b.sourceIndex ) {
-                       if ( a == b ) {
-                               hasDuplicate = true;
-                       }
-                       return a.sourceIndex ? -1 : 1;
-               }
-
-               var ret = a.sourceIndex - b.sourceIndex;
-               if ( ret === 0 ) {
-                       hasDuplicate = true;
-               }
-               return ret;
-       };
-} else if ( document.createRange ) {
-       sortOrder = function( a, b ) {
-               if ( !a.ownerDocument || !b.ownerDocument ) {
-                       if ( a == b ) {
-                               hasDuplicate = true;
-                       }
-                       return a.ownerDocument ? -1 : 1;
-               }
-
-               var aRange = a.ownerDocument.createRange(), bRange = b.ownerDocument.createRange();
-               aRange.setStart(a, 0);
-               aRange.setEnd(a, 0);
-               bRange.setStart(b, 0);
-               bRange.setEnd(b, 0);
-               var ret = aRange.compareBoundaryPoints(Range.START_TO_END, bRange);
-               if ( ret === 0 ) {
-                       hasDuplicate = true;
-               }
-               return ret;
-       };
-}
-
-// Utility function for retreiving the text value of an array of DOM nodes
-function getText( elems ) {
-       var ret = "", elem;
-
-       for ( var i = 0; elems[i]; i++ ) {
-               elem = elems[i];
-
-               // Get the text from text nodes and CDATA nodes
-               if ( elem.nodeType === 3 || elem.nodeType === 4 ) {
-                       ret += elem.nodeValue;
-
-               // Traverse everything else, except comment nodes
-               } else if ( elem.nodeType !== 8 ) {
-                       ret += getText( elem.childNodes );
-               }
-       }
-
-       return ret;
-}
-
-// Check to see if the browser returns elements by name when
-// querying by getElementById (and provide a workaround)
-(function(){
-       // We're going to inject a fake input element with a specified name
-       var form = document.createElement("div"),
-               id = "script" + (new Date).getTime();
-       form.innerHTML = "<a name='" + id + "'/>";
-
-       // Inject it into the root element, check its status, and remove it quickly
-       var root = document.documentElement;
-       root.insertBefore( form, root.firstChild );
-
-       // The workaround has to do additional checks after a getElementById
-       // Which slows things down for other browsers (hence the branching)
-       if ( document.getElementById( id ) ) {
-               Expr.find.ID = function(match, context, isXML){
-                       if ( typeof context.getElementById !== "undefined" && !isXML ) {
-                               var m = context.getElementById(match[1]);
-                               return m ? m.id === match[1] || typeof m.getAttributeNode !== "undefined" && m.getAttributeNode("id").nodeValue === match[1] ? [m] : undefined : [];
-                       }
-               };
-
-               Expr.filter.ID = function(elem, match){
-                       var node = typeof elem.getAttributeNode !== "undefined" && elem.getAttributeNode("id");
-                       return elem.nodeType === 1 && node && node.nodeValue === match;
-               };
-       }
-
-       root.removeChild( form );
-       root = form = null; // release memory in IE
-})();
-
-(function(){
-       // Check to see if the browser returns only elements
-       // when doing getElementsByTagName("*")
-
-       // Create a fake element
-       var div = document.createElement("div");
-       div.appendChild( document.createComment("") );
-
-       // Make sure no comments are found
-       if ( div.getElementsByTagName("*").length > 0 ) {
-               Expr.find.TAG = function(match, context){
-                       var results = context.getElementsByTagName(match[1]);
-
-                       // Filter out possible comments
-                       if ( match[1] === "*" ) {
-                               var tmp = [];
-
-                               for ( var i = 0; results[i]; i++ ) {
-                                       if ( results[i].nodeType === 1 ) {
-                                               tmp.push( results[i] );
-                                       }
-                               }
-
-                               results = tmp;
-                       }
-
-                       return results;
-               };
-       }
-
-       // Check to see if an attribute returns normalized href attributes
-       div.innerHTML = "<a href='#'></a>";
-       if ( div.firstChild && typeof div.firstChild.getAttribute !== "undefined" &&
-                       div.firstChild.getAttribute("href") !== "#" ) {
-               Expr.attrHandle.href = function(elem){
-                       return elem.getAttribute("href", 2);
-               };
-       }
-
-       div = null; // release memory in IE
-})();
-
-if ( document.querySelectorAll ) {
-       (function(){
-               var oldSizzle = Sizzle, div = document.createElement("div");
-               div.innerHTML = "<p class='TEST'></p>";
-
-               // Safari can't handle uppercase or unicode characters when
-               // in quirks mode.
-               if ( div.querySelectorAll && div.querySelectorAll(".TEST").length === 0 ) {
-                       return;
-               }
-       
-               Sizzle = function(query, context, extra, seed){
-                       context = context || document;
-
-                       // Only use querySelectorAll on non-XML documents
-                       // (ID selectors don't work in non-HTML documents)
-                       if ( !seed && context.nodeType === 9 && !isXML(context) ) {
-                               try {
-                                       return makeArray( context.querySelectorAll(query), extra );
-                               } catch(e){}
-                       }
-               
-                       return oldSizzle(query, context, extra, seed);
-               };
-
-               for ( var prop in oldSizzle ) {
-                       Sizzle[ prop ] = oldSizzle[ prop ];
-               }
-
-               div = null; // release memory in IE
-       })();
-}
-
-(function(){
-       var div = document.createElement("div");
-
-       div.innerHTML = "<div class='test e'></div><div class='test'></div>";
-
-       // Opera can't find a second classname (in 9.6)
-       // Also, make sure that getElementsByClassName actually exists
-       if ( !div.getElementsByClassName || div.getElementsByClassName("e").length === 0 ) {
-               return;
-       }
-
-       // Safari caches class attributes, doesn't catch changes (in 3.2)
-       div.lastChild.className = "e";
-
-       if ( div.getElementsByClassName("e").length === 1 ) {
-               return;
-       }
-       
-       Expr.order.splice(1, 0, "CLASS");
-       Expr.find.CLASS = function(match, context, isXML) {
-               if ( typeof context.getElementsByClassName !== "undefined" && !isXML ) {
-                       return context.getElementsByClassName(match[1]);
-               }
-       };
-
-       div = null; // release memory in IE
-})();
-
-function dirNodeCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) {
-       for ( var i = 0, l = checkSet.length; i < l; i++ ) {
-               var elem = checkSet[i];
-               if ( elem ) {
-                       elem = elem[dir];
-                       var match = false;
-
-                       while ( elem ) {
-                               if ( elem.sizcache === doneName ) {
-                                       match = checkSet[elem.sizset];
-                                       break;
-                               }
-
-                               if ( elem.nodeType === 1 && !isXML ){
-                                       elem.sizcache = doneName;
-                                       elem.sizset = i;
-                               }
-
-                               if ( elem.nodeName.toLowerCase() === cur ) {
-                                       match = elem;
-                                       break;
-                               }
-
-                               elem = elem[dir];
-                       }
-
-                       checkSet[i] = match;
-               }
-       }
-}
-
-function dirCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) {
-       for ( var i = 0, l = checkSet.length; i < l; i++ ) {
-               var elem = checkSet[i];
-               if ( elem ) {
-                       elem = elem[dir];
-                       var match = false;
-
-                       while ( elem ) {
-                               if ( elem.sizcache === doneName ) {
-                                       match = checkSet[elem.sizset];
-                                       break;
-                               }
-
-                               if ( elem.nodeType === 1 ) {
-                                       if ( !isXML ) {
-                                               elem.sizcache = doneName;
-                                               elem.sizset = i;
-                                       }
-                                       if ( typeof cur !== "string" ) {
-                                               if ( elem === cur ) {
-                                                       match = true;
-                                                       break;
-                                               }
-
-                                       } else if ( Sizzle.filter( cur, [elem] ).length > 0 ) {
-                                               match = elem;
-                                               break;
-                                       }
-                               }
-
-                               elem = elem[dir];
-                       }
-
-                       checkSet[i] = match;
-               }
-       }
-}
-
-var contains = document.compareDocumentPosition ? function(a, b){
-       return !!(a.compareDocumentPosition(b) & 16);
-} : function(a, b){
-       return a !== b && (a.contains ? a.contains(b) : true);
-};
-
-var isXML = function(elem){
-       // documentElement is verified for cases where it doesn't yet exist
-       // (such as loading iframes in IE - #4833) 
-       var documentElement = (elem ? elem.ownerDocument || elem : 0).documentElement;
-       return documentElement ? documentElement.nodeName !== "HTML" : false;
-};
-
-var posProcess = function(selector, context){
-       var tmpSet = [], later = "", match,
-               root = context.nodeType ? [context] : context;
-
-       // Position selectors must be done after the filter
-       // And so must :not(positional) so we move all PSEUDOs to the end
-       while ( (match = Expr.match.PSEUDO.exec( selector )) ) {
-               later += match[0];
-               selector = selector.replace( Expr.match.PSEUDO, "" );
-       }
-
-       selector = Expr.relative[selector] ? selector + "*" : selector;
-
-       for ( var i = 0, l = root.length; i < l; i++ ) {
-               Sizzle( selector, root[i], tmpSet );
-       }
-
-       return Sizzle.filter( later, tmpSet );
-};
-
-// EXPOSE
-jQuery.find = Sizzle;
-jQuery.expr = Sizzle.selectors;
-jQuery.expr[":"] = jQuery.expr.filters;
-jQuery.unique = Sizzle.uniqueSort;
-jQuery.text = getText;
-jQuery.isXMLDoc = isXML;
-jQuery.contains = contains;
-
-return;
-
-window.Sizzle = Sizzle;
-
-})();
-var runtil = /Until$/,
-       rparentsprev = /^(?:parents|prevUntil|prevAll)/,
-       // Note: This RegExp should be improved, or likely pulled from Sizzle
-       rmultiselector = /,/,
-       slice = Array.prototype.slice;
-
-// Implement the identical functionality for filter and not
-var winnow = function( elements, qualifier, keep ) {
-       if ( jQuery.isFunction( qualifier ) ) {
-               return jQuery.grep(elements, function( elem, i ) {
-                       return !!qualifier.call( elem, i, elem ) === keep;
-               });
-
-       } else if ( qualifier.nodeType ) {
-               return jQuery.grep(elements, function( elem, i ) {
-                       return (elem === qualifier) === keep;
-               });
-
-       } else if ( typeof qualifier === "string" ) {
-               var filtered = jQuery.grep(elements, function( elem ) {
-                       return elem.nodeType === 1;
-               });
-
-               if ( isSimple.test( qualifier ) ) {
-                       return jQuery.filter(qualifier, filtered, !keep);
-               } else {
-                       qualifier = jQuery.filter( qualifier, filtered );
-               }
-       }
-
-       return jQuery.grep(elements, function( elem, i ) {
-               return (jQuery.inArray( elem, qualifier ) >= 0) === keep;
-       });
-};
-
-jQuery.fn.extend({
-       find: function( selector ) {
-               var ret = this.pushStack( "", "find", selector ), length = 0;
-
-               for ( var i = 0, l = this.length; i < l; i++ ) {
-                       length = ret.length;
-                       jQuery.find( selector, this[i], ret );
-
-                       if ( i > 0 ) {
-                               // Make sure that the results are unique
-                               for ( var n = length; n < ret.length; n++ ) {
-                                       for ( var r = 0; r < length; r++ ) {
-                                               if ( ret[r] === ret[n] ) {
-                                                       ret.splice(n--, 1);
-                                                       break;
-                                               }
-                                       }
-                               }
-                       }
-               }
-
-               return ret;
-       },
-
-       has: function( target ) {
-               var targets = jQuery( target );
-               return this.filter(function() {
-                       for ( var i = 0, l = targets.length; i < l; i++ ) {
-                               if ( jQuery.contains( this, targets[i] ) ) {
-                                       return true;
-                               }
-                       }
-               });
-       },
-
-       not: function( selector ) {
-               return this.pushStack( winnow(this, selector, false), "not", selector);
-       },
-
-       filter: function( selector ) {
-               return this.pushStack( winnow(this, selector, true), "filter", selector );
-       },
-       
-       is: function( selector ) {
-               return !!selector && jQuery.filter( selector, this ).length > 0;
-       },
-
-       closest: function( selectors, context ) {
-               if ( jQuery.isArray( selectors ) ) {
-                       var ret = [], cur = this[0], match, matches = {}, selector;
-
-                       if ( cur && selectors.length ) {
-                               for ( var i = 0, l = selectors.length; i < l; i++ ) {
-                                       selector = selectors[i];
-
-                                       if ( !matches[selector] ) {
-                                               matches[selector] = jQuery.expr.match.POS.test( selector ) ? 
-                                                       jQuery( selector, context || this.context ) :
-                                                       selector;
-                                       }
-                               }
-
-                               while ( cur && cur.ownerDocument && cur !== context ) {
-                                       for ( selector in matches ) {
-                                               match = matches[selector];
-
-                                               if ( match.jquery ? match.index(cur) > -1 : jQuery(cur).is(match) ) {
-                                                       ret.push({ selector: selector, elem: cur });
-                                                       delete matches[selector];
-                                               }
-                                       }
-                                       cur = cur.parentNode;
-                               }
-                       }
-
-                       return ret;
-               }
-
-               var pos = jQuery.expr.match.POS.test( selectors ) ? 
-                       jQuery( selectors, context || this.context ) : null;
-
-               return this.map(function( i, cur ) {
-                       while ( cur && cur.ownerDocument && cur !== context ) {
-                               if ( pos ? pos.index(cur) > -1 : jQuery(cur).is(selectors) ) {
-                                       return cur;
-                               }
-                               cur = cur.parentNode;
-                       }
-                       return null;
-               });
-       },
-       
-       // Determine the position of an element within
-       // the matched set of elements
-       index: function( elem ) {
-               if ( !elem || typeof elem === "string" ) {
-                       return jQuery.inArray( this[0],
-                               // If it receives a string, the selector is used
-                               // If it receives nothing, the siblings are used
-                               elem ? jQuery( elem ) : this.parent().children() );
-               }
-               // Locate the position of the desired element
-               return jQuery.inArray(
-                       // If it receives a jQuery object, the first element is used
-                       elem.jquery ? elem[0] : elem, this );
-       },
-
-       add: function( selector, context ) {
-               var set = typeof selector === "string" ?
-                               jQuery( selector, context || this.context ) :
-                               jQuery.makeArray( selector ),
-                       all = jQuery.merge( this.get(), set );
-
-               return this.pushStack( isDisconnected( set[0] ) || isDisconnected( all[0] ) ?
-                       all :
-                       jQuery.unique( all ) );
-       },
-
-       andSelf: function() {
-               return this.add( this.prevObject );
-       }
-});
-
-// A painfully simple check to see if an element is disconnected
-// from a document (should be improved, where feasible).
-function isDisconnected( node ) {
-       return !node || !node.parentNode || node.parentNode.nodeType === 11;
-}
-
-jQuery.each({
-       parent: function( elem ) {
-               var parent = elem.parentNode;
-               return parent && parent.nodeType !== 11 ? parent : null;
-       },
-       parents: function( elem ) {
-               return jQuery.dir( elem, "parentNode" );
-       },
-       parentsUntil: function( elem, i, until ) {
-               return jQuery.dir( elem, "parentNode", until );
-       },
-       next: function( elem ) {
-               return jQuery.nth( elem, 2, "nextSibling" );
-       },
-       prev: function( elem ) {
-               return jQuery.nth( elem, 2, "previousSibling" );
-       },
-       nextAll: function( elem ) {
-               return jQuery.dir( elem, "nextSibling" );
-       },
-       prevAll: function( elem ) {
-               return jQuery.dir( elem, "previousSibling" );
-       },
-       nextUntil: function( elem, i, until ) {
-               return jQuery.dir( elem, "nextSibling", until );
-       },
-       prevUntil: function( elem, i, until ) {
-               return jQuery.dir( elem, "previousSibling", until );
-       },
-       siblings: function( elem ) {
-               return jQuery.sibling( elem.parentNode.firstChild, elem );
-       },
-       children: function( elem ) {
-               return jQuery.sibling( elem.firstChild );
-       },
-       contents: function( elem ) {
-               return jQuery.nodeName( elem, "iframe" ) ?
-                       elem.contentDocument || elem.contentWindow.document :
-                       jQuery.makeArray( elem.childNodes );
-       }
-}, function( name, fn ) {
-       jQuery.fn[ name ] = function( until, selector ) {
-               var ret = jQuery.map( this, fn, until );
-               
-               if ( !runtil.test( name ) ) {
-                       selector = until;
-               }
-
-               if ( selector && typeof selector === "string" ) {
-                       ret = jQuery.filter( selector, ret );
-               }
-
-               ret = this.length > 1 ? jQuery.unique( ret ) : ret;
-
-               if ( (this.length > 1 || rmultiselector.test( selector )) && rparentsprev.test( name ) ) {
-                       ret = ret.reverse();
-               }
-
-               return this.pushStack( ret, name, slice.call(arguments).join(",") );
-       };
-});
-
-jQuery.extend({
-       filter: function( expr, elems, not ) {
-               if ( not ) {
-                       expr = ":not(" + expr + ")";
-               }
-
-               return jQuery.find.matches(expr, elems);
-       },
-       
-       dir: function( elem, dir, until ) {
-               var matched = [], cur = elem[dir];
-               while ( cur && cur.nodeType !== 9 && (until === undefined || cur.nodeType !== 1 || !jQuery( cur ).is( until )) ) {
-                       if ( cur.nodeType === 1 ) {
-                               matched.push( cur );
-                       }
-                       cur = cur[dir];
-               }
-               return matched;
-       },
-
-       nth: function( cur, result, dir, elem ) {
-               result = result || 1;
-               var num = 0;
-
-               for ( ; cur; cur = cur[dir] ) {
-                       if ( cur.nodeType === 1 && ++num === result ) {
-                               break;
-                       }
-               }
-
-               return cur;
-       },
-
-       sibling: function( n, elem ) {
-               var r = [];
-
-               for ( ; n; n = n.nextSibling ) {
-                       if ( n.nodeType === 1 && n !== elem ) {
-                               r.push( n );
-                       }
-               }
-
-               return r;
-       }
-});
-var rinlinejQuery = / jQuery\d+="(?:\d+|null)"/g,
-       rleadingWhitespace = /^\s+/,
-       rxhtmlTag = /(<([\w:]+)[^>]*?)\/>/g,
-       rselfClosing = /^(?:area|br|col|embed|hr|img|input|link|meta|param)$/i,
-       rtagName = /<([\w:]+)/,
-       rtbody = /<tbody/i,
-       rhtml = /<|&#?\w+;/,
-       rnocache = /<script|<object|<embed|<option|<style/i,
-       rchecked = /checked\s*(?:[^=]|=\s*.checked.)/i,  // checked="checked" or checked (html5)
-       fcloseTag = function( all, front, tag ) {
-               return rselfClosing.test( tag ) ?
-                       all :
-                       front + "></" + tag + ">";
-       },
-       wrapMap = {
-               option: [ 1, "<select multiple='multiple'>", "</select>" ],
-               legend: [ 1, "<fieldset>", "</fieldset>" ],
-               thead: [ 1, "<table>", "</table>" ],
-               tr: [ 2, "<table><tbody>", "</tbody></table>" ],
-               td: [ 3, "<table><tbody><tr>", "</tr></tbody></table>" ],
-               col: [ 2, "<table><tbody></tbody><colgroup>", "</colgroup></table>" ],
-               area: [ 1, "<map>", "</map>" ],
-               _default: [ 0, "", "" ]
-       };
-
-wrapMap.optgroup = wrapMap.option;
-wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead;
-wrapMap.th = wrapMap.td;
-
-// IE can't serialize <link> and <script> tags normally
-if ( !jQuery.support.htmlSerialize ) {
-       wrapMap._default = [ 1, "div<div>", "</div>" ];
-}
-
-jQuery.fn.extend({
-       text: function( text ) {
-               if ( jQuery.isFunction(text) ) {
-                       return this.each(function(i) {
-                               var self = jQuery(this);
-                               self.text( text.call(this, i, self.text()) );
-                       });
-               }
-
-               if ( typeof text !== "object" && text !== undefined ) {
-                       return this.empty().append( (this[0] && this[0].ownerDocument || document).createTextNode( text ) );
-               }
-
-               return jQuery.text( this );
-       },
-
-       wrapAll: function( html ) {
-               if ( jQuery.isFunction( html ) ) {
-                       return this.each(function(i) {
-                               jQuery(this).wrapAll( html.call(this, i) );
-                       });
-               }
-
-               if ( this[0] ) {
-                       // The elements to wrap the target around
-                       var wrap = jQuery( html, this[0].ownerDocument ).eq(0).clone(true);
-
-                       if ( this[0].parentNode ) {
-                               wrap.insertBefore( this[0] );
-                       }
-
-                       wrap.map(function() {
-                               var elem = this;
-
-                               while ( elem.firstChild && elem.firstChild.nodeType === 1 ) {
-                                       elem = elem.firstChild;
-                               }
-
-                               return elem;
-                       }).append(this);
-               }
-
-               return this;
-       },
-
-       wrapInner: function( html ) {
-               if ( jQuery.isFunction( html ) ) {
-                       return this.each(function(i) {
-                               jQuery(this).wrapInner( html.call(this, i) );
-                       });
-               }
-
-               return this.each(function() {
-                       var self = jQuery( this ), contents = self.contents();
-
-                       if ( contents.length ) {
-                               contents.wrapAll( html );
-
-                       } else {
-                               self.append( html );
-                       }
-               });
-       },
-
-       wrap: function( html ) {
-               return this.each(function() {
-                       jQuery( this ).wrapAll( html );
-               });
-       },
-
-       unwrap: function() {
-               return this.parent().each(function() {
-                       if ( !jQuery.nodeName( this, "body" ) ) {
-                               jQuery( this ).replaceWith( this.childNodes );
-                       }
-               }).end();
-       },
-
-       append: function() {
-               return this.domManip(arguments, true, function( elem ) {
-                       if ( this.nodeType === 1 ) {
-                               this.appendChild( elem );
-                       }
-               });
-       },
-
-       prepend: function() {
-               return this.domManip(arguments, true, function( elem ) {
-                       if ( this.nodeType === 1 ) {
-                               this.insertBefore( elem, this.firstChild );
-                       }
-               });
-       },
-
-       before: function() {
-               if ( this[0] && this[0].parentNode ) {
-                       return this.domManip(arguments, false, function( elem ) {
-                               this.parentNode.insertBefore( elem, this );
-                       });
-               } else if ( arguments.length ) {
-                       var set = jQuery(arguments[0]);
-                       set.push.apply( set, this.toArray() );
-                       return this.pushStack( set, "before", arguments );
-               }
-       },
-
-       after: function() {
-               if ( this[0] && this[0].parentNode ) {
-                       return this.domManip(arguments, false, function( elem ) {
-                               this.parentNode.insertBefore( elem, this.nextSibling );
-                       });
-               } else if ( arguments.length ) {
-                       var set = this.pushStack( this, "after", arguments );
-                       set.push.apply( set, jQuery(arguments[0]).toArray() );
-                       return set;
-               }
-       },
-       
-       // keepData is for internal use only--do not document
-       remove: function( selector, keepData ) {
-               for ( var i = 0, elem; (elem = this[i]) != null; i++ ) {
-                       if ( !selector || jQuery.filter( selector, [ elem ] ).length ) {
-                               if ( !keepData && elem.nodeType === 1 ) {
-                                       jQuery.cleanData( elem.getElementsByTagName("*") );
-                                       jQuery.cleanData( [ elem ] );
-                               }
-
-                               if ( elem.parentNode ) {
-                                        elem.parentNode.removeChild( elem );
-                               }
-                       }
-               }
-               
-               return this;
-       },
-
-       empty: function() {
-               for ( var i = 0, elem; (elem = this[i]) != null; i++ ) {
-                       // Remove element nodes and prevent memory leaks
-                       if ( elem.nodeType === 1 ) {
-                               jQuery.cleanData( elem.getElementsByTagName("*") );
-                       }
-
-                       // Remove any remaining nodes
-                       while ( elem.firstChild ) {
-                               elem.removeChild( elem.firstChild );
-                       }
-               }
-               
-               return this;
-       },
-
-       clone: function( events ) {
-               // Do the clone
-               var ret = this.map(function() {
-                       if ( !jQuery.support.noCloneEvent && !jQuery.isXMLDoc(this) ) {
-                               // IE copies events bound via attachEvent when
-                               // using cloneNode. Calling detachEvent on the
-                               // clone will also remove the events from the orignal
-                               // In order to get around this, we use innerHTML.
-                               // Unfortunately, this means some modifications to
-                               // attributes in IE that are actually only stored
-                               // as properties will not be copied (such as the
-                               // the name attribute on an input).
-                               var html = this.outerHTML, ownerDocument = this.ownerDocument;
-                               if ( !html ) {
-                                       var div = ownerDocument.createElement("div");
-                                       div.appendChild( this.cloneNode(true) );
-                                       html = div.innerHTML;
-                               }
-
-                               return jQuery.clean([html.replace(rinlinejQuery, "")
-                                       // Handle the case in IE 8 where action=/test/> self-closes a tag
-                                       .replace(/=([^="'>\s]+\/)>/g, '="$1">')
-                                       .replace(rleadingWhitespace, "")], ownerDocument)[0];
-                       } else {
-                               return this.cloneNode(true);
-                       }
-               });
-
-               // Copy the events from the original to the clone
-               if ( events === true ) {
-                       cloneCopyEvent( this, ret );
-                       cloneCopyEvent( this.find("*"), ret.find("*") );
-               }
-
-               // Return the cloned set
-               return ret;
-       },
-
-       html: function( value ) {
-               if ( value === undefined ) {
-                       return this[0] && this[0].nodeType === 1 ?
-                               this[0].innerHTML.replace(rinlinejQuery, "") :
-                               null;
-
-               // See if we can take a shortcut and just use innerHTML
-               } else if ( typeof value === "string" && !rnocache.test( value ) &&
-                       (jQuery.support.leadingWhitespace || !rleadingWhitespace.test( value )) &&
-                       !wrapMap[ (rtagName.exec( value ) || ["", ""])[1].toLowerCase() ] ) {
-
-                       value = value.replace(rxhtmlTag, fcloseTag);
-
-                       try {
-                               for ( var i = 0, l = this.length; i < l; i++ ) {
-                                       // Remove element nodes and prevent memory leaks
-                                       if ( this[i].nodeType === 1 ) {
-                                               jQuery.cleanData( this[i].getElementsByTagName("*") );
-                                               this[i].innerHTML = value;
-                                       }
-                               }
-
-                       // If using innerHTML throws an exception, use the fallback method
-                       } catch(e) {
-                               this.empty().append( value );
-                       }
-
-               } else if ( jQuery.isFunction( value ) ) {
-                       this.each(function(i){
-                               var self = jQuery(this), old = self.html();
-                               self.empty().append(function(){
-                                       return value.call( this, i, old );
-                               });
-                       });
-
-               } else {
-                       this.empty().append( value );
-               }
-
-               return this;
-       },
-
-       replaceWith: function( value ) {
-               if ( this[0] && this[0].parentNode ) {
-                       // Make sure that the elements are removed from the DOM before they are inserted
-                       // this can help fix replacing a parent with child elements
-                       if ( jQuery.isFunction( value ) ) {
-                               return this.each(function(i) {
-                                       var self = jQuery(this), old = self.html();
-                                       self.replaceWith( value.call( this, i, old ) );
-                               });
-                       }
-
-                       if ( typeof value !== "string" ) {
-                               value = jQuery(value).detach();
-                       }
-
-                       return this.each(function() {
-                               var next = this.nextSibling, parent = this.parentNode;
-
-                               jQuery(this).remove();
-
-                               if ( next ) {
-                                       jQuery(next).before( value );
-                               } else {
-                                       jQuery(parent).append( value );
-                               }
-                       });
-               } else {
-                       return this.pushStack( jQuery(jQuery.isFunction(value) ? value() : value), "replaceWith", value );
-               }
-       },
-
-       detach: function( selector ) {
-               return this.remove( selector, true );
-       },
-
-       domManip: function( args, table, callback ) {
-               var results, first, value = args[0], scripts = [], fragment, parent;
-
-               // We can't cloneNode fragments that contain checked, in WebKit
-               if ( !jQuery.support.checkClone && arguments.length === 3 && typeof value === "string" && rchecked.test( value ) ) {
-                       return this.each(function() {
-                               jQuery(this).domManip( args, table, callback, true );
-                       });
-               }
-
-               if ( jQuery.isFunction(value) ) {
-                       return this.each(function(i) {
-                               var self = jQuery(this);
-                               args[0] = value.call(this, i, table ? self.html() : undefined);
-                               self.domManip( args, table, callback );
-                       });
-               }
-
-               if ( this[0] ) {
-                       parent = value && value.parentNode;
-
-                       // If we're in a fragment, just use that instead of building a new one
-                       if ( jQuery.support.parentNode && parent && parent.nodeType === 11 && parent.childNodes.length === this.length ) {
-                               results = { fragment: parent };
-
-                       } else {
-                               results = buildFragment( args, this, scripts );
-                       }
-                       
-                       fragment = results.fragment;
-                       
-                       if ( fragment.childNodes.length === 1 ) {
-                               first = fragment = fragment.firstChild;
-                       } else {
-                               first = fragment.firstChild;
-                       }
-
-                       if ( first ) {
-                               table = table && jQuery.nodeName( first, "tr" );
-
-                               for ( var i = 0, l = this.length; i < l; i++ ) {
-                                       callback.call(
-                                               table ?
-                                                       root(this[i], first) :
-                                                       this[i],
-                                               i > 0 || results.cacheable || this.length > 1  ?
-                                                       fragment.cloneNode(true) :
-                                                       fragment
-                                       );
-                               }
-                       }
-
-                       if ( scripts.length ) {
-                               jQuery.each( scripts, evalScript );
-                       }
-               }
-
-               return this;
-
-               function root( elem, cur ) {
-                       return jQuery.nodeName(elem, "table") ?
-                               (elem.getElementsByTagName("tbody")[0] ||
-                               elem.appendChild(elem.ownerDocument.createElement("tbody"))) :
-                               elem;
-               }
-       }
-});
-
-function cloneCopyEvent(orig, ret) {
-       var i = 0;
-
-       ret.each(function() {
-               if ( this.nodeName !== (orig[i] && orig[i].nodeName) ) {
-                       return;
-               }
-
-               var oldData = jQuery.data( orig[i++] ), curData = jQuery.data( this, oldData ), events = oldData && oldData.events;
-
-               if ( events ) {
-                       delete curData.handle;
-                       curData.events = {};
-
-                       for ( var type in events ) {
-                               for ( var handler in events[ type ] ) {
-                                       jQuery.event.add( this, type, events[ type ][ handler ], events[ type ][ handler ].data );
-                               }
-                       }
-               }
-       });
-}
-
-function buildFragment( args, nodes, scripts ) {
-       var fragment, cacheable, cacheresults,
-               doc = (nodes && nodes[0] ? nodes[0].ownerDocument || nodes[0] : document);
-
-       // Only cache "small" (1/2 KB) strings that are associated with the main document
-       // Cloning options loses the selected state, so don't cache them
-       // IE 6 doesn't like it when you put <object> or <embed> elements in a fragment
-       // Also, WebKit does not clone 'checked' attributes on cloneNode, so don't cache
-       if ( args.length === 1 && typeof args[0] === "string" && args[0].length < 512 && doc === document &&
-               !rnocache.test( args[0] ) && (jQuery.support.checkClone || !rchecked.test( args[0] )) ) {
-
-               cacheable = true;
-               cacheresults = jQuery.fragments[ args[0] ];
-               if ( cacheresults ) {
-                       if ( cacheresults !== 1 ) {
-                               fragment = cacheresults;
-                       }
-               }
-       }
-
-       if ( !fragment ) {
-               fragment = doc.createDocumentFragment();
-               jQuery.clean( args, doc, fragment, scripts );
-       }
-
-       if ( cacheable ) {
-               jQuery.fragments[ args[0] ] = cacheresults ? fragment : 1;
-       }
-
-       return { fragment: fragment, cacheable: cacheable };
-}
-
-jQuery.fragments = {};
-
-jQuery.each({
-       appendTo: "append",
-       prependTo: "prepend",
-       insertBefore: "before",
-       insertAfter: "after",
-       replaceAll: "replaceWith"
-}, function( name, original ) {
-       jQuery.fn[ name ] = function( selector ) {
-               var ret = [], insert = jQuery( selector ),
-                       parent = this.length === 1 && this[0].parentNode;
-               
-               if ( parent && parent.nodeType === 11 && parent.childNodes.length === 1 && insert.length === 1 ) {
-                       insert[ original ]( this[0] );
-                       return this;
-                       
-               } else {
-                       for ( var i = 0, l = insert.length; i < l; i++ ) {
-                               var elems = (i > 0 ? this.clone(true) : this).get();
-                               jQuery.fn[ original ].apply( jQuery(insert[i]), elems );
-                               ret = ret.concat( elems );
-                       }
-               
-                       return this.pushStack( ret, name, insert.selector );
-               }
-       };
-});
-
-jQuery.extend({
-       clean: function( elems, context, fragment, scripts ) {
-               context = context || document;
-
-               // !context.createElement fails in IE with an error but returns typeof 'object'
-               if ( typeof context.createElement === "undefined" ) {
-                       context = context.ownerDocument || context[0] && context[0].ownerDocument || document;
-               }
-
-               var ret = [];
-
-               for ( var i = 0, elem; (elem = elems[i]) != null; i++ ) {
-                       if ( typeof elem === "number" ) {
-                               elem += "";
-                       }
-
-                       if ( !elem ) {
-                               continue;
-                       }
-
-                       // Convert html string into DOM nodes
-                       if ( typeof elem === "string" && !rhtml.test( elem ) ) {
-                               elem = context.createTextNode( elem );
-
-                       } else if ( typeof elem === "string" ) {
-                               // Fix "XHTML"-style tags in all browsers
-                               elem = elem.replace(rxhtmlTag, fcloseTag);
-
-                               // Trim whitespace, otherwise indexOf won't work as expected
-                               var tag = (rtagName.exec( elem ) || ["", ""])[1].toLowerCase(),
-                                       wrap = wrapMap[ tag ] || wrapMap._default,
-                                       depth = wrap[0],
-                                       div = context.createElement("div");
-
-                               // Go to html and back, then peel off extra wrappers
-                               div.innerHTML = wrap[1] + elem + wrap[2];
-
-                               // Move to the right depth
-                               while ( depth-- ) {
-                                       div = div.lastChild;
-                               }
-
-                               // Remove IE's autoinserted <tbody> from table fragments
-                               if ( !jQuery.support.tbody ) {
-
-                                       // String was a <table>, *may* have spurious <tbody>
-                                       var hasBody = rtbody.test(elem),
-                                               tbody = tag === "table" && !hasBody ?
-                                                       div.firstChild && div.firstChild.childNodes :
-
-                                                       // String was a bare <thead> or <tfoot>
-                                                       wrap[1] === "<table>" && !hasBody ?
-                                                               div.childNodes :
-                                                               [];
-
-                                       for ( var j = tbody.length - 1; j >= 0 ; --j ) {
-                                               if ( jQuery.nodeName( tbody[ j ], "tbody" ) && !tbody[ j ].childNodes.length ) {
-                                                       tbody[ j ].parentNode.removeChild( tbody[ j ] );
-                                               }
-                                       }
-
-                               }
-
-                               // IE completely kills leading whitespace when innerHTML is used
-                               if ( !jQuery.support.leadingWhitespace && rleadingWhitespace.test( elem ) ) {
-                                       div.insertBefore( context.createTextNode( rleadingWhitespace.exec(elem)[0] ), div.firstChild );
-                               }
-
-                               elem = div.childNodes;
-                       }
-
-                       if ( elem.nodeType ) {
-                               ret.push( elem );
-                       } else {
-                               ret = jQuery.merge( ret, elem );
-                       }
-               }
-
-               if ( fragment ) {
-                       for ( var i = 0; ret[i]; i++ ) {
-                               if ( scripts && jQuery.nodeName( ret[i], "script" ) && (!ret[i].type || ret[i].type.toLowerCase() === "text/javascript") ) {
-                                       scripts.push( ret[i].parentNode ? ret[i].parentNode.removeChild( ret[i] ) : ret[i] );
-                               
-                               } else {
-                                       if ( ret[i].nodeType === 1 ) {
-                                               ret.splice.apply( ret, [i + 1, 0].concat(jQuery.makeArray(ret[i].getElementsByTagName("script"))) );
-                                       }
-                                       fragment.appendChild( ret[i] );
-                               }
-                       }
-               }
-
-               return ret;
-       },
-       
-       cleanData: function( elems ) {
-               var data, id, cache = jQuery.cache,
-                       special = jQuery.event.special,
-                       deleteExpando = jQuery.support.deleteExpando;
-               
-               for ( var i = 0, elem; (elem = elems[i]) != null; i++ ) {
-                       id = elem[ jQuery.expando ];
-                       
-                       if ( id ) {
-                               data = cache[ id ];
-                               
-                               if ( data.events ) {
-                                       for ( var type in data.events ) {
-                                               if ( special[ type ] ) {
-                                                       jQuery.event.remove( elem, type );
-
-                                               } else {
-                                                       removeEvent( elem, type, data.handle );
-                                               }
-                                       }
-                               }
-                               
-                               if ( deleteExpando ) {
-                                       delete elem[ jQuery.expando ];
-
-                               } else if ( elem.removeAttribute ) {
-                                       elem.removeAttribute( jQuery.expando );
-                               }
-                               
-                               delete cache[ id ];
-                       }
-               }
-       }
-});
-// exclude the following css properties to add px
-var rexclude = /z-?index|font-?weight|opacity|zoom|line-?height/i,
-       ralpha = /alpha\([^)]*\)/,
-       ropacity = /opacity=([^)]*)/,
-       rfloat = /float/i,
-       rdashAlpha = /-([a-z])/ig,
-       rupper = /([A-Z])/g,
-       rnumpx = /^-?\d+(?:px)?$/i,
-       rnum = /^-?\d/,
-
-       cssShow = { position: "absolute", visibility: "hidden", display:"block" },
-       cssWidth = [ "Left", "Right" ],
-       cssHeight = [ "Top", "Bottom" ],
-
-       // cache check for defaultView.getComputedStyle
-       getComputedStyle = document.defaultView && document.defaultView.getComputedStyle,
-       // normalize float css property
-       styleFloat = jQuery.support.cssFloat ? "cssFloat" : "styleFloat",
-       fcamelCase = function( all, letter ) {
-               return letter.toUpperCase();
-       };
-
-jQuery.fn.css = function( name, value ) {
-       return access( this, name, value, true, function( elem, name, value ) {
-               if ( value === undefined ) {
-                       return jQuery.curCSS( elem, name );
-               }
-               
-               if ( typeof value === "number" && !rexclude.test(name) ) {
-                       value += "px";
-               }
-
-               jQuery.style( elem, name, value );
-       });
-};
-
-jQuery.extend({
-       style: function( elem, name, value ) {
-               // don't set styles on text and comment nodes
-               if ( !elem || elem.nodeType === 3 || elem.nodeType === 8 ) {
-                       return undefined;
-               }
-
-               // ignore negative width and height values #1599
-               if ( (name === "width" || name === "height") && parseFloat(value) < 0 ) {
-                       value = undefined;
-               }
-
-               var style = elem.style || elem, set = value !== undefined;
-
-               // IE uses filters for opacity
-               if ( !jQuery.support.opacity && name === "opacity" ) {
-                       if ( set ) {
-                               // IE has trouble with opacity if it does not have layout
-                               // Force it by setting the zoom level
-                               style.zoom = 1;
-
-                               // Set the alpha filter to set the opacity
-                               var opacity = parseInt( value, 10 ) + "" === "NaN" ? "" : "alpha(opacity=" + value * 100 + ")";
-                               var filter = style.filter || jQuery.curCSS( elem, "filter" ) || "";
-                               style.filter = ralpha.test(filter) ? filter.replace(ralpha, opacity) : opacity;
-                       }
-
-                       return style.filter && style.filter.indexOf("opacity=") >= 0 ?
-                               (parseFloat( ropacity.exec(style.filter)[1] ) / 100) + "":
-                               "";
-               }
-
-               // Make sure we're using the right name for getting the float value
-               if ( rfloat.test( name ) ) {
-                       name = styleFloat;
-               }
-
-               name = name.replace(rdashAlpha, fcamelCase);
-
-               if ( set ) {
-                       style[ name ] = value;
-               }
-
-               return style[ name ];
-       },
-
-       css: function( elem, name, force, extra ) {
-               if ( name === "width" || name === "height" ) {
-                       var val, props = cssShow, which = name === "width" ? cssWidth : cssHeight;
-
-                       function getWH() {
-                               val = name === "width" ? elem.offsetWidth : elem.offsetHeight;
-
-                               if ( extra === "border" ) {
-                                       return;
-                               }
-
-                               jQuery.each( which, function() {
-                                       if ( !extra ) {
-                                               val -= parseFloat(jQuery.curCSS( elem, "padding" + this, true)) || 0;
-                                       }
-
-                                       if ( extra === "margin" ) {
-                                               val += parseFloat(jQuery.curCSS( elem, "margin" + this, true)) || 0;
-                                       } else {
-                                               val -= parseFloat(jQuery.curCSS( elem, "border" + this + "Width", true)) || 0;
-                                       }
-                               });
-                       }
-
-                       if ( elem.offsetWidth !== 0 ) {
-                               getWH();
-                       } else {
-                               jQuery.swap( elem, props, getWH );
-                       }
-
-                       return Math.max(0, Math.round(val));
-               }
-
-               return jQuery.curCSS( elem, name, force );
-       },
-
-       curCSS: function( elem, name, force ) {
-               var ret, style = elem.style, filter;
-
-               // IE uses filters for opacity
-               if ( !jQuery.support.opacity && name === "opacity" && elem.currentStyle ) {
-                       ret = ropacity.test(elem.currentStyle.filter || "") ?
-                               (parseFloat(RegExp.$1) / 100) + "" :
-                               "";
-
-                       return ret === "" ?
-                               "1" :
-                               ret;
-               }
-
-               // Make sure we're using the right name for getting the float value
-               if ( rfloat.test( name ) ) {
-                       name = styleFloat;
-               }
-
-               if ( !force && style && style[ name ] ) {
-                       ret = style[ name ];
-
-               } else if ( getComputedStyle ) {
-
-                       // Only "float" is needed here
-                       if ( rfloat.test( name ) ) {
-                               name = "float";
-                       }
-
-                       name = name.replace( rupper, "-$1" ).toLowerCase();
-
-                       var defaultView = elem.ownerDocument.defaultView;
-
-                       if ( !defaultView ) {
-                               return null;
-                       }
-
-                       var computedStyle = defaultView.getComputedStyle( elem, null );
-
-                       if ( computedStyle ) {
-                               ret = computedStyle.getPropertyValue( name );
-                       }
-
-                       // We should always get a number back from opacity
-                       if ( name === "opacity" && ret === "" ) {
-                               ret = "1";
-                       }
-
-               } else if ( elem.currentStyle ) {
-                       var camelCase = name.replace(rdashAlpha, fcamelCase);
-
-                       ret = elem.currentStyle[ name ] || elem.currentStyle[ camelCase ];
-
-                       // From the awesome hack by Dean Edwards
-                       // http://erik.eae.net/archives/2007/07/27/18.54.15/#comment-102291
-
-                       // If we're not dealing with a regular pixel number
-                       // but a number that has a weird ending, we need to convert it to pixels
-                       if ( !rnumpx.test( ret ) && rnum.test( ret ) ) {
-                               // Remember the original values
-                               var left = style.left, rsLeft = elem.runtimeStyle.left;
-
-                               // Put in the new values to get a computed value out
-                               elem.runtimeStyle.left = elem.currentStyle.left;
-                               style.left = camelCase === "fontSize" ? "1em" : (ret || 0);
-                               ret = style.pixelLeft + "px";
-
-                               // Revert the changed values
-                               style.left = left;
-                               elem.runtimeStyle.left = rsLeft;
-                       }
-               }
-
-               return ret;
-       },
-
-       // A method for quickly swapping in/out CSS properties to get correct calculations
-       swap: function( elem, options, callback ) {
-               var old = {};
-
-               // Remember the old values, and insert the new ones
-               for ( var name in options ) {
-                       old[ name ] = elem.style[ name ];
-                       elem.style[ name ] = options[ name ];
-               }
-
-               callback.call( elem );
-
-               // Revert the old values
-               for ( var name in options ) {
-                       elem.style[ name ] = old[ name ];
-               }
-       }
-});
-
-if ( jQuery.expr && jQuery.expr.filters ) {
-       jQuery.expr.filters.hidden = function( elem ) {
-               var width = elem.offsetWidth, height = elem.offsetHeight,
-                       skip = elem.nodeName.toLowerCase() === "tr";
-
-               return width === 0 && height === 0 && !skip ?
-                       true :
-                       width > 0 && height > 0 && !skip ?
-                               false :
-                               jQuery.curCSS(elem, "display") === "none";
-       };
-
-       jQuery.expr.filters.visible = function( elem ) {
-               return !jQuery.expr.filters.hidden( elem );
-       };
-}
-var jsc = now(),
-       rscript = /<script(.|\s)*?\/script>/gi,
-       rselectTextarea = /select|textarea/i,
-       rinput = /color|date|datetime|email|hidden|month|number|password|range|search|tel|text|time|url|week/i,
-       jsre = /=\?(&|$)/,
-       rquery = /\?/,
-       rts = /(\?|&)_=.*?(&|$)/,
-       rurl = /^(\w+:)?\/\/([^\/?#]+)/,
-       r20 = /%20/g,
-
-       // Keep a copy of the old load method
-       _load = jQuery.fn.load;
-
-jQuery.fn.extend({
-       load: function( url, params, callback ) {
-               if ( typeof url !== "string" ) {
-                       return _load.call( this, url );
-
-               // Don't do a request if no elements are being requested
-               } else if ( !this.length ) {
-                       return this;
-               }
-
-               var off = url.indexOf(" ");
-               if ( off >= 0 ) {
-                       var selector = url.slice(off, url.length);
-                       url = url.slice(0, off);
-               }
-
-               // Default to a GET request
-               var type = "GET";
-
-               // If the second parameter was provided
-               if ( params ) {
-                       // If it's a function
-                       if ( jQuery.isFunction( params ) ) {
-                               // We assume that it's the callback
-                               callback = params;
-                               params = null;
-
-                       // Otherwise, build a param string
-                       } else if ( typeof params === "object" ) {
-                               params = jQuery.param( params, jQuery.ajaxSettings.traditional );
-                               type = "POST";
-                       }
-               }
-
-               var self = this;
-
-               // Request the remote document
-               jQuery.ajax({
-                       url: url,
-                       type: type,
-                       dataType: "html",
-                       data: params,
-                       complete: function( res, status ) {
-                               // If successful, inject the HTML into all the matched elements
-                               if ( status === "success" || status === "notmodified" ) {
-                                       // See if a selector was specified
-                                       self.html( selector ?
-                                               // Create a dummy div to hold the results
-                                               jQuery("<div />")
-                                                       // inject the contents of the document in, removing the scripts
-                                                       // to avoid any 'Permission Denied' errors in IE
-                                                       .append(res.responseText.replace(rscript, ""))
-
-                                                       // Locate the specified elements
-                                                       .find(selector) :
-
-                                               // If not, just inject the full result
-                                               res.responseText );
-                               }
-
-                               if ( callback ) {
-                                       self.each( callback, [res.responseText, status, res] );
-                               }
-                       }
-               });
-
-               return this;
-       },
-
-       serialize: function() {
-               return jQuery.param(this.serializeArray());
-       },
-       serializeArray: function() {
-               return this.map(function() {
-                       return this.elements ? jQuery.makeArray(this.elements) : this;
-               })
-               .filter(function() {
-                       return this.name && !this.disabled &&
-                               (this.checked || rselectTextarea.test(this.nodeName) ||
-                                       rinput.test(this.type));
-               })
-               .map(function( i, elem ) {
-                       var val = jQuery(this).val();
-
-                       return val == null ?
-                               null :
-                               jQuery.isArray(val) ?
-                                       jQuery.map( val, function( val, i ) {
-                                               return { name: elem.name, value: val };
-                                       }) :
-                                       { name: elem.name, value: val };
-               }).get();
-       }
-});
-
-// Attach a bunch of functions for handling common AJAX events
-jQuery.each( "ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split(" "), function( i, o ) {
-       jQuery.fn[o] = function( f ) {
-               return this.bind(o, f);
-       };
-});
-
-jQuery.extend({
-
-       get: function( url, data, callback, type ) {
-               // shift arguments if data argument was omited
-               if ( jQuery.isFunction( data ) ) {
-                       type = type || callback;
-                       callback = data;
-                       data = null;
-               }
-
-               return jQuery.ajax({
-                       type: "GET",
-                       url: url,
-                       data: data,
-                       success: callback,
-                       dataType: type
-               });
-       },
-
-       getScript: function( url, callback ) {
-               return jQuery.get(url, null, callback, "script");
-       },
-
-       getJSON: function( url, data, callback ) {
-               return jQuery.get(url, data, callback, "json");
-       },
-
-       post: function( url, data, callback, type ) {
-               // shift arguments if data argument was omited
-               if ( jQuery.isFunction( data ) ) {
-                       type = type || callback;
-                       callback = data;
-                       data = {};
-               }
-
-               return jQuery.ajax({
-                       type: "POST",
-                       url: url,
-                       data: data,
-                       success: callback,
-                       dataType: type
-               });
-       },
-
-       ajaxSetup: function( settings ) {
-               jQuery.extend( jQuery.ajaxSettings, settings );
-       },
-
-       ajaxSettings: {
-               url: location.href,
-               global: true,
-               type: "GET",
-               contentType: "application/x-www-form-urlencoded",
-               processData: true,
-               async: true,
-               /*
-               timeout: 0,
-               data: null,
-               username: null,
-               password: null,
-               traditional: false,
-               */
-               // Create the request object; Microsoft failed to properly
-               // implement the XMLHttpRequest in IE7 (can't request local files),
-               // so we use the ActiveXObject when it is available
-               // This function can be overriden by calling jQuery.ajaxSetup
-               xhr: window.XMLHttpRequest && (window.location.protocol !== "file:" || !window.ActiveXObject) ?
-                       function() {
-                               return new window.XMLHttpRequest();
-                       } :
-                       function() {
-                               try {
-                                       return new window.ActiveXObject("Microsoft.XMLHTTP");
-                               } catch(e) {}
-                       },
-               accepts: {
-                       xml: "application/xml, text/xml",
-                       html: "text/html",
-                       script: "text/javascript, application/javascript",
-                       json: "application/json, text/javascript",
-                       text: "text/plain",
-                       _default: "*/*"
-               }
-       },
-
-       // Last-Modified header cache for next request
-       lastModified: {},
-       etag: {},
-
-       ajax: function( origSettings ) {
-               var s = jQuery.extend(true, {}, jQuery.ajaxSettings, origSettings);
-               
-               var jsonp, status, data,
-                       callbackContext = origSettings && origSettings.context || s,
-                       type = s.type.toUpperCase();
-
-               // convert data if not already a string
-               if ( s.data && s.processData && typeof s.data !== "string" ) {
-                       s.data = jQuery.param( s.data, s.traditional );
-               }
-
-               // Handle JSONP Parameter Callbacks
-               if ( s.dataType === "jsonp" ) {
-                       if ( type === "GET" ) {
-                               if ( !jsre.test( s.url ) ) {
-                                       s.url += (rquery.test( s.url ) ? "&" : "?") + (s.jsonp || "callback") + "=?";
-                               }
-                       } else if ( !s.data || !jsre.test(s.data) ) {
-                               s.data = (s.data ? s.data + "&" : "") + (s.jsonp || "callback") + "=?";
-                       }
-                       s.dataType = "json";
-               }
-
-               // Build temporary JSONP function
-               if ( s.dataType === "json" && (s.data && jsre.test(s.data) || jsre.test(s.url)) ) {
-                       jsonp = s.jsonpCallback || ("jsonp" + jsc++);
-
-                       // Replace the =? sequence both in the query string and the data
-                       if ( s.data ) {
-                               s.data = (s.data + "").replace(jsre, "=" + jsonp + "$1");
-                       }
-
-                       s.url = s.url.replace(jsre, "=" + jsonp + "$1");
-
-                       // We need to make sure
-                       // that a JSONP style response is executed properly
-                       s.dataType = "script";
-
-                       // Handle JSONP-style loading
-                       window[ jsonp ] = window[ jsonp ] || function( tmp ) {
-                               data = tmp;
-                               success();
-                               complete();
-                               // Garbage collect
-                               window[ jsonp ] = undefined;
-
-                               try {
-                                       delete window[ jsonp ];
-                               } catch(e) {}
-
-                               if ( head ) {
-                                       head.removeChild( script );
-                               }
-                       };
-               }
-
-               if ( s.dataType === "script" && s.cache === null ) {
-                       s.cache = false;
-               }
-
-               if ( s.cache === false && type === "GET" ) {
-                       var ts = now();
-
-                       // try replacing _= if it is there
-                       var ret = s.url.replace(rts, "$1_=" + ts + "$2");
-
-                       // if nothing was replaced, add timestamp to the end
-                       s.url = ret + ((ret === s.url) ? (rquery.test(s.url) ? "&" : "?") + "_=" + ts : "");
-               }
-
-               // If data is available, append data to url for get requests
-               if ( s.data && type === "GET" ) {
-                       s.url += (rquery.test(s.url) ? "&" : "?") + s.data;
-               }
-
-               // Watch for a new set of requests
-               if ( s.global && ! jQuery.active++ ) {
-                       jQuery.event.trigger( "ajaxStart" );
-               }
-
-               // Matches an absolute URL, and saves the domain
-               var parts = rurl.exec( s.url ),
-                       remote = parts && (parts[1] && parts[1] !== location.protocol || parts[2] !== location.host);
-
-               // If we're requesting a remote document
-               // and trying to load JSON or Script with a GET
-               if ( s.dataType === "script" && type === "GET" && remote ) {
-                       var head = document.getElementsByTagName("head")[0] || document.documentElement;
-                       var script = document.createElement("script");
-                       script.src = s.url;
-                       if ( s.scriptCharset ) {
-                               script.charset = s.scriptCharset;
-                       }
-
-                       // Handle Script loading
-                       if ( !jsonp ) {
-                               var done = false;
-
-                               // Attach handlers for all browsers
-                               script.onload = script.onreadystatechange = function() {
-                                       if ( !done && (!this.readyState ||
-                                                       this.readyState === "loaded" || this.readyState === "complete") ) {
-                                               done = true;
-                                               success();
-                                               complete();
-
-                                               // Handle memory leak in IE
-                                               script.onload = script.onreadystatechange = null;
-                                               if ( head && script.parentNode ) {
-                                                       head.removeChild( script );
-                                               }
-                                       }
-                               };
-                       }
-
-                       // Use insertBefore instead of appendChild  to circumvent an IE6 bug.
-                       // This arises when a base node is used (#2709 and #4378).
-                       head.insertBefore( script, head.firstChild );
-
-                       // We handle everything using the script element injection
-                       return undefined;
-               }
-
-               var requestDone = false;
-
-               // Create the request object
-               var xhr = s.xhr();
-
-               if ( !xhr ) {
-                       return;
-               }
-
-               // Open the socket
-               // Passing null username, generates a login popup on Opera (#2865)
-               if ( s.username ) {
-                       xhr.open(type, s.url, s.async, s.username, s.password);
-               } else {
-                       xhr.open(type, s.url, s.async);
-               }
-
-               // Need an extra try/catch for cross domain requests in Firefox 3
-               try {
-                       // Set the correct header, if data is being sent
-                       if ( s.data || origSettings && origSettings.contentType ) {
-                               xhr.setRequestHeader("Content-Type", s.contentType);
-                       }
-
-                       // Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode.
-                       if ( s.ifModified ) {
-                               if ( jQuery.lastModified[s.url] ) {
-                                       xhr.setRequestHeader("If-Modified-Since", jQuery.lastModified[s.url]);
-                               }
-
-                               if ( jQuery.etag[s.url] ) {
-                                       xhr.setRequestHeader("If-None-Match", jQuery.etag[s.url]);
-                               }
-                       }
-
-                       // Set header so the called script knows that it's an XMLHttpRequest
-                       // Only send the header if it's not a remote XHR
-                       if ( !remote ) {
-                               xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
-                       }
-
-                       // Set the Accepts header for the server, depending on the dataType
-                       xhr.setRequestHeader("Accept", s.dataType && s.accepts[ s.dataType ] ?
-                               s.accepts[ s.dataType ] + ", */*" :
-                               s.accepts._default );
-               } catch(e) {}
-
-               // Allow custom headers/mimetypes and early abort
-               if ( s.beforeSend && s.beforeSend.call(callbackContext, xhr, s) === false ) {
-                       // Handle the global AJAX counter
-                       if ( s.global && ! --jQuery.active ) {
-                               jQuery.event.trigger( "ajaxStop" );
-                       }
-
-                       // close opended socket
-                       xhr.abort();
-                       return false;
-               }
-
-               if ( s.global ) {
-                       trigger("ajaxSend", [xhr, s]);
-               }
-
-               // Wait for a response to come back
-               var onreadystatechange = xhr.onreadystatechange = function( isTimeout ) {
-                       // The request was aborted
-                       if ( !xhr || xhr.readyState === 0 || isTimeout === "abort" ) {
-                               // Opera doesn't call onreadystatechange before this point
-                               // so we simulate the call
-                               if ( !requestDone ) {
-                                       complete();
-                               }
-
-                               requestDone = true;
-                               if ( xhr ) {
-                                       xhr.onreadystatechange = jQuery.noop;
-                               }
-
-                       // The transfer is complete and the data is available, or the request timed out
-                       } else if ( !requestDone && xhr && (xhr.readyState === 4 || isTimeout === "timeout") ) {
-                               requestDone = true;
-                               xhr.onreadystatechange = jQuery.noop;
-
-                               status = isTimeout === "timeout" ?
-                                       "timeout" :
-                                       !jQuery.httpSuccess( xhr ) ?
-                                               "error" :
-                                               s.ifModified && jQuery.httpNotModified( xhr, s.url ) ?
-                                                       "notmodified" :
-                                                       "success";
-
-                               var errMsg;
-
-                               if ( status === "success" ) {
-                                       // Watch for, and catch, XML document parse errors
-                                       try {
-                                               // process the data (runs the xml through httpData regardless of callback)
-                                               data = jQuery.httpData( xhr, s.dataType, s );
-                                       } catch(err) {
-                                               status = "parsererror";
-                                               errMsg = err;
-                                       }
-                               }
-
-                               // Make sure that the request was successful or notmodified
-                               if ( status === "success" || status === "notmodified" ) {
-                                       // JSONP handles its own success callback
-                                       if ( !jsonp ) {
-                                               success();
-                                       }
-                               } else {
-                                       jQuery.handleError(s, xhr, status, errMsg);
-                               }
-
-                               // Fire the complete handlers
-                               complete();
-
-                               if ( isTimeout === "timeout" ) {
-                                       xhr.abort();
-                               }
-
-                               // Stop memory leaks
-                               if ( s.async ) {
-                                       xhr = null;
-                               }
-                       }
-               };
-
-               // Override the abort handler, if we can (IE doesn't allow it, but that's OK)
-               // Opera doesn't fire onreadystatechange at all on abort
-               try {
-                       var oldAbort = xhr.abort;
-                       xhr.abort = function() {
-                               if ( xhr ) {
-                                       oldAbort.call( xhr );
-                               }
-
-                               onreadystatechange( "abort" );
-                       };
-               } catch(e) { }
-
-               // Timeout checker
-               if ( s.async && s.timeout > 0 ) {
-                       setTimeout(function() {
-                               // Check to see if the request is still happening
-                               if ( xhr && !requestDone ) {
-                                       onreadystatechange( "timeout" );
-                               }
-                       }, s.timeout);
-               }
-
-               // Send the data
-               try {
-                       xhr.send( type === "POST" || type === "PUT" || type === "DELETE" ? s.data : null );
-               } catch(e) {
-                       jQuery.handleError(s, xhr, null, e);
-                       // Fire the complete handlers
-                       complete();
-               }
-
-               // firefox 1.5 doesn't fire statechange for sync requests
-               if ( !s.async ) {
-                       onreadystatechange();
-               }
-
-               function success() {
-                       // If a local callback was specified, fire it and pass it the data
-                       if ( s.success ) {
-                               s.success.call( callbackContext, data, status, xhr );
-                       }
-
-                       // Fire the global callback
-                       if ( s.global ) {
-                               trigger( "ajaxSuccess", [xhr, s] );
-                       }
-               }
-
-               function complete() {
-                       // Process result
-                       if ( s.complete ) {
-                               s.complete.call( callbackContext, xhr, status);
-                       }
-
-                       // The request was completed
-                       if ( s.global ) {
-                               trigger( "ajaxComplete", [xhr, s] );
-                       }
-
-                       // Handle the global AJAX counter
-                       if ( s.global && ! --jQuery.active ) {
-                               jQuery.event.trigger( "ajaxStop" );
-                       }
-               }
-               
-               function trigger(type, args) {
-                       (s.context ? jQuery(s.context) : jQuery.event).trigger(type, args);
-               }
-
-               // return XMLHttpRequest to allow aborting the request etc.
-               return xhr;
-       },
-
-       handleError: function( s, xhr, status, e ) {
-               // If a local callback was specified, fire it
-               if ( s.error ) {
-                       s.error.call( s.context || s, xhr, status, e );
-               }
-
-               // Fire the global callback
-               if ( s.global ) {
-                       (s.context ? jQuery(s.context) : jQuery.event).trigger( "ajaxError", [xhr, s, e] );
-               }
-       },
-
-       // Counter for holding the number of active queries
-       active: 0,
-
-       // Determines if an XMLHttpRequest was successful or not
-       httpSuccess: function( xhr ) {
-               try {
-                       // IE error sometimes returns 1223 when it should be 204 so treat it as success, see #1450
-                       return !xhr.status && location.protocol === "file:" ||
-                               // Opera returns 0 when status is 304
-                               ( xhr.status >= 200 && xhr.status < 300 ) ||
-                               xhr.status === 304 || xhr.status === 1223 || xhr.status === 0;
-               } catch(e) {}
-
-               return false;
-       },
-
-       // Determines if an XMLHttpRequest returns NotModified
-       httpNotModified: function( xhr, url ) {
-               var lastModified = xhr.getResponseHeader("Last-Modified"),
-                       etag = xhr.getResponseHeader("Etag");
-
-               if ( lastModified ) {
-                       jQuery.lastModified[url] = lastModified;
-               }
-
-               if ( etag ) {
-                       jQuery.etag[url] = etag;
-               }
-
-               // Opera returns 0 when status is 304
-               return xhr.status === 304 || xhr.status === 0;
-       },
-
-       httpData: function( xhr, type, s ) {
-               var ct = xhr.getResponseHeader("content-type") || "",
-                       xml = type === "xml" || !type && ct.indexOf("xml") >= 0,
-                       data = xml ? xhr.responseXML : xhr.responseText;
-
-               if ( xml && data.documentElement.nodeName === "parsererror" ) {
-                       jQuery.error( "parsererror" );
-               }
-
-               // Allow a pre-filtering function to sanitize the response
-               // s is checked to keep backwards compatibility
-               if ( s && s.dataFilter ) {
-                       data = s.dataFilter( data, type );
-               }
-
-               // The filter can actually parse the response
-               if ( typeof data === "string" ) {
-                       // Get the JavaScript object, if JSON is used.
-                       if ( type === "json" || !type && ct.indexOf("json") >= 0 ) {
-                               data = jQuery.parseJSON( data );
-
-                       // If the type is "script", eval it in global context
-                       } else if ( type === "script" || !type && ct.indexOf("javascript") >= 0 ) {
-                               jQuery.globalEval( data );
-                       }
-               }
-
-               return data;
-       },
-
-       // Serialize an array of form elements or a set of
-       // key/values into a query string
-       param: function( a, traditional ) {
-               var s = [];
-               
-               // Set traditional to true for jQuery <= 1.3.2 behavior.
-               if ( traditional === undefined ) {
-                       traditional = jQuery.ajaxSettings.traditional;
-               }
-               
-               // If an array was passed in, assume that it is an array of form elements.
-               if ( jQuery.isArray(a) || a.jquery ) {
-                       // Serialize the form elements
-                       jQuery.each( a, function() {
-                               add( this.name, this.value );
-                       });
-                       
-               } else {
-                       // If traditional, encode the "old" way (the way 1.3.2 or older
-                       // did it), otherwise encode params recursively.
-                       for ( var prefix in a ) {
-                               buildParams( prefix, a[prefix] );
-                       }
-               }
-
-               // Return the resulting serialization
-               return s.join("&").replace(r20, "+");
-
-               function buildParams( prefix, obj ) {
-                       if ( jQuery.isArray(obj) ) {
-                               // Serialize array item.
-                               jQuery.each( obj, function( i, v ) {
-                                       if ( traditional || /\[\]$/.test( prefix ) ) {
-                                               // Treat each array item as a scalar.
-                                               add( prefix, v );
-                                       } else {
-                                               // If array item is non-scalar (array or object), encode its
-                                               // numeric index to resolve deserialization ambiguity issues.
-                                               // Note that rack (as of 1.0.0) can't currently deserialize
-                                               // nested arrays properly, and attempting to do so may cause
-                                               // a server error. Possible fixes are to modify rack's
-                                               // deserialization algorithm or to provide an option or flag
-                                               // to force array serialization to be shallow.
-                                               buildParams( prefix + "[" + ( typeof v === "object" || jQuery.isArray(v) ? i : "" ) + "]", v );
-                                       }
-                               });
-                                       
-                       } else if ( !traditional && obj != null && typeof obj === "object" ) {
-                               // Serialize object item.
-                               jQuery.each( obj, function( k, v ) {
-                                       buildParams( prefix + "[" + k + "]", v );
-                               });
-                                       
-                       } else {
-                               // Serialize scalar item.
-                               add( prefix, obj );
-                       }
-               }
-
-               function add( key, value ) {
-                       // If value is a function, invoke it and return its value
-                       value = jQuery.isFunction(value) ? value() : value;
-                       s[ s.length ] = encodeURIComponent(key) + "=" + encodeURIComponent(value);
-               }
-       }
-});
-var elemdisplay = {},
-       rfxtypes = /toggle|show|hide/,
-       rfxnum = /^([+-]=)?([\d+-.]+)(.*)$/,
-       timerId,
-       fxAttrs = [
-               // height animations
-               [ "height", "marginTop", "marginBottom", "paddingTop", "paddingBottom" ],
-               // width animations
-               [ "width", "marginLeft", "marginRight", "paddingLeft", "paddingRight" ],
-               // opacity animations
-               [ "opacity" ]
-       ];
-
-jQuery.fn.extend({
-       show: function( speed, callback ) {
-               if ( speed || speed === 0) {
-                       return this.animate( genFx("show", 3), speed, callback);
-
-               } else {
-                       for ( var i = 0, l = this.length; i < l; i++ ) {
-                               var old = jQuery.data(this[i], "olddisplay");
-
-                               this[i].style.display = old || "";
-
-                               if ( jQuery.css(this[i], "display") === "none" ) {
-                                       var nodeName = this[i].nodeName, display;
-
-                                       if ( elemdisplay[ nodeName ] ) {
-                                               display = elemdisplay[ nodeName ];
-
-                                       } else {
-                                               var elem = jQuery("<" + nodeName + " />").appendTo("body");
-
-                                               display = elem.css("display");
-
-                                               if ( display === "none" ) {
-                                                       display = "block";
-                                               }
-
-                                               elem.remove();
-
-                                               elemdisplay[ nodeName ] = display;
-                                       }
-
-                                       jQuery.data(this[i], "olddisplay", display);
-                               }
-                       }
-
-                       // Set the display of the elements in a second loop
-                       // to avoid the constant reflow
-                       for ( var j = 0, k = this.length; j < k; j++ ) {
-                               this[j].style.display = jQuery.data(this[j], "olddisplay") || "";
-                       }
-
-                       return this;
-               }
-       },
-
-       hide: function( speed, callback ) {
-               if ( speed || speed === 0 ) {
-                       return this.animate( genFx("hide", 3), speed, callback);
-
-               } else {
-                       for ( var i = 0, l = this.length; i < l; i++ ) {
-                               var old = jQuery.data(this[i], "olddisplay");
-                               if ( !old && old !== "none" ) {
-                                       jQuery.data(this[i], "olddisplay", jQuery.css(this[i], "display"));
-                               }
-                       }
-
-                       // Set the display of the elements in a second loop
-                       // to avoid the constant reflow
-                       for ( var j = 0, k = this.length; j < k; j++ ) {
-                               this[j].style.display = "none";
-                       }
-
-                       return this;
-               }
-       },
-
-       // Save the old toggle function
-       _toggle: jQuery.fn.toggle,
-
-       toggle: function( fn, fn2 ) {
-               var bool = typeof fn === "boolean";
-
-               if ( jQuery.isFunction(fn) && jQuery.isFunction(fn2) ) {
-                       this._toggle.apply( this, arguments );
-
-               } else if ( fn == null || bool ) {
-                       this.each(function() {
-                               var state = bool ? fn : jQuery(this).is(":hidden");
-                               jQuery(this)[ state ? "show" : "hide" ]();
-                       });
-
-               } else {
-                       this.animate(genFx("toggle", 3), fn, fn2);
-               }
-
-               return this;
-       },
-
-       fadeTo: function( speed, to, callback ) {
-               return this.filter(":hidden").css("opacity", 0).show().end()
-                                       .animate({opacity: to}, speed, callback);
-       },
-
-       animate: function( prop, speed, easing, callback ) {
-               var optall = jQuery.speed(speed, easing, callback);
-
-               if ( jQuery.isEmptyObject( prop ) ) {
-                       return this.each( optall.complete );
-               }
-
-               return this[ optall.queue === false ? "each" : "queue" ](function() {
-                       var opt = jQuery.extend({}, optall), p,
-                               hidden = this.nodeType === 1 && jQuery(this).is(":hidden"),
-                               self = this;
-
-                       for ( p in prop ) {
-                               var name = p.replace(rdashAlpha, fcamelCase);
-
-                               if ( p !== name ) {
-                                       prop[ name ] = prop[ p ];
-                                       delete prop[ p ];
-                                       p = name;
-                               }
-
-                               if ( prop[p] === "hide" && hidden || prop[p] === "show" && !hidden ) {
-                                       return opt.complete.call(this);
-                               }
-
-                               if ( ( p === "height" || p === "width" ) && this.style ) {
-                                       // Store display property
-                                       opt.display = jQuery.css(this, "display");
-
-                                       // Make sure that nothing sneaks out
-                                       opt.overflow = this.style.overflow;
-                               }
-
-                               if ( jQuery.isArray( prop[p] ) ) {
-                                       // Create (if needed) and add to specialEasing
-                                       (opt.specialEasing = opt.specialEasing || {})[p] = prop[p][1];
-                                       prop[p] = prop[p][0];
-                               }
-                       }
-
-                       if ( opt.overflow != null ) {
-                               this.style.overflow = "hidden";
-                       }
-
-                       opt.curAnim = jQuery.extend({}, prop);
-
-                       jQuery.each( prop, function( name, val ) {
-                               var e = new jQuery.fx( self, opt, name );
-
-                               if ( rfxtypes.test(val) ) {
-                                       e[ val === "toggle" ? hidden ? "show" : "hide" : val ]( prop );
-
-                               } else {
-                                       var parts = rfxnum.exec(val),
-                                               start = e.cur(true) || 0;
-
-                                       if ( parts ) {
-                                               var end = parseFloat( parts[2] ),
-                                                       unit = parts[3] || "px";
-
-                                               // We need to compute starting value
-                                               if ( unit !== "px" ) {
-                                                       self.style[ name ] = (end || 1) + unit;
-                                                       start = ((end || 1) / e.cur(true)) * start;
-                                                       self.style[ name ] = start + unit;
-                                               }
-
-                                               // If a +=/-= token was provided, we're doing a relative animation
-                                               if ( parts[1] ) {
-                                                       end = ((parts[1] === "-=" ? -1 : 1) * end) + start;
-                                               }
-
-                                               e.custom( start, end, unit );
-
-                                       } else {
-                                               e.custom( start, val, "" );
-                                       }
-                               }
-                       });
-
-                       // For JS strict compliance
-                       return true;
-               });
-       },
-
-       stop: function( clearQueue, gotoEnd ) {
-               var timers = jQuery.timers;
-
-               if ( clearQueue ) {
-                       this.queue([]);
-               }
-
-               this.each(function() {
-                       // go in reverse order so anything added to the queue during the loop is ignored
-                       for ( var i = timers.length - 1; i >= 0; i-- ) {
-                               if ( timers[i].elem === this ) {
-                                       if (gotoEnd) {
-                                               // force the next step to be the last
-                                               timers[i](true);
-                                       }
-
-                                       timers.splice(i, 1);
-                               }
-                       }
-               });
-
-               // start the next in the queue if the last step wasn't forced
-               if ( !gotoEnd ) {
-                       this.dequeue();
-               }
-
-               return this;
-       }
-
-});
-
-// Generate shortcuts for custom animations
-jQuery.each({
-       slideDown: genFx("show", 1),
-       slideUp: genFx("hide", 1),
-       slideToggle: genFx("toggle", 1),
-       fadeIn: { opacity: "show" },
-       fadeOut: { opacity: "hide" }
-}, function( name, props ) {
-       jQuery.fn[ name ] = function( speed, callback ) {
-               return this.animate( props, speed, callback );
-       };
-});
-
-jQuery.extend({
-       speed: function( speed, easing, fn ) {
-               var opt = speed && typeof speed === "object" ? speed : {
-                       complete: fn || !fn && easing ||
-                               jQuery.isFunction( speed ) && speed,
-                       duration: speed,
-                       easing: fn && easing || easing && !jQuery.isFunction(easing) && easing
-               };
-
-               opt.duration = jQuery.fx.off ? 0 : typeof opt.duration === "number" ? opt.duration :
-                       jQuery.fx.speeds[opt.duration] || jQuery.fx.speeds._default;
-
-               // Queueing
-               opt.old = opt.complete;
-               opt.complete = function() {
-                       if ( opt.queue !== false ) {
-                               jQuery(this).dequeue();
-                       }
-                       if ( jQuery.isFunction( opt.old ) ) {
-                               opt.old.call( this );
-                       }
-               };
-
-               return opt;
-       },
-
-       easing: {
-               linear: function( p, n, firstNum, diff ) {
-                       return firstNum + diff * p;
-               },
-               swing: function( p, n, firstNum, diff ) {
-                       return ((-Math.cos(p*Math.PI)/2) + 0.5) * diff + firstNum;
-               }
-       },
-
-       timers: [],
-
-       fx: function( elem, options, prop ) {
-               this.options = options;
-               this.elem = elem;
-               this.prop = prop;
-
-               if ( !options.orig ) {
-                       options.orig = {};
-               }
-       }
-
-});
-
-jQuery.fx.prototype = {
-       // Simple function for setting a style value
-       update: function() {
-               if ( this.options.step ) {
-                       this.options.step.call( this.elem, this.now, this );
-               }
-
-               (jQuery.fx.step[this.prop] || jQuery.fx.step._default)( this );
-
-               // Set display property to block for height/width animations
-               if ( ( this.prop === "height" || this.prop === "width" ) && this.elem.style ) {
-                       this.elem.style.display = "block";
-               }
-       },
-
-       // Get the current size
-       cur: function( force ) {
-               if ( this.elem[this.prop] != null && (!this.elem.style || this.elem.style[this.prop] == null) ) {
-                       return this.elem[ this.prop ];
-               }
-
-               var r = parseFloat(jQuery.css(this.elem, this.prop, force));
-               return r && r > -10000 ? r : parseFloat(jQuery.curCSS(this.elem, this.prop)) || 0;
-       },
-
-       // Start an animation from one number to another
-       custom: function( from, to, unit ) {
-               this.startTime = now();
-               this.start = from;
-               this.end = to;
-               this.unit = unit || this.unit || "px";
-               this.now = this.start;
-               this.pos = this.state = 0;
-
-               var self = this;
-               function t( gotoEnd ) {
-                       return self.step(gotoEnd);
-               }
-
-               t.elem = this.elem;
-
-               if ( t() && jQuery.timers.push(t) && !timerId ) {
-                       timerId = setInterval(jQuery.fx.tick, 13);
-               }
-       },
-
-       // Simple 'show' function
-       show: function() {
-               // Remember where we started, so that we can go back to it later
-               this.options.orig[this.prop] = jQuery.style( this.elem, this.prop );
-               this.options.show = true;
-
-               // Begin the animation
-               // Make sure that we start at a small width/height to avoid any
-               // flash of content
-               this.custom(this.prop === "width" || this.prop === "height" ? 1 : 0, this.cur());
-
-               // Start by showing the element
-               jQuery( this.elem ).show();
-       },
-
-       // Simple 'hide' function
-       hide: function() {
-               // Remember where we started, so that we can go back to it later
-               this.options.orig[this.prop] = jQuery.style( this.elem, this.prop );
-               this.options.hide = true;
-
-               // Begin the animation
-               this.custom(this.cur(), 0);
-       },
-
-       // Each step of an animation
-       step: function( gotoEnd ) {
-               var t = now(), done = true;
-
-               if ( gotoEnd || t >= this.options.duration + this.startTime ) {
-                       this.now = this.end;
-                       this.pos = this.state = 1;
-                       this.update();
-
-                       this.options.curAnim[ this.prop ] = true;
-
-                       for ( var i in this.options.curAnim ) {
-                               if ( this.options.curAnim[i] !== true ) {
-                                       done = false;
-                               }
-                       }
-
-                       if ( done ) {
-                               if ( this.options.display != null ) {
-                                       // Reset the overflow
-                                       this.elem.style.overflow = this.options.overflow;
-
-                                       // Reset the display
-                                       var old = jQuery.data(this.elem, "olddisplay");
-                                       this.elem.style.display = old ? old : this.options.display;
-
-                                       if ( jQuery.css(this.elem, "display") === "none" ) {
-                                               this.elem.style.display = "block";
-                                       }
-                               }
-
-                               // Hide the element if the "hide" operation was done
-                               if ( this.options.hide ) {
-                                       jQuery(this.elem).hide();
-                               }
-
-                               // Reset the properties, if the item has been hidden or shown
-                               if ( this.options.hide || this.options.show ) {
-                                       for ( var p in this.options.curAnim ) {
-                                               jQuery.style(this.elem, p, this.options.orig[p]);
-                                       }
-                               }
-
-                               // Execute the complete function
-                               this.options.complete.call( this.elem );
-                       }
-
-                       return false;
-
-               } else {
-                       var n = t - this.startTime;
-                       this.state = n / this.options.duration;
-
-                       // Perform the easing function, defaults to swing
-                       var specialEasing = this.options.specialEasing && this.options.specialEasing[this.prop];
-                       var defaultEasing = this.options.easing || (jQuery.easing.swing ? "swing" : "linear");
-                       this.pos = jQuery.easing[specialEasing || defaultEasing](this.state, n, 0, 1, this.options.duration);
-                       this.now = this.start + ((this.end - this.start) * this.pos);
-
-                       // Perform the next step of the animation
-                       this.update();
-               }
-
-               return true;
-       }
-};
-
-jQuery.extend( jQuery.fx, {
-       tick: function() {
-               var timers = jQuery.timers;
-
-               for ( var i = 0; i < timers.length; i++ ) {
-                       if ( !timers[i]() ) {
-                               timers.splice(i--, 1);
-                       }
-               }
-
-               if ( !timers.length ) {
-                       jQuery.fx.stop();
-               }
-       },
-               
-       stop: function() {
-               clearInterval( timerId );
-               timerId = null;
-       },
-       
-       speeds: {
-               slow: 600,
-               fast: 200,
-               // Default speed
-               _default: 400
-       },
-
-       step: {
-               opacity: function( fx ) {
-                       jQuery.style(fx.elem, "opacity", fx.now);
-               },
-
-               _default: function( fx ) {
-                       if ( fx.elem.style && fx.elem.style[ fx.prop ] != null ) {
-                               fx.elem.style[ fx.prop ] = (fx.prop === "width" || fx.prop === "height" ? Math.max(0, fx.now) : fx.now) + fx.unit;
-                       } else {
-                               fx.elem[ fx.prop ] = fx.now;
-                       }
-               }
-       }
-});
-
-if ( jQuery.expr && jQuery.expr.filters ) {
-       jQuery.expr.filters.animated = function( elem ) {
-               return jQuery.grep(jQuery.timers, function( fn ) {
-                       return elem === fn.elem;
-               }).length;
-       };
-}
-
-function genFx( type, num ) {
-       var obj = {};
-
-       jQuery.each( fxAttrs.concat.apply([], fxAttrs.slice(0,num)), function() {
-               obj[ this ] = type;
-       });
-
-       return obj;
-}
-if ( "getBoundingClientRect" in document.documentElement ) {
-       jQuery.fn.offset = function( options ) {
-               var elem = this[0];
-
-               if ( options ) { 
-                       return this.each(function( i ) {
-                               jQuery.offset.setOffset( this, options, i );
-                       });
-               }
-
-               if ( !elem || !elem.ownerDocument ) {
-                       return null;
-               }
-
-               if ( elem === elem.ownerDocument.body ) {
-                       return jQuery.offset.bodyOffset( elem );
-               }
-
-               var box = elem.getBoundingClientRect(), doc = elem.ownerDocument, body = doc.body, docElem = doc.documentElement,
-                       clientTop = docElem.clientTop || body.clientTop || 0, clientLeft = docElem.clientLeft || body.clientLeft || 0,
-                       top  = box.top  + (self.pageYOffset || jQuery.support.boxModel && docElem.scrollTop  || body.scrollTop ) - clientTop,
-                       left = box.left + (self.pageXOffset || jQuery.support.boxModel && docElem.scrollLeft || body.scrollLeft) - clientLeft;
-
-               return { top: top, left: left };
-       };
-
-} else {
-       jQuery.fn.offset = function( options ) {
-               var elem = this[0];
-
-               if ( options ) { 
-                       return this.each(function( i ) {
-                               jQuery.offset.setOffset( this, options, i );
-                       });
-               }
-
-               if ( !elem || !elem.ownerDocument ) {
-                       return null;
-               }
-
-               if ( elem === elem.ownerDocument.body ) {
-                       return jQuery.offset.bodyOffset( elem );
-               }
-
-               jQuery.offset.initialize();
-
-               var offsetParent = elem.offsetParent, prevOffsetParent = elem,
-                       doc = elem.ownerDocument, computedStyle, docElem = doc.documentElement,
-                       body = doc.body, defaultView = doc.defaultView,
-                       prevComputedStyle = defaultView ? defaultView.getComputedStyle( elem, null ) : elem.currentStyle,
-                       top = elem.offsetTop, left = elem.offsetLeft;
-
-               while ( (elem = elem.parentNode) && elem !== body && elem !== docElem ) {
-                       if ( jQuery.offset.supportsFixedPosition && prevComputedStyle.position === "fixed" ) {
-                               break;
-                       }
-
-                       computedStyle = defaultView ? defaultView.getComputedStyle(elem, null) : elem.currentStyle;
-                       top  -= elem.scrollTop;
-                       left -= elem.scrollLeft;
-
-                       if ( elem === offsetParent ) {
-                               top  += elem.offsetTop;
-                               left += elem.offsetLeft;
-
-                               if ( jQuery.offset.doesNotAddBorder && !(jQuery.offset.doesAddBorderForTableAndCells && /^t(able|d|h)$/i.test(elem.nodeName)) ) {
-                                       top  += parseFloat( computedStyle.borderTopWidth  ) || 0;
-                                       left += parseFloat( computedStyle.borderLeftWidth ) || 0;
-                               }
-
-                               prevOffsetParent = offsetParent, offsetParent = elem.offsetParent;
-                       }
-
-                       if ( jQuery.offset.subtractsBorderForOverflowNotVisible && computedStyle.overflow !== "visible" ) {
-                               top  += parseFloat( computedStyle.borderTopWidth  ) || 0;
-                               left += parseFloat( computedStyle.borderLeftWidth ) || 0;
-                       }
-
-                       prevComputedStyle = computedStyle;
-               }
-
-               if ( prevComputedStyle.position === "relative" || prevComputedStyle.position === "static" ) {
-                       top  += body.offsetTop;
-                       left += body.offsetLeft;
-               }
-
-               if ( jQuery.offset.supportsFixedPosition && prevComputedStyle.position === "fixed" ) {
-                       top  += Math.max( docElem.scrollTop, body.scrollTop );
-                       left += Math.max( docElem.scrollLeft, body.scrollLeft );
-               }
-
-               return { top: top, left: left };
-       };
-}
-
-jQuery.offset = {
-       initialize: function() {
-               var body = document.body, container = document.createElement("div"), innerDiv, checkDiv, table, td, bodyMarginTop = parseFloat( jQuery.curCSS(body, "marginTop", true) ) || 0,
-                       html = "<div style='position:absolute;top:0;left:0;margin:0;border:5px solid #000;padding:0;width:1px;height:1px;'><div></div></div><table style='position:absolute;top:0;left:0;margin:0;border:5px solid #000;padding:0;width:1px;height:1px;' cellpadding='0' cellspacing='0'><tr><td></td></tr></table>";
-
-               jQuery.extend( container.style, { position: "absolute", top: 0, left: 0, margin: 0, border: 0, width: "1px", height: "1px", visibility: "hidden" } );
-
-               container.innerHTML = html;
-               body.insertBefore( container, body.firstChild );
-               innerDiv = container.firstChild;
-               checkDiv = innerDiv.firstChild;
-               td = innerDiv.nextSibling.firstChild.firstChild;
-
-               this.doesNotAddBorder = (checkDiv.offsetTop !== 5);
-               this.doesAddBorderForTableAndCells = (td.offsetTop === 5);
-
-               checkDiv.style.position = "fixed", checkDiv.style.top = "20px";
-               // safari subtracts parent border width here which is 5px
-               this.supportsFixedPosition = (checkDiv.offsetTop === 20 || checkDiv.offsetTop === 15);
-               checkDiv.style.position = checkDiv.style.top = "";
-
-               innerDiv.style.overflow = "hidden", innerDiv.style.position = "relative";
-               this.subtractsBorderForOverflowNotVisible = (checkDiv.offsetTop === -5);
-
-               this.doesNotIncludeMarginInBodyOffset = (body.offsetTop !== bodyMarginTop);
-
-               body.removeChild( container );
-               body = container = innerDiv = checkDiv = table = td = null;
-               jQuery.offset.initialize = jQuery.noop;
-       },
-
-       bodyOffset: function( body ) {
-               var top = body.offsetTop, left = body.offsetLeft;
-
-               jQuery.offset.initialize();
-
-               if ( jQuery.offset.doesNotIncludeMarginInBodyOffset ) {
-                       top  += parseFloat( jQuery.curCSS(body, "marginTop",  true) ) || 0;
-                       left += parseFloat( jQuery.curCSS(body, "marginLeft", true) ) || 0;
-               }
-
-               return { top: top, left: left };
-       },
-       
-       setOffset: function( elem, options, i ) {
-               // set position first, in-case top/left are set even on static elem
-               if ( /static/.test( jQuery.curCSS( elem, "position" ) ) ) {
-                       elem.style.position = "relative";
-               }
-               var curElem   = jQuery( elem ),
-                       curOffset = curElem.offset(),
-                       curTop    = parseInt( jQuery.curCSS( elem, "top",  true ), 10 ) || 0,
-                       curLeft   = parseInt( jQuery.curCSS( elem, "left", true ), 10 ) || 0;
-
-               if ( jQuery.isFunction( options ) ) {
-                       options = options.call( elem, i, curOffset );
-               }
-
-               var props = {
-                       top:  (options.top  - curOffset.top)  + curTop,
-                       left: (options.left - curOffset.left) + curLeft
-               };
-               
-               if ( "using" in options ) {
-                       options.using.call( elem, props );
-               } else {
-                       curElem.css( props );
-               }
-       }
-};
-
-
-jQuery.fn.extend({
-       position: function() {
-               if ( !this[0] ) {
-                       return null;
-               }
-
-               var elem = this[0],
-
-               // Get *real* offsetParent
-               offsetParent = this.offsetParent(),
-
-               // Get correct offsets
-               offset       = this.offset(),
-               parentOffset = /^body|html$/i.test(offsetParent[0].nodeName) ? { top: 0, left: 0 } : offsetParent.offset();
-
-               // Subtract element margins
-               // note: when an element has margin: auto the offsetLeft and marginLeft
-               // are the same in Safari causing offset.left to incorrectly be 0
-               offset.top  -= parseFloat( jQuery.curCSS(elem, "marginTop",  true) ) || 0;
-               offset.left -= parseFloat( jQuery.curCSS(elem, "marginLeft", true) ) || 0;
-
-               // Add offsetParent borders
-               parentOffset.top  += parseFloat( jQuery.curCSS(offsetParent[0], "borderTopWidth",  true) ) || 0;
-               parentOffset.left += parseFloat( jQuery.curCSS(offsetParent[0], "borderLeftWidth", true) ) || 0;
-
-               // Subtract the two offsets
-               return {
-                       top:  offset.top  - parentOffset.top,
-                       left: offset.left - parentOffset.left
-               };
-       },
-
-       offsetParent: function() {
-               return this.map(function() {
-                       var offsetParent = this.offsetParent || document.body;
-                       while ( offsetParent && (!/^body|html$/i.test(offsetParent.nodeName) && jQuery.css(offsetParent, "position") === "static") ) {
-                               offsetParent = offsetParent.offsetParent;
-                       }
-                       return offsetParent;
-               });
-       }
-});
-
-
-// Create scrollLeft and scrollTop methods
-jQuery.each( ["Left", "Top"], function( i, name ) {
-       var method = "scroll" + name;
-
-       jQuery.fn[ method ] = function(val) {
-               var elem = this[0], win;
-               
-               if ( !elem ) {
-                       return null;
-               }
-
-               if ( val !== undefined ) {
-                       // Set the scroll offset
-                       return this.each(function() {
-                               win = getWindow( this );
-
-                               if ( win ) {
-                                       win.scrollTo(
-                                               !i ? val : jQuery(win).scrollLeft(),
-                                                i ? val : jQuery(win).scrollTop()
-                                       );
-
-                               } else {
-                                       this[ method ] = val;
-                               }
-                       });
-               } else {
-                       win = getWindow( elem );
-
-                       // Return the scroll offset
-                       return win ? ("pageXOffset" in win) ? win[ i ? "pageYOffset" : "pageXOffset" ] :
-                               jQuery.support.boxModel && win.document.documentElement[ method ] ||
-                                       win.document.body[ method ] :
-                               elem[ method ];
-               }
-       };
-});
-
-function getWindow( elem ) {
-       return ("scrollTo" in elem && elem.document) ?
-               elem :
-               elem.nodeType === 9 ?
-                       elem.defaultView || elem.parentWindow :
-                       false;
-}
-// Create innerHeight, innerWidth, outerHeight and outerWidth methods
-jQuery.each([ "Height", "Width" ], function( i, name ) {
-
-       var type = name.toLowerCase();
-
-       // innerHeight and innerWidth
-       jQuery.fn["inner" + name] = function() {
-               return this[0] ?
-                       jQuery.css( this[0], type, false, "padding" ) :
-                       null;
-       };
-
-       // outerHeight and outerWidth
-       jQuery.fn["outer" + name] = function( margin ) {
-               return this[0] ?
-                       jQuery.css( this[0], type, false, margin ? "margin" : "border" ) :
-                       null;
-       };
-
-       jQuery.fn[ type ] = function( size ) {
-               // Get window width or height
-               var elem = this[0];
-               if ( !elem ) {
-                       return size == null ? null : this;
-               }
-               
-               if ( jQuery.isFunction( size ) ) {
-                       return this.each(function( i ) {
-                               var self = jQuery( this );
-                               self[ type ]( size.call( this, i, self[ type ]() ) );
-                       });
-               }
-
-               return ("scrollTo" in elem && elem.document) ? // does it walk and quack like a window?
-                       // Everyone else use document.documentElement or document.body depending on Quirks vs Standards mode
-                       elem.document.compatMode === "CSS1Compat" && elem.document.documentElement[ "client" + name ] ||
-                       elem.document.body[ "client" + name ] :
-
-                       // Get document width or height
-                       (elem.nodeType === 9) ? // is it a document
-                               // Either scroll[Width/Height] or offset[Width/Height], whichever is greater
-                               Math.max(
-                                       elem.documentElement["client" + name],
-                                       elem.body["scroll" + name], elem.documentElement["scroll" + name],
-                                       elem.body["offset" + name], elem.documentElement["offset" + name]
-                               ) :
-
-                               // Get or set width or height on the element
-                               size === undefined ?
-                                       // Get width or height on the element
-                                       jQuery.css( elem, type ) :
-
-                                       // Set the width or height on the element (default to pixels if value is unitless)
-                                       this.css( type, typeof size === "string" ? size : size + "px" );
-       };
-
-});
-// Expose jQuery to the global object
-window.jQuery = window.$ = jQuery;
-
-})(window);
diff --git a/src/main/resources/static/javascript/jquery-3.4.1.js b/src/main/resources/static/javascript/jquery-3.4.1.js
new file mode 100644 (file)
index 0000000..773ad95
--- /dev/null
@@ -0,0 +1,10598 @@
+/*!
+ * jQuery JavaScript Library v3.4.1
+ * https://jquery.com/
+ *
+ * Includes Sizzle.js
+ * https://sizzlejs.com/
+ *
+ * Copyright JS Foundation and other contributors
+ * Released under the MIT license
+ * https://jquery.org/license
+ *
+ * Date: 2019-05-01T21:04Z
+ */
+( function( global, factory ) {
+
+       "use strict";
+
+       if ( typeof module === "object" && typeof module.exports === "object" ) {
+
+               // For CommonJS and CommonJS-like environments where a proper `window`
+               // is present, execute the factory and get jQuery.
+               // For environments that do not have a `window` with a `document`
+               // (such as Node.js), expose a factory as module.exports.
+               // This accentuates the need for the creation of a real `window`.
+               // e.g. var jQuery = require("jquery")(window);
+               // See ticket #14549 for more info.
+               module.exports = global.document ?
+                       factory( global, true ) :
+                       function( w ) {
+                               if ( !w.document ) {
+                                       throw new Error( "jQuery requires a window with a document" );
+                               }
+                               return factory( w );
+                       };
+       } else {
+               factory( global );
+       }
+
+// Pass this if window is not defined yet
+} )( typeof window !== "undefined" ? window : this, function( window, noGlobal ) {
+
+// Edge <= 12 - 13+, Firefox <=18 - 45+, IE 10 - 11, Safari 5.1 - 9+, iOS 6 - 9.1
+// throw exceptions when non-strict code (e.g., ASP.NET 4.5) accesses strict mode
+// arguments.callee.caller (trac-13335). But as of jQuery 3.0 (2016), strict mode should be common
+// enough that all such attempts are guarded in a try block.
+"use strict";
+
+var arr = [];
+
+var document = window.document;
+
+var getProto = Object.getPrototypeOf;
+
+var slice = arr.slice;
+
+var concat = arr.concat;
+
+var push = arr.push;
+
+var indexOf = arr.indexOf;
+
+var class2type = {};
+
+var toString = class2type.toString;
+
+var hasOwn = class2type.hasOwnProperty;
+
+var fnToString = hasOwn.toString;
+
+var ObjectFunctionString = fnToString.call( Object );
+
+var support = {};
+
+var isFunction = function isFunction( obj ) {
+
+      // Support: Chrome <=57, Firefox <=52
+      // In some browsers, typeof returns "function" for HTML <object> elements
+      // (i.e., `typeof document.createElement( "object" ) === "function"`).
+      // We don't want to classify *any* DOM node as a function.
+      return typeof obj === "function" && typeof obj.nodeType !== "number";
+  };
+
+
+var isWindow = function isWindow( obj ) {
+               return obj != null && obj === obj.window;
+       };
+
+
+
+
+       var preservedScriptAttributes = {
+               type: true,
+               src: true,
+               nonce: true,
+               noModule: true
+       };
+
+       function DOMEval( code, node, doc ) {
+               doc = doc || document;
+
+               var i, val,
+                       script = doc.createElement( "script" );
+
+               script.text = code;
+               if ( node ) {
+                       for ( i in preservedScriptAttributes ) {
+
+                               // Support: Firefox 64+, Edge 18+
+                               // Some browsers don't support the "nonce" property on scripts.
+                               // On the other hand, just using `getAttribute` is not enough as
+                               // the `nonce` attribute is reset to an empty string whenever it
+                               // becomes browsing-context connected.
+                               // See https://github.com/whatwg/html/issues/2369
+                               // See https://html.spec.whatwg.org/#nonce-attributes
+                               // The `node.getAttribute` check was added for the sake of
+                               // `jQuery.globalEval` so that it can fake a nonce-containing node
+                               // via an object.
+                               val = node[ i ] || node.getAttribute && node.getAttribute( i );
+                               if ( val ) {
+                                       script.setAttribute( i, val );
+                               }
+                       }
+               }
+               doc.head.appendChild( script ).parentNode.removeChild( script );
+       }
+
+
+function toType( obj ) {
+       if ( obj == null ) {
+               return obj + "";
+       }
+
+       // Support: Android <=2.3 only (functionish RegExp)
+       return typeof obj === "object" || typeof obj === "function" ?
+               class2type[ toString.call( obj ) ] || "object" :
+               typeof obj;
+}
+/* global Symbol */
+// Defining this global in .eslintrc.json would create a danger of using the global
+// unguarded in another place, it seems safer to define global only for this module
+
+
+
+var
+       version = "3.4.1",
+
+       // Define a local copy of jQuery
+       jQuery = function( selector, context ) {
+
+               // The jQuery object is actually just the init constructor 'enhanced'
+               // Need init if jQuery is called (just allow error to be thrown if not included)
+               return new jQuery.fn.init( selector, context );
+       },
+
+       // Support: Android <=4.0 only
+       // Make sure we trim BOM and NBSP
+       rtrim = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g;
+
+jQuery.fn = jQuery.prototype = {
+
+       // The current version of jQuery being used
+       jquery: version,
+
+       constructor: jQuery,
+
+       // The default length of a jQuery object is 0
+       length: 0,
+
+       toArray: function() {
+               return slice.call( this );
+       },
+
+       // Get the Nth element in the matched element set OR
+       // Get the whole matched element set as a clean array
+       get: function( num ) {
+
+               // Return all the elements in a clean array
+               if ( num == null ) {
+                       return slice.call( this );
+               }
+
+               // Return just the one element from the set
+               return num < 0 ? this[ num + this.length ] : this[ num ];
+       },
+
+       // Take an array of elements and push it onto the stack
+       // (returning the new matched element set)
+       pushStack: function( elems ) {
+
+               // Build a new jQuery matched element set
+               var ret = jQuery.merge( this.constructor(), elems );
+
+               // Add the old object onto the stack (as a reference)
+               ret.prevObject = this;
+
+               // Return the newly-formed element set
+               return ret;
+       },
+
+       // Execute a callback for every element in the matched set.
+       each: function( callback ) {
+               return jQuery.each( this, callback );
+       },
+
+       map: function( callback ) {
+               return this.pushStack( jQuery.map( this, function( elem, i ) {
+                       return callback.call( elem, i, elem );
+               } ) );
+       },
+
+       slice: function() {
+               return this.pushStack( slice.apply( this, arguments ) );
+       },
+
+       first: function() {
+               return this.eq( 0 );
+       },
+
+       last: function() {
+               return this.eq( -1 );
+       },
+
+       eq: function( i ) {
+               var len = this.length,
+                       j = +i + ( i < 0 ? len : 0 );
+               return this.pushStack( j >= 0 && j < len ? [ this[ j ] ] : [] );
+       },
+
+       end: function() {
+               return this.prevObject || this.constructor();
+       },
+
+       // For internal use only.
+       // Behaves like an Array's method, not like a jQuery method.
+       push: push,
+       sort: arr.sort,
+       splice: arr.splice
+};
+
+jQuery.extend = jQuery.fn.extend = function() {
+       var options, name, src, copy, copyIsArray, clone,
+               target = arguments[ 0 ] || {},
+               i = 1,
+               length = arguments.length,
+               deep = false;
+
+       // Handle a deep copy situation
+       if ( typeof target === "boolean" ) {
+               deep = target;
+
+               // Skip the boolean and the target
+               target = arguments[ i ] || {};
+               i++;
+       }
+
+       // Handle case when target is a string or something (possible in deep copy)
+       if ( typeof target !== "object" && !isFunction( target ) ) {
+               target = {};
+       }
+
+       // Extend jQuery itself if only one argument is passed
+       if ( i === length ) {
+               target = this;
+               i--;
+       }
+
+       for ( ; i < length; i++ ) {
+
+               // Only deal with non-null/undefined values
+               if ( ( options = arguments[ i ] ) != null ) {
+
+                       // Extend the base object
+                       for ( name in options ) {
+                               copy = options[ name ];
+
+                               // Prevent Object.prototype pollution
+                               // Prevent never-ending loop
+                               if ( name === "__proto__" || target === copy ) {
+                                       continue;
+                               }
+
+                               // Recurse if we're merging plain objects or arrays
+                               if ( deep && copy && ( jQuery.isPlainObject( copy ) ||
+                                       ( copyIsArray = Array.isArray( copy ) ) ) ) {
+                                       src = target[ name ];
+
+                                       // Ensure proper type for the source value
+                                       if ( copyIsArray && !Array.isArray( src ) ) {
+                                               clone = [];
+                                       } else if ( !copyIsArray && !jQuery.isPlainObject( src ) ) {
+                                               clone = {};
+                                       } else {
+                                               clone = src;
+                                       }
+                                       copyIsArray = false;
+
+                                       // Never move original objects, clone them
+                                       target[ name ] = jQuery.extend( deep, clone, copy );
+
+                               // Don't bring in undefined values
+                               } else if ( copy !== undefined ) {
+                                       target[ name ] = copy;
+                               }
+                       }
+               }
+       }
+
+       // Return the modified object
+       return target;
+};
+
+jQuery.extend( {
+
+       // Unique for each copy of jQuery on the page
+       expando: "jQuery" + ( version + Math.random() ).replace( /\D/g, "" ),
+
+       // Assume jQuery is ready without the ready module
+       isReady: true,
+
+       error: function( msg ) {
+               throw new Error( msg );
+       },
+
+       noop: function() {},
+
+       isPlainObject: function( obj ) {
+               var proto, Ctor;
+
+               // Detect obvious negatives
+               // Use toString instead of jQuery.type to catch host objects
+               if ( !obj || toString.call( obj ) !== "[object Object]" ) {
+                       return false;
+               }
+
+               proto = getProto( obj );
+
+               // Objects with no prototype (e.g., `Object.create( null )`) are plain
+               if ( !proto ) {
+                       return true;
+               }
+
+               // Objects with prototype are plain iff they were constructed by a global Object function
+               Ctor = hasOwn.call( proto, "constructor" ) && proto.constructor;
+               return typeof Ctor === "function" && fnToString.call( Ctor ) === ObjectFunctionString;
+       },
+
+       isEmptyObject: function( obj ) {
+               var name;
+
+               for ( name in obj ) {
+                       return false;
+               }
+               return true;
+       },
+
+       // Evaluates a script in a global context
+       globalEval: function( code, options ) {
+               DOMEval( code, { nonce: options && options.nonce } );
+       },
+
+       each: function( obj, callback ) {
+               var length, i = 0;
+
+               if ( isArrayLike( obj ) ) {
+                       length = obj.length;
+                       for ( ; i < length; i++ ) {
+                               if ( callback.call( obj[ i ], i, obj[ i ] ) === false ) {
+                                       break;
+                               }
+                       }
+               } else {
+                       for ( i in obj ) {
+                               if ( callback.call( obj[ i ], i, obj[ i ] ) === false ) {
+                                       break;
+                               }
+                       }
+               }
+
+               return obj;
+       },
+
+       // Support: Android <=4.0 only
+       trim: function( text ) {
+               return text == null ?
+                       "" :
+                       ( text + "" ).replace( rtrim, "" );
+       },
+
+       // results is for internal usage only
+       makeArray: function( arr, results ) {
+               var ret = results || [];
+
+               if ( arr != null ) {
+                       if ( isArrayLike( Object( arr ) ) ) {
+                               jQuery.merge( ret,
+                                       typeof arr === "string" ?
+                                       [ arr ] : arr
+                               );
+                       } else {
+                               push.call( ret, arr );
+                       }
+               }
+
+               return ret;
+       },
+
+       inArray: function( elem, arr, i ) {
+               return arr == null ? -1 : indexOf.call( arr, elem, i );
+       },
+
+       // Support: Android <=4.0 only, PhantomJS 1 only
+       // push.apply(_, arraylike) throws on ancient WebKit
+       merge: function( first, second ) {
+               var len = +second.length,
+                       j = 0,
+                       i = first.length;
+
+               for ( ; j < len; j++ ) {
+                       first[ i++ ] = second[ j ];
+               }
+
+               first.length = i;
+
+               return first;
+       },
+
+       grep: function( elems, callback, invert ) {
+               var callbackInverse,
+                       matches = [],
+                       i = 0,
+                       length = elems.length,
+                       callbackExpect = !invert;
+
+               // Go through the array, only saving the items
+               // that pass the validator function
+               for ( ; i < length; i++ ) {
+                       callbackInverse = !callback( elems[ i ], i );
+                       if ( callbackInverse !== callbackExpect ) {
+                               matches.push( elems[ i ] );
+                       }
+               }
+
+               return matches;
+       },
+
+       // arg is for internal usage only
+       map: function( elems, callback, arg ) {
+               var length, value,
+                       i = 0,
+                       ret = [];
+
+               // Go through the array, translating each of the items to their new values
+               if ( isArrayLike( elems ) ) {
+                       length = elems.length;
+                       for ( ; i < length; i++ ) {
+                               value = callback( elems[ i ], i, arg );
+
+                               if ( value != null ) {
+                                       ret.push( value );
+                               }
+                       }
+
+               // Go through every key on the object,
+               } else {
+                       for ( i in elems ) {
+                               value = callback( elems[ i ], i, arg );
+
+                               if ( value != null ) {
+                                       ret.push( value );
+                               }
+                       }
+               }
+
+               // Flatten any nested arrays
+               return concat.apply( [], ret );
+       },
+
+       // A global GUID counter for objects
+       guid: 1,
+
+       // jQuery.support is not used in Core but other projects attach their
+       // properties to it so it needs to exist.
+       support: support
+} );
+
+if ( typeof Symbol === "function" ) {
+       jQuery.fn[ Symbol.iterator ] = arr[ Symbol.iterator ];
+}
+
+// Populate the class2type map
+jQuery.each( "Boolean Number String Function Array Date RegExp Object Error Symbol".split( " " ),
+function( i, name ) {
+       class2type[ "[object " + name + "]" ] = name.toLowerCase();
+} );
+
+function isArrayLike( obj ) {
+
+       // Support: real iOS 8.2 only (not reproducible in simulator)
+       // `in` check used to prevent JIT error (gh-2145)
+       // hasOwn isn't used here due to false negatives
+       // regarding Nodelist length in IE
+       var length = !!obj && "length" in obj && obj.length,
+               type = toType( obj );
+
+       if ( isFunction( obj ) || isWindow( obj ) ) {
+               return false;
+       }
+
+       return type === "array" || length === 0 ||
+               typeof length === "number" && length > 0 && ( length - 1 ) in obj;
+}
+var Sizzle =
+/*!
+ * Sizzle CSS Selector Engine v2.3.4
+ * https://sizzlejs.com/
+ *
+ * Copyright JS Foundation and other contributors
+ * Released under the MIT license
+ * https://js.foundation/
+ *
+ * Date: 2019-04-08
+ */
+(function( window ) {
+
+var i,
+       support,
+       Expr,
+       getText,
+       isXML,
+       tokenize,
+       compile,
+       select,
+       outermostContext,
+       sortInput,
+       hasDuplicate,
+
+       // Local document vars
+       setDocument,
+       document,
+       docElem,
+       documentIsHTML,
+       rbuggyQSA,
+       rbuggyMatches,
+       matches,
+       contains,
+
+       // Instance-specific data
+       expando = "sizzle" + 1 * new Date(),
+       preferredDoc = window.document,
+       dirruns = 0,
+       done = 0,
+       classCache = createCache(),
+       tokenCache = createCache(),
+       compilerCache = createCache(),
+       nonnativeSelectorCache = createCache(),
+       sortOrder = function( a, b ) {
+               if ( a === b ) {
+                       hasDuplicate = true;
+               }
+               return 0;
+       },
+
+       // Instance methods
+       hasOwn = ({}).hasOwnProperty,
+       arr = [],
+       pop = arr.pop,
+       push_native = arr.push,
+       push = arr.push,
+       slice = arr.slice,
+       // Use a stripped-down indexOf as it's faster than native
+       // https://jsperf.com/thor-indexof-vs-for/5
+       indexOf = function( list, elem ) {
+               var i = 0,
+                       len = list.length;
+               for ( ; i < len; i++ ) {
+                       if ( list[i] === elem ) {
+                               return i;
+                       }
+               }
+               return -1;
+       },
+
+       booleans = "checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",
+
+       // Regular expressions
+
+       // http://www.w3.org/TR/css3-selectors/#whitespace
+       whitespace = "[\\x20\\t\\r\\n\\f]",
+
+       // http://www.w3.org/TR/CSS21/syndata.html#value-def-identifier
+       identifier = "(?:\\\\.|[\\w-]|[^\0-\\xa0])+",
+
+       // Attribute selectors: http://www.w3.org/TR/selectors/#attribute-selectors
+       attributes = "\\[" + whitespace + "*(" + identifier + ")(?:" + whitespace +
+               // Operator (capture 2)
+               "*([*^$|!~]?=)" + whitespace +
+               // "Attribute values must be CSS identifiers [capture 5] or strings [capture 3 or capture 4]"
+               "*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|(" + identifier + "))|)" + whitespace +
+               "*\\]",
+
+       pseudos = ":(" + identifier + ")(?:\\((" +
+               // To reduce the number of selectors needing tokenize in the preFilter, prefer arguments:
+               // 1. quoted (capture 3; capture 4 or capture 5)
+               "('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|" +
+               // 2. simple (capture 6)
+               "((?:\\\\.|[^\\\\()[\\]]|" + attributes + ")*)|" +
+               // 3. anything else (capture 2)
+               ".*" +
+               ")\\)|)",
+
+       // Leading and non-escaped trailing whitespace, capturing some non-whitespace characters preceding the latter
+       rwhitespace = new RegExp( whitespace + "+", "g" ),
+       rtrim = new RegExp( "^" + whitespace + "+|((?:^|[^\\\\])(?:\\\\.)*)" + whitespace + "+$", "g" ),
+
+       rcomma = new RegExp( "^" + whitespace + "*," + whitespace + "*" ),
+       rcombinators = new RegExp( "^" + whitespace + "*([>+~]|" + whitespace + ")" + whitespace + "*" ),
+       rdescend = new RegExp( whitespace + "|>" ),
+
+       rpseudo = new RegExp( pseudos ),
+       ridentifier = new RegExp( "^" + identifier + "$" ),
+
+       matchExpr = {
+               "ID": new RegExp( "^#(" + identifier + ")" ),
+               "CLASS": new RegExp( "^\\.(" + identifier + ")" ),
+               "TAG": new RegExp( "^(" + identifier + "|[*])" ),
+               "ATTR": new RegExp( "^" + attributes ),
+               "PSEUDO": new RegExp( "^" + pseudos ),
+               "CHILD": new RegExp( "^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\(" + whitespace +
+                       "*(even|odd|(([+-]|)(\\d*)n|)" + whitespace + "*(?:([+-]|)" + whitespace +
+                       "*(\\d+)|))" + whitespace + "*\\)|)", "i" ),
+               "bool": new RegExp( "^(?:" + booleans + ")$", "i" ),
+               // For use in libraries implementing .is()
+               // We use this for POS matching in `select`
+               "needsContext": new RegExp( "^" + whitespace + "*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\(" +
+                       whitespace + "*((?:-\\d)?\\d*)" + whitespace + "*\\)|)(?=[^-]|$)", "i" )
+       },
+
+       rhtml = /HTML$/i,
+       rinputs = /^(?:input|select|textarea|button)$/i,
+       rheader = /^h\d$/i,
+
+       rnative = /^[^{]+\{\s*\[native \w/,
+
+       // Easily-parseable/retrievable ID or TAG or CLASS selectors
+       rquickExpr = /^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,
+
+       rsibling = /[+~]/,
+
+       // CSS escapes
+       // http://www.w3.org/TR/CSS21/syndata.html#escaped-characters
+       runescape = new RegExp( "\\\\([\\da-f]{1,6}" + whitespace + "?|(" + whitespace + ")|.)", "ig" ),
+       funescape = function( _, escaped, escapedWhitespace ) {
+               var high = "0x" + escaped - 0x10000;
+               // NaN means non-codepoint
+               // Support: Firefox<24
+               // Workaround erroneous numeric interpretation of +"0x"
+               return high !== high || escapedWhitespace ?
+                       escaped :
+                       high < 0 ?
+                               // BMP codepoint
+                               String.fromCharCode( high + 0x10000 ) :
+                               // Supplemental Plane codepoint (surrogate pair)
+                               String.fromCharCode( high >> 10 | 0xD800, high & 0x3FF | 0xDC00 );
+       },
+
+       // CSS string/identifier serialization
+       // https://drafts.csswg.org/cssom/#common-serializing-idioms
+       rcssescape = /([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,
+       fcssescape = function( ch, asCodePoint ) {
+               if ( asCodePoint ) {
+
+                       // U+0000 NULL becomes U+FFFD REPLACEMENT CHARACTER
+                       if ( ch === "\0" ) {
+                               return "\uFFFD";
+                       }
+
+                       // Control characters and (dependent upon position) numbers get escaped as code points
+                       return ch.slice( 0, -1 ) + "\\" + ch.charCodeAt( ch.length - 1 ).toString( 16 ) + " ";
+               }
+
+               // Other potentially-special ASCII characters get backslash-escaped
+               return "\\" + ch;
+       },
+
+       // Used for iframes
+       // See setDocument()
+       // Removing the function wrapper causes a "Permission Denied"
+       // error in IE
+       unloadHandler = function() {
+               setDocument();
+       },
+
+       inDisabledFieldset = addCombinator(
+               function( elem ) {
+                       return elem.disabled === true && elem.nodeName.toLowerCase() === "fieldset";
+               },
+               { dir: "parentNode", next: "legend" }
+       );
+
+// Optimize for push.apply( _, NodeList )
+try {
+       push.apply(
+               (arr = slice.call( preferredDoc.childNodes )),
+               preferredDoc.childNodes
+       );
+       // Support: Android<4.0
+       // Detect silently failing push.apply
+       arr[ preferredDoc.childNodes.length ].nodeType;
+} catch ( e ) {
+       push = { apply: arr.length ?
+
+               // Leverage slice if possible
+               function( target, els ) {
+                       push_native.apply( target, slice.call(els) );
+               } :
+
+               // Support: IE<9
+               // Otherwise append directly
+               function( target, els ) {
+                       var j = target.length,
+                               i = 0;
+                       // Can't trust NodeList.length
+                       while ( (target[j++] = els[i++]) ) {}
+                       target.length = j - 1;
+               }
+       };
+}
+
+function Sizzle( selector, context, results, seed ) {
+       var m, i, elem, nid, match, groups, newSelector,
+               newContext = context && context.ownerDocument,
+
+               // nodeType defaults to 9, since context defaults to document
+               nodeType = context ? context.nodeType : 9;
+
+       results = results || [];
+
+       // Return early from calls with invalid selector or context
+       if ( typeof selector !== "string" || !selector ||
+               nodeType !== 1 && nodeType !== 9 && nodeType !== 11 ) {
+
+               return results;
+       }
+
+       // Try to shortcut find operations (as opposed to filters) in HTML documents
+       if ( !seed ) {
+
+               if ( ( context ? context.ownerDocument || context : preferredDoc ) !== document ) {
+                       setDocument( context );
+               }
+               context = context || document;
+
+               if ( documentIsHTML ) {
+
+                       // If the selector is sufficiently simple, try using a "get*By*" DOM method
+                       // (excepting DocumentFragment context, where the methods don't exist)
+                       if ( nodeType !== 11 && (match = rquickExpr.exec( selector )) ) {
+
+                               // ID selector
+                               if ( (m = match[1]) ) {
+
+                                       // Document context
+                                       if ( nodeType === 9 ) {
+                                               if ( (elem = context.getElementById( m )) ) {
+
+                                                       // Support: IE, Opera, Webkit
+                                                       // TODO: identify versions
+                                                       // getElementById can match elements by name instead of ID
+                                                       if ( elem.id === m ) {
+                                                               results.push( elem );
+                                                               return results;
+                                                       }
+                                               } else {
+                                                       return results;
+                                               }
+
+                                       // Element context
+                                       } else {
+
+                                               // Support: IE, Opera, Webkit
+                                               // TODO: identify versions
+                                               // getElementById can match elements by name instead of ID
+                                               if ( newContext && (elem = newContext.getElementById( m )) &&
+                                                       contains( context, elem ) &&
+                                                       elem.id === m ) {
+
+                                                       results.push( elem );
+                                                       return results;
+                                               }
+                                       }
+
+                               // Type selector
+                               } else if ( match[2] ) {
+                                       push.apply( results, context.getElementsByTagName( selector ) );
+                                       return results;
+
+                               // Class selector
+                               } else if ( (m = match[3]) && support.getElementsByClassName &&
+                                       context.getElementsByClassName ) {
+
+                                       push.apply( results, context.getElementsByClassName( m ) );
+                                       return results;
+                               }
+                       }
+
+                       // Take advantage of querySelectorAll
+                       if ( support.qsa &&
+                               !nonnativeSelectorCache[ selector + " " ] &&
+                               (!rbuggyQSA || !rbuggyQSA.test( selector )) &&
+
+                               // Support: IE 8 only
+                               // Exclude object elements
+                               (nodeType !== 1 || context.nodeName.toLowerCase() !== "object") ) {
+
+                               newSelector = selector;
+                               newContext = context;
+
+                               // qSA considers elements outside a scoping root when evaluating child or
+                               // descendant combinators, which is not what we want.
+                               // In such cases, we work around the behavior by prefixing every selector in the
+                               // list with an ID selector referencing the scope context.
+                               // Thanks to Andrew Dupont for this technique.
+                               if ( nodeType === 1 && rdescend.test( selector ) ) {
+
+                                       // Capture the context ID, setting it first if necessary
+                                       if ( (nid = context.getAttribute( "id" )) ) {
+                                               nid = nid.replace( rcssescape, fcssescape );
+                                       } else {
+                                               context.setAttribute( "id", (nid = expando) );
+                                       }
+
+                                       // Prefix every selector in the list
+                                       groups = tokenize( selector );
+                                       i = groups.length;
+                                       while ( i-- ) {
+                                               groups[i] = "#" + nid + " " + toSelector( groups[i] );
+                                       }
+                                       newSelector = groups.join( "," );
+
+                                       // Expand context for sibling selectors
+                                       newContext = rsibling.test( selector ) && testContext( context.parentNode ) ||
+                                               context;
+                               }
+
+                               try {
+                                       push.apply( results,
+                                               newContext.querySelectorAll( newSelector )
+                                       );
+                                       return results;
+                               } catch ( qsaError ) {
+                                       nonnativeSelectorCache( selector, true );
+                               } finally {
+                                       if ( nid === expando ) {
+                                               context.removeAttribute( "id" );
+                                       }
+                               }
+                       }
+               }
+       }
+
+       // All others
+       return select( selector.replace( rtrim, "$1" ), context, results, seed );
+}
+
+/**
+ * Create key-value caches of limited size
+ * @returns {function(string, object)} Returns the Object data after storing it on itself with
+ *     property name the (space-suffixed) string and (if the cache is larger than Expr.cacheLength)
+ *     deleting the oldest entry
+ */
+function createCache() {
+       var keys = [];
+
+       function cache( key, value ) {
+               // Use (key + " ") to avoid collision with native prototype properties (see Issue #157)
+               if ( keys.push( key + " " ) > Expr.cacheLength ) {
+                       // Only keep the most recent entries
+                       delete cache[ keys.shift() ];
+               }
+               return (cache[ key + " " ] = value);
+       }
+       return cache;
+}
+
+/**
+ * Mark a function for special use by Sizzle
+ * @param {Function} fn The function to mark
+ */
+function markFunction( fn ) {
+       fn[ expando ] = true;
+       return fn;
+}
+
+/**
+ * Support testing using an element
+ * @param {Function} fn Passed the created element and returns a boolean result
+ */
+function assert( fn ) {
+       var el = document.createElement("fieldset");
+
+       try {
+               return !!fn( el );
+       } catch (e) {
+               return false;
+       } finally {
+               // Remove from its parent by default
+               if ( el.parentNode ) {
+                       el.parentNode.removeChild( el );
+               }
+               // release memory in IE
+               el = null;
+       }
+}
+
+/**
+ * Adds the same handler for all of the specified attrs
+ * @param {String} attrs Pipe-separated list of attributes
+ * @param {Function} handler The method that will be applied
+ */
+function addHandle( attrs, handler ) {
+       var arr = attrs.split("|"),
+               i = arr.length;
+
+       while ( i-- ) {
+               Expr.attrHandle[ arr[i] ] = handler;
+       }
+}
+
+/**
+ * Checks document order of two siblings
+ * @param {Element} a
+ * @param {Element} b
+ * @returns {Number} Returns less than 0 if a precedes b, greater than 0 if a follows b
+ */
+function siblingCheck( a, b ) {
+       var cur = b && a,
+               diff = cur && a.nodeType === 1 && b.nodeType === 1 &&
+                       a.sourceIndex - b.sourceIndex;
+
+       // Use IE sourceIndex if available on both nodes
+       if ( diff ) {
+               return diff;
+       }
+
+       // Check if b follows a
+       if ( cur ) {
+               while ( (cur = cur.nextSibling) ) {
+                       if ( cur === b ) {
+                               return -1;
+                       }
+               }
+       }
+
+       return a ? 1 : -1;
+}
+
+/**
+ * Returns a function to use in pseudos for input types
+ * @param {String} type
+ */
+function createInputPseudo( type ) {
+       return function( elem ) {
+               var name = elem.nodeName.toLowerCase();
+               return name === "input" && elem.type === type;
+       };
+}
+
+/**
+ * Returns a function to use in pseudos for buttons
+ * @param {String} type
+ */
+function createButtonPseudo( type ) {
+       return function( elem ) {
+               var name = elem.nodeName.toLowerCase();
+               return (name === "input" || name === "button") && elem.type === type;
+       };
+}
+
+/**
+ * Returns a function to use in pseudos for :enabled/:disabled
+ * @param {Boolean} disabled true for :disabled; false for :enabled
+ */
+function createDisabledPseudo( disabled ) {
+
+       // Known :disabled false positives: fieldset[disabled] > legend:nth-of-type(n+2) :can-disable
+       return function( elem ) {
+
+               // Only certain elements can match :enabled or :disabled
+               // https://html.spec.whatwg.org/multipage/scripting.html#selector-enabled
+               // https://html.spec.whatwg.org/multipage/scripting.html#selector-disabled
+               if ( "form" in elem ) {
+
+                       // Check for inherited disabledness on relevant non-disabled elements:
+                       // * listed form-associated elements in a disabled fieldset
+                       //   https://html.spec.whatwg.org/multipage/forms.html#category-listed
+                       //   https://html.spec.whatwg.org/multipage/forms.html#concept-fe-disabled
+                       // * option elements in a disabled optgroup
+                       //   https://html.spec.whatwg.org/multipage/forms.html#concept-option-disabled
+                       // All such elements have a "form" property.
+                       if ( elem.parentNode && elem.disabled === false ) {
+
+                               // Option elements defer to a parent optgroup if present
+                               if ( "label" in elem ) {
+                                       if ( "label" in elem.parentNode ) {
+                                               return elem.parentNode.disabled === disabled;
+                                       } else {
+                                               return elem.disabled === disabled;
+                                       }
+                               }
+
+                               // Support: IE 6 - 11
+                               // Use the isDisabled shortcut property to check for disabled fieldset ancestors
+                               return elem.isDisabled === disabled ||
+
+                                       // Where there is no isDisabled, check manually
+                                       /* jshint -W018 */
+                                       elem.isDisabled !== !disabled &&
+                                               inDisabledFieldset( elem ) === disabled;
+                       }
+
+                       return elem.disabled === disabled;
+
+               // Try to winnow out elements that can't be disabled before trusting the disabled property.
+               // Some victims get caught in our net (label, legend, menu, track), but it shouldn't
+               // even exist on them, let alone have a boolean value.
+               } else if ( "label" in elem ) {
+                       return elem.disabled === disabled;
+               }
+
+               // Remaining elements are neither :enabled nor :disabled
+               return false;
+       };
+}
+
+/**
+ * Returns a function to use in pseudos for positionals
+ * @param {Function} fn
+ */
+function createPositionalPseudo( fn ) {
+       return markFunction(function( argument ) {
+               argument = +argument;
+               return markFunction(function( seed, matches ) {
+                       var j,
+                               matchIndexes = fn( [], seed.length, argument ),
+                               i = matchIndexes.length;
+
+                       // Match elements found at the specified indexes
+                       while ( i-- ) {
+                               if ( seed[ (j = matchIndexes[i]) ] ) {
+                                       seed[j] = !(matches[j] = seed[j]);
+                               }
+                       }
+               });
+       });
+}
+
+/**
+ * Checks a node for validity as a Sizzle context
+ * @param {Element|Object=} context
+ * @returns {Element|Object|Boolean} The input node if acceptable, otherwise a falsy value
+ */
+function testContext( context ) {
+       return context && typeof context.getElementsByTagName !== "undefined" && context;
+}
+
+// Expose support vars for convenience
+support = Sizzle.support = {};
+
+/**
+ * Detects XML nodes
+ * @param {Element|Object} elem An element or a document
+ * @returns {Boolean} True iff elem is a non-HTML XML node
+ */
+isXML = Sizzle.isXML = function( elem ) {
+       var namespace = elem.namespaceURI,
+               docElem = (elem.ownerDocument || elem).documentElement;
+
+       // Support: IE <=8
+       // Assume HTML when documentElement doesn't yet exist, such as inside loading iframes
+       // https://bugs.jquery.com/ticket/4833
+       return !rhtml.test( namespace || docElem && docElem.nodeName || "HTML" );
+};
+
+/**
+ * Sets document-related variables once based on the current document
+ * @param {Element|Object} [doc] An element or document object to use to set the document
+ * @returns {Object} Returns the current document
+ */
+setDocument = Sizzle.setDocument = function( node ) {
+       var hasCompare, subWindow,
+               doc = node ? node.ownerDocument || node : preferredDoc;
+
+       // Return early if doc is invalid or already selected
+       if ( doc === document || doc.nodeType !== 9 || !doc.documentElement ) {
+               return document;
+       }
+
+       // Update global variables
+       document = doc;
+       docElem = document.documentElement;
+       documentIsHTML = !isXML( document );
+
+       // Support: IE 9-11, Edge
+       // Accessing iframe documents after unload throws "permission denied" errors (jQuery #13936)
+       if ( preferredDoc !== document &&
+               (subWindow = document.defaultView) && subWindow.top !== subWindow ) {
+
+               // Support: IE 11, Edge
+               if ( subWindow.addEventListener ) {
+                       subWindow.addEventListener( "unload", unloadHandler, false );
+
+               // Support: IE 9 - 10 only
+               } else if ( subWindow.attachEvent ) {
+                       subWindow.attachEvent( "onunload", unloadHandler );
+               }
+       }
+
+       /* Attributes
+       ---------------------------------------------------------------------- */
+
+       // Support: IE<8
+       // Verify that getAttribute really returns attributes and not properties
+       // (excepting IE8 booleans)
+       support.attributes = assert(function( el ) {
+               el.className = "i";
+               return !el.getAttribute("className");
+       });
+
+       /* getElement(s)By*
+       ---------------------------------------------------------------------- */
+
+       // Check if getElementsByTagName("*") returns only elements
+       support.getElementsByTagName = assert(function( el ) {
+               el.appendChild( document.createComment("") );
+               return !el.getElementsByTagName("*").length;
+       });
+
+       // Support: IE<9
+       support.getElementsByClassName = rnative.test( document.getElementsByClassName );
+
+       // Support: IE<10
+       // Check if getElementById returns elements by name
+       // The broken getElementById methods don't pick up programmatically-set names,
+       // so use a roundabout getElementsByName test
+       support.getById = assert(function( el ) {
+               docElem.appendChild( el ).id = expando;
+               return !document.getElementsByName || !document.getElementsByName( expando ).length;
+       });
+
+       // ID filter and find
+       if ( support.getById ) {
+               Expr.filter["ID"] = function( id ) {
+                       var attrId = id.replace( runescape, funescape );
+                       return function( elem ) {
+                               return elem.getAttribute("id") === attrId;
+                       };
+               };
+               Expr.find["ID"] = function( id, context ) {
+                       if ( typeof context.getElementById !== "undefined" && documentIsHTML ) {
+                               var elem = context.getElementById( id );
+                               return elem ? [ elem ] : [];
+                       }
+               };
+       } else {
+               Expr.filter["ID"] =  function( id ) {
+                       var attrId = id.replace( runescape, funescape );
+                       return function( elem ) {
+                               var node = typeof elem.getAttributeNode !== "undefined" &&
+                                       elem.getAttributeNode("id");
+                               return node && node.value === attrId;
+                       };
+               };
+
+               // Support: IE 6 - 7 only
+               // getElementById is not reliable as a find shortcut
+               Expr.find["ID"] = function( id, context ) {
+                       if ( typeof context.getElementById !== "undefined" && documentIsHTML ) {
+                               var node, i, elems,
+                                       elem = context.getElementById( id );
+
+                               if ( elem ) {
+
+                                       // Verify the id attribute
+                                       node = elem.getAttributeNode("id");
+                                       if ( node && node.value === id ) {
+                                               return [ elem ];
+                                       }
+
+                                       // Fall back on getElementsByName
+                                       elems = context.getElementsByName( id );
+                                       i = 0;
+                                       while ( (elem = elems[i++]) ) {
+                                               node = elem.getAttributeNode("id");
+                                               if ( node && node.value === id ) {
+                                                       return [ elem ];
+                                               }
+                                       }
+                               }
+
+                               return [];
+                       }
+               };
+       }
+
+       // Tag
+       Expr.find["TAG"] = support.getElementsByTagName ?
+               function( tag, context ) {
+                       if ( typeof context.getElementsByTagName !== "undefined" ) {
+                               return context.getElementsByTagName( tag );
+
+                       // DocumentFragment nodes don't have gEBTN
+                       } else if ( support.qsa ) {
+                               return context.querySelectorAll( tag );
+                       }
+               } :
+
+               function( tag, context ) {
+                       var elem,
+                               tmp = [],
+                               i = 0,
+                               // By happy coincidence, a (broken) gEBTN appears on DocumentFragment nodes too
+                               results = context.getElementsByTagName( tag );
+
+                       // Filter out possible comments
+                       if ( tag === "*" ) {
+                               while ( (elem = results[i++]) ) {
+                                       if ( elem.nodeType === 1 ) {
+                                               tmp.push( elem );
+                                       }
+                               }
+
+                               return tmp;
+                       }
+                       return results;
+               };
+
+       // Class
+       Expr.find["CLASS"] = support.getElementsByClassName && function( className, context ) {
+               if ( typeof context.getElementsByClassName !== "undefined" && documentIsHTML ) {
+                       return context.getElementsByClassName( className );
+               }
+       };
+
+       /* QSA/matchesSelector
+       ---------------------------------------------------------------------- */
+
+       // QSA and matchesSelector support
+
+       // matchesSelector(:active) reports false when true (IE9/Opera 11.5)
+       rbuggyMatches = [];
+
+       // qSa(:focus) reports false when true (Chrome 21)
+       // We allow this because of a bug in IE8/9 that throws an error
+       // whenever `document.activeElement` is accessed on an iframe
+       // So, we allow :focus to pass through QSA all the time to avoid the IE error
+       // See https://bugs.jquery.com/ticket/13378
+       rbuggyQSA = [];
+
+       if ( (support.qsa = rnative.test( document.querySelectorAll )) ) {
+               // Build QSA regex
+               // Regex strategy adopted from Diego Perini
+               assert(function( el ) {
+                       // Select is set to empty string on purpose
+                       // This is to test IE's treatment of not explicitly
+                       // setting a boolean content attribute,
+                       // since its presence should be enough
+                       // https://bugs.jquery.com/ticket/12359
+                       docElem.appendChild( el ).innerHTML = "<a id='" + expando + "'></a>" +
+                               "<select id='" + expando + "-\r\\' msallowcapture=''>" +
+                               "<option selected=''></option></select>";
+
+                       // Support: IE8, Opera 11-12.16
+                       // Nothing should be selected when empty strings follow ^= or $= or *=
+                       // The test attribute must be unknown in Opera but "safe" for WinRT
+                       // https://msdn.microsoft.com/en-us/library/ie/hh465388.aspx#attribute_section
+                       if ( el.querySelectorAll("[msallowcapture^='']").length ) {
+                               rbuggyQSA.push( "[*^$]=" + whitespace + "*(?:''|\"\")" );
+                       }
+
+                       // Support: IE8
+                       // Boolean attributes and "value" are not treated correctly
+                       if ( !el.querySelectorAll("[selected]").length ) {
+                               rbuggyQSA.push( "\\[" + whitespace + "*(?:value|" + booleans + ")" );
+                       }
+
+                       // Support: Chrome<29, Android<4.4, Safari<7.0+, iOS<7.0+, PhantomJS<1.9.8+
+                       if ( !el.querySelectorAll( "[id~=" + expando + "-]" ).length ) {
+                               rbuggyQSA.push("~=");
+                       }
+
+                       // Webkit/Opera - :checked should return selected option elements
+                       // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked
+                       // IE8 throws error here and will not see later tests
+                       if ( !el.querySelectorAll(":checked").length ) {
+                               rbuggyQSA.push(":checked");
+                       }
+
+                       // Support: Safari 8+, iOS 8+
+                       // https://bugs.webkit.org/show_bug.cgi?id=136851
+                       // In-page `selector#id sibling-combinator selector` fails
+                       if ( !el.querySelectorAll( "a#" + expando + "+*" ).length ) {
+                               rbuggyQSA.push(".#.+[+~]");
+                       }
+               });
+
+               assert(function( el ) {
+                       el.innerHTML = "<a href='' disabled='disabled'></a>" +
+                               "<select disabled='disabled'><option/></select>";
+
+                       // Support: Windows 8 Native Apps
+                       // The type and name attributes are restricted during .innerHTML assignment
+                       var input = document.createElement("input");
+                       input.setAttribute( "type", "hidden" );
+                       el.appendChild( input ).setAttribute( "name", "D" );
+
+                       // Support: IE8
+                       // Enforce case-sensitivity of name attribute
+                       if ( el.querySelectorAll("[name=d]").length ) {
+                               rbuggyQSA.push( "name" + whitespace + "*[*^$|!~]?=" );
+                       }
+
+                       // FF 3.5 - :enabled/:disabled and hidden elements (hidden elements are still enabled)
+                       // IE8 throws error here and will not see later tests
+                       if ( el.querySelectorAll(":enabled").length !== 2 ) {
+                               rbuggyQSA.push( ":enabled", ":disabled" );
+                       }
+
+                       // Support: IE9-11+
+                       // IE's :disabled selector does not pick up the children of disabled fieldsets
+                       docElem.appendChild( el ).disabled = true;
+                       if ( el.querySelectorAll(":disabled").length !== 2 ) {
+                               rbuggyQSA.push( ":enabled", ":disabled" );
+                       }
+
+                       // Opera 10-11 does not throw on post-comma invalid pseudos
+                       el.querySelectorAll("*,:x");
+                       rbuggyQSA.push(",.*:");
+               });
+       }
+
+       if ( (support.matchesSelector = rnative.test( (matches = docElem.matches ||
+               docElem.webkitMatchesSelector ||
+               docElem.mozMatchesSelector ||
+               docElem.oMatchesSelector ||
+               docElem.msMatchesSelector) )) ) {
+
+               assert(function( el ) {
+                       // Check to see if it's possible to do matchesSelector
+                       // on a disconnected node (IE 9)
+                       support.disconnectedMatch = matches.call( el, "*" );
+
+                       // This should fail with an exception
+                       // Gecko does not error, returns false instead
+                       matches.call( el, "[s!='']:x" );
+                       rbuggyMatches.push( "!=", pseudos );
+               });
+       }
+
+       rbuggyQSA = rbuggyQSA.length && new RegExp( rbuggyQSA.join("|") );
+       rbuggyMatches = rbuggyMatches.length && new RegExp( rbuggyMatches.join("|") );
+
+       /* Contains
+       ---------------------------------------------------------------------- */
+       hasCompare = rnative.test( docElem.compareDocumentPosition );
+
+       // Element contains another
+       // Purposefully self-exclusive
+       // As in, an element does not contain itself
+       contains = hasCompare || rnative.test( docElem.contains ) ?
+               function( a, b ) {
+                       var adown = a.nodeType === 9 ? a.documentElement : a,
+                               bup = b && b.parentNode;
+                       return a === bup || !!( bup && bup.nodeType === 1 && (
+                               adown.contains ?
+                                       adown.contains( bup ) :
+                                       a.compareDocumentPosition && a.compareDocumentPosition( bup ) & 16
+                       ));
+               } :
+               function( a, b ) {
+                       if ( b ) {
+                               while ( (b = b.parentNode) ) {
+                                       if ( b === a ) {
+                                               return true;
+                                       }
+                               }
+                       }
+                       return false;
+               };
+
+       /* Sorting
+       ---------------------------------------------------------------------- */
+
+       // Document order sorting
+       sortOrder = hasCompare ?
+       function( a, b ) {
+
+               // Flag for duplicate removal
+               if ( a === b ) {
+                       hasDuplicate = true;
+                       return 0;
+               }
+
+               // Sort on method existence if only one input has compareDocumentPosition
+               var compare = !a.compareDocumentPosition - !b.compareDocumentPosition;
+               if ( compare ) {
+                       return compare;
+               }
+
+               // Calculate position if both inputs belong to the same document
+               compare = ( a.ownerDocument || a ) === ( b.ownerDocument || b ) ?
+                       a.compareDocumentPosition( b ) :
+
+                       // Otherwise we know they are disconnected
+                       1;
+
+               // Disconnected nodes
+               if ( compare & 1 ||
+                       (!support.sortDetached && b.compareDocumentPosition( a ) === compare) ) {
+
+                       // Choose the first element that is related to our preferred document
+                       if ( a === document || a.ownerDocument === preferredDoc && contains(preferredDoc, a) ) {
+                               return -1;
+                       }
+                       if ( b === document || b.ownerDocument === preferredDoc && contains(preferredDoc, b) ) {
+                               return 1;
+                       }
+
+                       // Maintain original order
+                       return sortInput ?
+                               ( indexOf( sortInput, a ) - indexOf( sortInput, b ) ) :
+                               0;
+               }
+
+               return compare & 4 ? -1 : 1;
+       } :
+       function( a, b ) {
+               // Exit early if the nodes are identical
+               if ( a === b ) {
+                       hasDuplicate = true;
+                       return 0;
+               }
+
+               var cur,
+                       i = 0,
+                       aup = a.parentNode,
+                       bup = b.parentNode,
+                       ap = [ a ],
+                       bp = [ b ];
+
+               // Parentless nodes are either documents or disconnected
+               if ( !aup || !bup ) {
+                       return a === document ? -1 :
+                               b === document ? 1 :
+                               aup ? -1 :
+                               bup ? 1 :
+                               sortInput ?
+                               ( indexOf( sortInput, a ) - indexOf( sortInput, b ) ) :
+                               0;
+
+               // If the nodes are siblings, we can do a quick check
+               } else if ( aup === bup ) {
+                       return siblingCheck( a, b );
+               }
+
+               // Otherwise we need full lists of their ancestors for comparison
+               cur = a;
+               while ( (cur = cur.parentNode) ) {
+                       ap.unshift( cur );
+               }
+               cur = b;
+               while ( (cur = cur.parentNode) ) {
+                       bp.unshift( cur );
+               }
+
+               // Walk down the tree looking for a discrepancy
+               while ( ap[i] === bp[i] ) {
+                       i++;
+               }
+
+               return i ?
+                       // Do a sibling check if the nodes have a common ancestor
+                       siblingCheck( ap[i], bp[i] ) :
+
+                       // Otherwise nodes in our document sort first
+                       ap[i] === preferredDoc ? -1 :
+                       bp[i] === preferredDoc ? 1 :
+                       0;
+       };
+
+       return document;
+};
+
+Sizzle.matches = function( expr, elements ) {
+       return Sizzle( expr, null, null, elements );
+};
+
+Sizzle.matchesSelector = function( elem, expr ) {
+       // Set document vars if needed
+       if ( ( elem.ownerDocument || elem ) !== document ) {
+               setDocument( elem );
+       }
+
+       if ( support.matchesSelector && documentIsHTML &&
+               !nonnativeSelectorCache[ expr + " " ] &&
+               ( !rbuggyMatches || !rbuggyMatches.test( expr ) ) &&
+               ( !rbuggyQSA     || !rbuggyQSA.test( expr ) ) ) {
+
+               try {
+                       var ret = matches.call( elem, expr );
+
+                       // IE 9's matchesSelector returns false on disconnected nodes
+                       if ( ret || support.disconnectedMatch ||
+                                       // As well, disconnected nodes are said to be in a document
+                                       // fragment in IE 9
+                                       elem.document && elem.document.nodeType !== 11 ) {
+                               return ret;
+                       }
+               } catch (e) {
+                       nonnativeSelectorCache( expr, true );
+               }
+       }
+
+       return Sizzle( expr, document, null, [ elem ] ).length > 0;
+};
+
+Sizzle.contains = function( context, elem ) {
+       // Set document vars if needed
+       if ( ( context.ownerDocument || context ) !== document ) {
+               setDocument( context );
+       }
+       return contains( context, elem );
+};
+
+Sizzle.attr = function( elem, name ) {
+       // Set document vars if needed
+       if ( ( elem.ownerDocument || elem ) !== document ) {
+               setDocument( elem );
+       }
+
+       var fn = Expr.attrHandle[ name.toLowerCase() ],
+               // Don't get fooled by Object.prototype properties (jQuery #13807)
+               val = fn && hasOwn.call( Expr.attrHandle, name.toLowerCase() ) ?
+                       fn( elem, name, !documentIsHTML ) :
+                       undefined;
+
+       return val !== undefined ?
+               val :
+               support.attributes || !documentIsHTML ?
+                       elem.getAttribute( name ) :
+                       (val = elem.getAttributeNode(name)) && val.specified ?
+                               val.value :
+                               null;
+};
+
+Sizzle.escape = function( sel ) {
+       return (sel + "").replace( rcssescape, fcssescape );
+};
+
+Sizzle.error = function( msg ) {
+       throw new Error( "Syntax error, unrecognized expression: " + msg );
+};
+
+/**
+ * Document sorting and removing duplicates
+ * @param {ArrayLike} results
+ */
+Sizzle.uniqueSort = function( results ) {
+       var elem,
+               duplicates = [],
+               j = 0,
+               i = 0;
+
+       // Unless we *know* we can detect duplicates, assume their presence
+       hasDuplicate = !support.detectDuplicates;
+       sortInput = !support.sortStable && results.slice( 0 );
+       results.sort( sortOrder );
+
+       if ( hasDuplicate ) {
+               while ( (elem = results[i++]) ) {
+                       if ( elem === results[ i ] ) {
+                               j = duplicates.push( i );
+                       }
+               }
+               while ( j-- ) {
+                       results.splice( duplicates[ j ], 1 );
+               }
+       }
+
+       // Clear input after sorting to release objects
+       // See https://github.com/jquery/sizzle/pull/225
+       sortInput = null;
+
+       return results;
+};
+
+/**
+ * Utility function for retrieving the text value of an array of DOM nodes
+ * @param {Array|Element} elem
+ */
+getText = Sizzle.getText = function( elem ) {
+       var node,
+               ret = "",
+               i = 0,
+               nodeType = elem.nodeType;
+
+       if ( !nodeType ) {
+               // If no nodeType, this is expected to be an array
+               while ( (node = elem[i++]) ) {
+                       // Do not traverse comment nodes
+                       ret += getText( node );
+               }
+       } else if ( nodeType === 1 || nodeType === 9 || nodeType === 11 ) {
+               // Use textContent for elements
+               // innerText usage removed for consistency of new lines (jQuery #11153)
+               if ( typeof elem.textContent === "string" ) {
+                       return elem.textContent;
+               } else {
+                       // Traverse its children
+                       for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) {
+                               ret += getText( elem );
+                       }
+               }
+       } else if ( nodeType === 3 || nodeType === 4 ) {
+               return elem.nodeValue;
+       }
+       // Do not include comment or processing instruction nodes
+
+       return ret;
+};
+
+Expr = Sizzle.selectors = {
+
+       // Can be adjusted by the user
+       cacheLength: 50,
+
+       createPseudo: markFunction,
+
+       match: matchExpr,
+
+       attrHandle: {},
+
+       find: {},
+
+       relative: {
+               ">": { dir: "parentNode", first: true },
+               " ": { dir: "parentNode" },
+               "+": { dir: "previousSibling", first: true },
+               "~": { dir: "previousSibling" }
+       },
+
+       preFilter: {
+               "ATTR": function( match ) {
+                       match[1] = match[1].replace( runescape, funescape );
+
+                       // Move the given value to match[3] whether quoted or unquoted
+                       match[3] = ( match[3] || match[4] || match[5] || "" ).replace( runescape, funescape );
+
+                       if ( match[2] === "~=" ) {
+                               match[3] = " " + match[3] + " ";
+                       }
+
+                       return match.slice( 0, 4 );
+               },
+
+               "CHILD": function( match ) {
+                       /* matches from matchExpr["CHILD"]
+                               1 type (only|nth|...)
+                               2 what (child|of-type)
+                               3 argument (even|odd|\d*|\d*n([+-]\d+)?|...)
+                               4 xn-component of xn+y argument ([+-]?\d*n|)
+                               5 sign of xn-component
+                               6 x of xn-component
+                               7 sign of y-component
+                               8 y of y-component
+                       */
+                       match[1] = match[1].toLowerCase();
+
+                       if ( match[1].slice( 0, 3 ) === "nth" ) {
+                               // nth-* requires argument
+                               if ( !match[3] ) {
+                                       Sizzle.error( match[0] );
+                               }
+
+                               // numeric x and y parameters for Expr.filter.CHILD
+                               // remember that false/true cast respectively to 0/1
+                               match[4] = +( match[4] ? match[5] + (match[6] || 1) : 2 * ( match[3] === "even" || match[3] === "odd" ) );
+                               match[5] = +( ( match[7] + match[8] ) || match[3] === "odd" );
+
+                       // other types prohibit arguments
+                       } else if ( match[3] ) {
+                               Sizzle.error( match[0] );
+                       }
+
+                       return match;
+               },
+
+               "PSEUDO": function( match ) {
+                       var excess,
+                               unquoted = !match[6] && match[2];
+
+                       if ( matchExpr["CHILD"].test( match[0] ) ) {
+                               return null;
+                       }
+
+                       // Accept quoted arguments as-is
+                       if ( match[3] ) {
+                               match[2] = match[4] || match[5] || "";
+
+                       // Strip excess characters from unquoted arguments
+                       } else if ( unquoted && rpseudo.test( unquoted ) &&
+                               // Get excess from tokenize (recursively)
+                               (excess = tokenize( unquoted, true )) &&
+                               // advance to the next closing parenthesis
+                               (excess = unquoted.indexOf( ")", unquoted.length - excess ) - unquoted.length) ) {
+
+                               // excess is a negative index
+                               match[0] = match[0].slice( 0, excess );
+                               match[2] = unquoted.slice( 0, excess );
+                       }
+
+                       // Return only captures needed by the pseudo filter method (type and argument)
+                       return match.slice( 0, 3 );
+               }
+       },
+
+       filter: {
+
+               "TAG": function( nodeNameSelector ) {
+                       var nodeName = nodeNameSelector.replace( runescape, funescape ).toLowerCase();
+                       return nodeNameSelector === "*" ?
+                               function() { return true; } :
+                               function( elem ) {
+                                       return elem.nodeName && elem.nodeName.toLowerCase() === nodeName;
+                               };
+               },
+
+               "CLASS": function( className ) {
+                       var pattern = classCache[ className + " " ];
+
+                       return pattern ||
+                               (pattern = new RegExp( "(^|" + whitespace + ")" + className + "(" + whitespace + "|$)" )) &&
+                               classCache( className, function( elem ) {
+                                       return pattern.test( typeof elem.className === "string" && elem.className || typeof elem.getAttribute !== "undefined" && elem.getAttribute("class") || "" );
+                               });
+               },
+
+               "ATTR": function( name, operator, check ) {
+                       return function( elem ) {
+                               var result = Sizzle.attr( elem, name );
+
+                               if ( result == null ) {
+                                       return operator === "!=";
+                               }
+                               if ( !operator ) {
+                                       return true;
+                               }
+
+                               result += "";
+
+                               return operator === "=" ? result === check :
+                                       operator === "!=" ? result !== check :
+                                       operator === "^=" ? check && result.indexOf( check ) === 0 :
+                                       operator === "*=" ? check && result.indexOf( check ) > -1 :
+                                       operator === "$=" ? check && result.slice( -check.length ) === check :
+                                       operator === "~=" ? ( " " + result.replace( rwhitespace, " " ) + " " ).indexOf( check ) > -1 :
+                                       operator === "|=" ? result === check || result.slice( 0, check.length + 1 ) === check + "-" :
+                                       false;
+                       };
+               },
+
+               "CHILD": function( type, what, argument, first, last ) {
+                       var simple = type.slice( 0, 3 ) !== "nth",
+                               forward = type.slice( -4 ) !== "last",
+                               ofType = what === "of-type";
+
+                       return first === 1 && last === 0 ?
+
+                               // Shortcut for :nth-*(n)
+                               function( elem ) {
+                                       return !!elem.parentNode;
+                               } :
+
+                               function( elem, context, xml ) {
+                                       var cache, uniqueCache, outerCache, node, nodeIndex, start,
+                                               dir = simple !== forward ? "nextSibling" : "previousSibling",
+                                               parent = elem.parentNode,
+                                               name = ofType && elem.nodeName.toLowerCase(),
+                                               useCache = !xml && !ofType,
+                                               diff = false;
+
+                                       if ( parent ) {
+
+                                               // :(first|last|only)-(child|of-type)
+                                               if ( simple ) {
+                                                       while ( dir ) {
+                                                               node = elem;
+                                                               while ( (node = node[ dir ]) ) {
+                                                                       if ( ofType ?
+                                                                               node.nodeName.toLowerCase() === name :
+                                                                               node.nodeType === 1 ) {
+
+                                                                               return false;
+                                                                       }
+                                                               }
+                                                               // Reverse direction for :only-* (if we haven't yet done so)
+                                                               start = dir = type === "only" && !start && "nextSibling";
+                                                       }
+                                                       return true;
+                                               }
+
+                                               start = [ forward ? parent.firstChild : parent.lastChild ];
+
+                                               // non-xml :nth-child(...) stores cache data on `parent`
+                                               if ( forward && useCache ) {
+
+                                                       // Seek `elem` from a previously-cached index
+
+                                                       // ...in a gzip-friendly way
+                                                       node = parent;
+                                                       outerCache = node[ expando ] || (node[ expando ] = {});
+
+                                                       // Support: IE <9 only
+                                                       // Defend against cloned attroperties (jQuery gh-1709)
+                                                       uniqueCache = outerCache[ node.uniqueID ] ||
+                                                               (outerCache[ node.uniqueID ] = {});
+
+                                                       cache = uniqueCache[ type ] || [];
+                                                       nodeIndex = cache[ 0 ] === dirruns && cache[ 1 ];
+                                                       diff = nodeIndex && cache[ 2 ];
+                                                       node = nodeIndex && parent.childNodes[ nodeIndex ];
+
+                                                       while ( (node = ++nodeIndex && node && node[ dir ] ||
+
+                                                               // Fallback to seeking `elem` from the start
+                                                               (diff = nodeIndex = 0) || start.pop()) ) {
+
+                                                               // When found, cache indexes on `parent` and break
+                                                               if ( node.nodeType === 1 && ++diff && node === elem ) {
+                                                                       uniqueCache[ type ] = [ dirruns, nodeIndex, diff ];
+                                                                       break;
+                                                               }
+                                                       }
+
+                                               } else {
+                                                       // Use previously-cached element index if available
+                                                       if ( useCache ) {
+                                                               // ...in a gzip-friendly way
+                                                               node = elem;
+                                                               outerCache = node[ expando ] || (node[ expando ] = {});
+
+                                                               // Support: IE <9 only
+                                                               // Defend against cloned attroperties (jQuery gh-1709)
+                                                               uniqueCache = outerCache[ node.uniqueID ] ||
+                                                                       (outerCache[ node.uniqueID ] = {});
+
+                                                               cache = uniqueCache[ type ] || [];
+                                                               nodeIndex = cache[ 0 ] === dirruns && cache[ 1 ];
+                                                               diff = nodeIndex;
+                                                       }
+
+                                                       // xml :nth-child(...)
+                                                       // or :nth-last-child(...) or :nth(-last)?-of-type(...)
+                                                       if ( diff === false ) {
+                                                               // Use the same loop as above to seek `elem` from the start
+                                                               while ( (node = ++nodeIndex && node && node[ dir ] ||
+                                                                       (diff = nodeIndex = 0) || start.pop()) ) {
+
+                                                                       if ( ( ofType ?
+                                                                               node.nodeName.toLowerCase() === name :
+                                                                               node.nodeType === 1 ) &&
+                                                                               ++diff ) {
+
+                                                                               // Cache the index of each encountered element
+                                                                               if ( useCache ) {
+                                                                                       outerCache = node[ expando ] || (node[ expando ] = {});
+
+                                                                                       // Support: IE <9 only
+                                                                                       // Defend against cloned attroperties (jQuery gh-1709)
+                                                                                       uniqueCache = outerCache[ node.uniqueID ] ||
+                                                                                               (outerCache[ node.uniqueID ] = {});
+
+                                                                                       uniqueCache[ type ] = [ dirruns, diff ];
+                                                                               }
+
+                                                                               if ( node === elem ) {
+                                                                                       break;
+                                                                               }
+                                                                       }
+                                                               }
+                                                       }
+                                               }
+
+                                               // Incorporate the offset, then check against cycle size
+                                               diff -= last;
+                                               return diff === first || ( diff % first === 0 && diff / first >= 0 );
+                                       }
+                               };
+               },
+
+               "PSEUDO": function( pseudo, argument ) {
+                       // pseudo-class names are case-insensitive
+                       // http://www.w3.org/TR/selectors/#pseudo-classes
+                       // Prioritize by case sensitivity in case custom pseudos are added with uppercase letters
+                       // Remember that setFilters inherits from pseudos
+                       var args,
+                               fn = Expr.pseudos[ pseudo ] || Expr.setFilters[ pseudo.toLowerCase() ] ||
+                                       Sizzle.error( "unsupported pseudo: " + pseudo );
+
+                       // The user may use createPseudo to indicate that
+                       // arguments are needed to create the filter function
+                       // just as Sizzle does
+                       if ( fn[ expando ] ) {
+                               return fn( argument );
+                       }
+
+                       // But maintain support for old signatures
+                       if ( fn.length > 1 ) {
+                               args = [ pseudo, pseudo, "", argument ];
+                               return Expr.setFilters.hasOwnProperty( pseudo.toLowerCase() ) ?
+                                       markFunction(function( seed, matches ) {
+                                               var idx,
+                                                       matched = fn( seed, argument ),
+                                                       i = matched.length;
+                                               while ( i-- ) {
+                                                       idx = indexOf( seed, matched[i] );
+                                                       seed[ idx ] = !( matches[ idx ] = matched[i] );
+                                               }
+                                       }) :
+                                       function( elem ) {
+                                               return fn( elem, 0, args );
+                                       };
+                       }
+
+                       return fn;
+               }
+       },
+
+       pseudos: {
+               // Potentially complex pseudos
+               "not": markFunction(function( selector ) {
+                       // Trim the selector passed to compile
+                       // to avoid treating leading and trailing
+                       // spaces as combinators
+                       var input = [],
+                               results = [],
+                               matcher = compile( selector.replace( rtrim, "$1" ) );
+
+                       return matcher[ expando ] ?
+                               markFunction(function( seed, matches, context, xml ) {
+                                       var elem,
+                                               unmatched = matcher( seed, null, xml, [] ),
+                                               i = seed.length;
+
+                                       // Match elements unmatched by `matcher`
+                                       while ( i-- ) {
+                                               if ( (elem = unmatched[i]) ) {
+                                                       seed[i] = !(matches[i] = elem);
+                                               }
+                                       }
+                               }) :
+                               function( elem, context, xml ) {
+                                       input[0] = elem;
+                                       matcher( input, null, xml, results );
+                                       // Don't keep the element (issue #299)
+                                       input[0] = null;
+                                       return !results.pop();
+                               };
+               }),
+
+               "has": markFunction(function( selector ) {
+                       return function( elem ) {
+                               return Sizzle( selector, elem ).length > 0;
+                       };
+               }),
+
+               "contains": markFunction(function( text ) {
+                       text = text.replace( runescape, funescape );
+                       return function( elem ) {
+                               return ( elem.textContent || getText( elem ) ).indexOf( text ) > -1;
+                       };
+               }),
+
+               // "Whether an element is represented by a :lang() selector
+               // is based solely on the element's language value
+               // being equal to the identifier C,
+               // or beginning with the identifier C immediately followed by "-".
+               // The matching of C against the element's language value is performed case-insensitively.
+               // The identifier C does not have to be a valid language name."
+               // http://www.w3.org/TR/selectors/#lang-pseudo
+               "lang": markFunction( function( lang ) {
+                       // lang value must be a valid identifier
+                       if ( !ridentifier.test(lang || "") ) {
+                               Sizzle.error( "unsupported lang: " + lang );
+                       }
+                       lang = lang.replace( runescape, funescape ).toLowerCase();
+                       return function( elem ) {
+                               var elemLang;
+                               do {
+                                       if ( (elemLang = documentIsHTML ?
+                                               elem.lang :
+                                               elem.getAttribute("xml:lang") || elem.getAttribute("lang")) ) {
+
+                                               elemLang = elemLang.toLowerCase();
+                                               return elemLang === lang || elemLang.indexOf( lang + "-" ) === 0;
+                                       }
+                               } while ( (elem = elem.parentNode) && elem.nodeType === 1 );
+                               return false;
+                       };
+               }),
+
+               // Miscellaneous
+               "target": function( elem ) {
+                       var hash = window.location && window.location.hash;
+                       return hash && hash.slice( 1 ) === elem.id;
+               },
+
+               "root": function( elem ) {
+                       return elem === docElem;
+               },
+
+               "focus": function( elem ) {
+                       return elem === document.activeElement && (!document.hasFocus || document.hasFocus()) && !!(elem.type || elem.href || ~elem.tabIndex);
+               },
+
+               // Boolean properties
+               "enabled": createDisabledPseudo( false ),
+               "disabled": createDisabledPseudo( true ),
+
+               "checked": function( elem ) {
+                       // In CSS3, :checked should return both checked and selected elements
+                       // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked
+                       var nodeName = elem.nodeName.toLowerCase();
+                       return (nodeName === "input" && !!elem.checked) || (nodeName === "option" && !!elem.selected);
+               },
+
+               "selected": function( elem ) {
+                       // Accessing this property makes selected-by-default
+                       // options in Safari work properly
+                       if ( elem.parentNode ) {
+                               elem.parentNode.selectedIndex;
+                       }
+
+                       return elem.selected === true;
+               },
+
+               // Contents
+               "empty": function( elem ) {
+                       // http://www.w3.org/TR/selectors/#empty-pseudo
+                       // :empty is negated by element (1) or content nodes (text: 3; cdata: 4; entity ref: 5),
+                       //   but not by others (comment: 8; processing instruction: 7; etc.)
+                       // nodeType < 6 works because attributes (2) do not appear as children
+                       for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) {
+                               if ( elem.nodeType < 6 ) {
+                                       return false;
+                               }
+                       }
+                       return true;
+               },
+
+               "parent": function( elem ) {
+                       return !Expr.pseudos["empty"]( elem );
+               },
+
+               // Element/input types
+               "header": function( elem ) {
+                       return rheader.test( elem.nodeName );
+               },
+
+               "input": function( elem ) {
+                       return rinputs.test( elem.nodeName );
+               },
+
+               "button": function( elem ) {
+                       var name = elem.nodeName.toLowerCase();
+                       return name === "input" && elem.type === "button" || name === "button";
+               },
+
+               "text": function( elem ) {
+                       var attr;
+                       return elem.nodeName.toLowerCase() === "input" &&
+                               elem.type === "text" &&
+
+                               // Support: IE<8
+                               // New HTML5 attribute values (e.g., "search") appear with elem.type === "text"
+                               ( (attr = elem.getAttribute("type")) == null || attr.toLowerCase() === "text" );
+               },
+
+               // Position-in-collection
+               "first": createPositionalPseudo(function() {
+                       return [ 0 ];
+               }),
+
+               "last": createPositionalPseudo(function( matchIndexes, length ) {
+                       return [ length - 1 ];
+               }),
+
+               "eq": createPositionalPseudo(function( matchIndexes, length, argument ) {
+                       return [ argument < 0 ? argument + length : argument ];
+               }),
+
+               "even": createPositionalPseudo(function( matchIndexes, length ) {
+                       var i = 0;
+                       for ( ; i < length; i += 2 ) {
+                               matchIndexes.push( i );
+                       }
+                       return matchIndexes;
+               }),
+
+               "odd": createPositionalPseudo(function( matchIndexes, length ) {
+                       var i = 1;
+                       for ( ; i < length; i += 2 ) {
+                               matchIndexes.push( i );
+                       }
+                       return matchIndexes;
+               }),
+
+               "lt": createPositionalPseudo(function( matchIndexes, length, argument ) {
+                       var i = argument < 0 ?
+                               argument + length :
+                               argument > length ?
+                                       length :
+                                       argument;
+                       for ( ; --i >= 0; ) {
+                               matchIndexes.push( i );
+                       }
+                       return matchIndexes;
+               }),
+
+               "gt": createPositionalPseudo(function( matchIndexes, length, argument ) {
+                       var i = argument < 0 ? argument + length : argument;
+                       for ( ; ++i < length; ) {
+                               matchIndexes.push( i );
+                       }
+                       return matchIndexes;
+               })
+       }
+};
+
+Expr.pseudos["nth"] = Expr.pseudos["eq"];
+
+// Add button/input type pseudos
+for ( i in { radio: true, checkbox: true, file: true, password: true, image: true } ) {
+       Expr.pseudos[ i ] = createInputPseudo( i );
+}
+for ( i in { submit: true, reset: true } ) {
+       Expr.pseudos[ i ] = createButtonPseudo( i );
+}
+
+// Easy API for creating new setFilters
+function setFilters() {}
+setFilters.prototype = Expr.filters = Expr.pseudos;
+Expr.setFilters = new setFilters();
+
+tokenize = Sizzle.tokenize = function( selector, parseOnly ) {
+       var matched, match, tokens, type,
+               soFar, groups, preFilters,
+               cached = tokenCache[ selector + " " ];
+
+       if ( cached ) {
+               return parseOnly ? 0 : cached.slice( 0 );
+       }
+
+       soFar = selector;
+       groups = [];
+       preFilters = Expr.preFilter;
+
+       while ( soFar ) {
+
+               // Comma and first run
+               if ( !matched || (match = rcomma.exec( soFar )) ) {
+                       if ( match ) {
+                               // Don't consume trailing commas as valid
+                               soFar = soFar.slice( match[0].length ) || soFar;
+                       }
+                       groups.push( (tokens = []) );
+               }
+
+               matched = false;
+
+               // Combinators
+               if ( (match = rcombinators.exec( soFar )) ) {
+                       matched = match.shift();
+                       tokens.push({
+                               value: matched,
+                               // Cast descendant combinators to space
+                               type: match[0].replace( rtrim, " " )
+                       });
+                       soFar = soFar.slice( matched.length );
+               }
+
+               // Filters
+               for ( type in Expr.filter ) {
+                       if ( (match = matchExpr[ type ].exec( soFar )) && (!preFilters[ type ] ||
+                               (match = preFilters[ type ]( match ))) ) {
+                               matched = match.shift();
+                               tokens.push({
+                                       value: matched,
+                                       type: type,
+                                       matches: match
+                               });
+                               soFar = soFar.slice( matched.length );
+                       }
+               }
+
+               if ( !matched ) {
+                       break;
+               }
+       }
+
+       // Return the length of the invalid excess
+       // if we're just parsing
+       // Otherwise, throw an error or return tokens
+       return parseOnly ?
+               soFar.length :
+               soFar ?
+                       Sizzle.error( selector ) :
+                       // Cache the tokens
+                       tokenCache( selector, groups ).slice( 0 );
+};
+
+function toSelector( tokens ) {
+       var i = 0,
+               len = tokens.length,
+               selector = "";
+       for ( ; i < len; i++ ) {
+               selector += tokens[i].value;
+       }
+       return selector;
+}
+
+function addCombinator( matcher, combinator, base ) {
+       var dir = combinator.dir,
+               skip = combinator.next,
+               key = skip || dir,
+               checkNonElements = base && key === "parentNode",
+               doneName = done++;
+
+       return combinator.first ?
+               // Check against closest ancestor/preceding element
+               function( elem, context, xml ) {
+                       while ( (elem = elem[ dir ]) ) {
+                               if ( elem.nodeType === 1 || checkNonElements ) {
+                                       return matcher( elem, context, xml );
+                               }
+                       }
+                       return false;
+               } :
+
+               // Check against all ancestor/preceding elements
+               function( elem, context, xml ) {
+                       var oldCache, uniqueCache, outerCache,
+                               newCache = [ dirruns, doneName ];
+
+                       // We can't set arbitrary data on XML nodes, so they don't benefit from combinator caching
+                       if ( xml ) {
+                               while ( (elem = elem[ dir ]) ) {
+                                       if ( elem.nodeType === 1 || checkNonElements ) {
+                                               if ( matcher( elem, context, xml ) ) {
+                                                       return true;
+                                               }
+                                       }
+                               }
+                       } else {
+                               while ( (elem = elem[ dir ]) ) {
+                                       if ( elem.nodeType === 1 || checkNonElements ) {
+                                               outerCache = elem[ expando ] || (elem[ expando ] = {});
+
+                                               // Support: IE <9 only
+                                               // Defend against cloned attroperties (jQuery gh-1709)
+                                               uniqueCache = outerCache[ elem.uniqueID ] || (outerCache[ elem.uniqueID ] = {});
+
+                                               if ( skip && skip === elem.nodeName.toLowerCase() ) {
+                                                       elem = elem[ dir ] || elem;
+                                               } else if ( (oldCache = uniqueCache[ key ]) &&
+                                                       oldCache[ 0 ] === dirruns && oldCache[ 1 ] === doneName ) {
+
+                                                       // Assign to newCache so results back-propagate to previous elements
+                                                       return (newCache[ 2 ] = oldCache[ 2 ]);
+                                               } else {
+                                                       // Reuse newcache so results back-propagate to previous elements
+                                                       uniqueCache[ key ] = newCache;
+
+                                                       // A match means we're done; a fail means we have to keep checking
+                                                       if ( (newCache[ 2 ] = matcher( elem, context, xml )) ) {
+                                                               return true;
+                                                       }
+                                               }
+                                       }
+                               }
+                       }
+                       return false;
+               };
+}
+
+function elementMatcher( matchers ) {
+       return matchers.length > 1 ?
+               function( elem, context, xml ) {
+                       var i = matchers.length;
+                       while ( i-- ) {
+                               if ( !matchers[i]( elem, context, xml ) ) {
+                                       return false;
+                               }
+                       }
+                       return true;
+               } :
+               matchers[0];
+}
+
+function multipleContexts( selector, contexts, results ) {
+       var i = 0,
+               len = contexts.length;
+       for ( ; i < len; i++ ) {
+               Sizzle( selector, contexts[i], results );
+       }
+       return results;
+}
+
+function condense( unmatched, map, filter, context, xml ) {
+       var elem,
+               newUnmatched = [],
+               i = 0,
+               len = unmatched.length,
+               mapped = map != null;
+
+       for ( ; i < len; i++ ) {
+               if ( (elem = unmatched[i]) ) {
+                       if ( !filter || filter( elem, context, xml ) ) {
+                               newUnmatched.push( elem );
+                               if ( mapped ) {
+                                       map.push( i );
+                               }
+                       }
+               }
+       }
+
+       return newUnmatched;
+}
+
+function setMatcher( preFilter, selector, matcher, postFilter, postFinder, postSelector ) {
+       if ( postFilter && !postFilter[ expando ] ) {
+               postFilter = setMatcher( postFilter );
+       }
+       if ( postFinder && !postFinder[ expando ] ) {
+               postFinder = setMatcher( postFinder, postSelector );
+       }
+       return markFunction(function( seed, results, context, xml ) {
+               var temp, i, elem,
+                       preMap = [],
+                       postMap = [],
+                       preexisting = results.length,
+
+                       // Get initial elements from seed or context
+                       elems = seed || multipleContexts( selector || "*", context.nodeType ? [ context ] : context, [] ),
+
+                       // Prefilter to get matcher input, preserving a map for seed-results synchronization
+                       matcherIn = preFilter && ( seed || !selector ) ?
+                               condense( elems, preMap, preFilter, context, xml ) :
+                               elems,
+
+                       matcherOut = matcher ?
+                               // If we have a postFinder, or filtered seed, or non-seed postFilter or preexisting results,
+                               postFinder || ( seed ? preFilter : preexisting || postFilter ) ?
+
+                                       // ...intermediate processing is necessary
+                                       [] :
+
+                                       // ...otherwise use results directly
+                                       results :
+                               matcherIn;
+
+               // Find primary matches
+               if ( matcher ) {
+                       matcher( matcherIn, matcherOut, context, xml );
+               }
+
+               // Apply postFilter
+               if ( postFilter ) {
+                       temp = condense( matcherOut, postMap );
+                       postFilter( temp, [], context, xml );
+
+                       // Un-match failing elements by moving them back to matcherIn
+                       i = temp.length;
+                       while ( i-- ) {
+                               if ( (elem = temp[i]) ) {
+                                       matcherOut[ postMap[i] ] = !(matcherIn[ postMap[i] ] = elem);
+                               }
+                       }
+               }
+
+               if ( seed ) {
+                       if ( postFinder || preFilter ) {
+                               if ( postFinder ) {
+                                       // Get the final matcherOut by condensing this intermediate into postFinder contexts
+                                       temp = [];
+                                       i = matcherOut.length;
+                                       while ( i-- ) {
+                                               if ( (elem = matcherOut[i]) ) {
+                                                       // Restore matcherIn since elem is not yet a final match
+                                                       temp.push( (matcherIn[i] = elem) );
+                                               }
+                                       }
+                                       postFinder( null, (matcherOut = []), temp, xml );
+                               }
+
+                               // Move matched elements from seed to results to keep them synchronized
+                               i = matcherOut.length;
+                               while ( i-- ) {
+                                       if ( (elem = matcherOut[i]) &&
+                                               (temp = postFinder ? indexOf( seed, elem ) : preMap[i]) > -1 ) {
+
+                                               seed[temp] = !(results[temp] = elem);
+                                       }
+                               }
+                       }
+
+               // Add elements to results, through postFinder if defined
+               } else {
+                       matcherOut = condense(
+                               matcherOut === results ?
+                                       matcherOut.splice( preexisting, matcherOut.length ) :
+                                       matcherOut
+                       );
+                       if ( postFinder ) {
+                               postFinder( null, results, matcherOut, xml );
+                       } else {
+                               push.apply( results, matcherOut );
+                       }
+               }
+       });
+}
+
+function matcherFromTokens( tokens ) {
+       var checkContext, matcher, j,
+               len = tokens.length,
+               leadingRelative = Expr.relative[ tokens[0].type ],
+               implicitRelative = leadingRelative || Expr.relative[" "],
+               i = leadingRelative ? 1 : 0,
+
+               // The foundational matcher ensures that elements are reachable from top-level context(s)
+               matchContext = addCombinator( function( elem ) {
+                       return elem === checkContext;
+               }, implicitRelative, true ),
+               matchAnyContext = addCombinator( function( elem ) {
+                       return indexOf( checkContext, elem ) > -1;
+               }, implicitRelative, true ),
+               matchers = [ function( elem, context, xml ) {
+                       var ret = ( !leadingRelative && ( xml || context !== outermostContext ) ) || (
+                               (checkContext = context).nodeType ?
+                                       matchContext( elem, context, xml ) :
+                                       matchAnyContext( elem, context, xml ) );
+                       // Avoid hanging onto element (issue #299)
+                       checkContext = null;
+                       return ret;
+               } ];
+
+       for ( ; i < len; i++ ) {
+               if ( (matcher = Expr.relative[ tokens[i].type ]) ) {
+                       matchers = [ addCombinator(elementMatcher( matchers ), matcher) ];
+               } else {
+                       matcher = Expr.filter[ tokens[i].type ].apply( null, tokens[i].matches );
+
+                       // Return special upon seeing a positional matcher
+                       if ( matcher[ expando ] ) {
+                               // Find the next relative operator (if any) for proper handling
+                               j = ++i;
+                               for ( ; j < len; j++ ) {
+                                       if ( Expr.relative[ tokens[j].type ] ) {
+                                               break;
+                                       }
+                               }
+                               return setMatcher(
+                                       i > 1 && elementMatcher( matchers ),
+                                       i > 1 && toSelector(
+                                               // If the preceding token was a descendant combinator, insert an implicit any-element `*`
+                                               tokens.slice( 0, i - 1 ).concat({ value: tokens[ i - 2 ].type === " " ? "*" : "" })
+                                       ).replace( rtrim, "$1" ),
+                                       matcher,
+                                       i < j && matcherFromTokens( tokens.slice( i, j ) ),
+                                       j < len && matcherFromTokens( (tokens = tokens.slice( j )) ),
+                                       j < len && toSelector( tokens )
+                               );
+                       }
+                       matchers.push( matcher );
+               }
+       }
+
+       return elementMatcher( matchers );
+}
+
+function matcherFromGroupMatchers( elementMatchers, setMatchers ) {
+       var bySet = setMatchers.length > 0,
+               byElement = elementMatchers.length > 0,
+               superMatcher = function( seed, context, xml, results, outermost ) {
+                       var elem, j, matcher,
+                               matchedCount = 0,
+                               i = "0",
+                               unmatched = seed && [],
+                               setMatched = [],
+                               contextBackup = outermostContext,
+                               // We must always have either seed elements or outermost context
+                               elems = seed || byElement && Expr.find["TAG"]( "*", outermost ),
+                               // Use integer dirruns iff this is the outermost matcher
+                               dirrunsUnique = (dirruns += contextBackup == null ? 1 : Math.random() || 0.1),
+                               len = elems.length;
+
+                       if ( outermost ) {
+                               outermostContext = context === document || context || outermost;
+                       }
+
+                       // Add elements passing elementMatchers directly to results
+                       // Support: IE<9, Safari
+                       // Tolerate NodeList properties (IE: "length"; Safari: <number>) matching elements by id
+                       for ( ; i !== len && (elem = elems[i]) != null; i++ ) {
+                               if ( byElement && elem ) {
+                                       j = 0;
+                                       if ( !context && elem.ownerDocument !== document ) {
+                                               setDocument( elem );
+                                               xml = !documentIsHTML;
+                                       }
+                                       while ( (matcher = elementMatchers[j++]) ) {
+                                               if ( matcher( elem, context || document, xml) ) {
+                                                       results.push( elem );
+                                                       break;
+                                               }
+                                       }
+                                       if ( outermost ) {
+                                               dirruns = dirrunsUnique;
+                                       }
+                               }
+
+                               // Track unmatched elements for set filters
+                               if ( bySet ) {
+                                       // They will have gone through all possible matchers
+                                       if ( (elem = !matcher && elem) ) {
+                                               matchedCount--;
+                                       }
+
+                                       // Lengthen the array for every element, matched or not
+                                       if ( seed ) {
+                                               unmatched.push( elem );
+                                       }
+                               }
+                       }
+
+                       // `i` is now the count of elements visited above, and adding it to `matchedCount`
+                       // makes the latter nonnegative.
+                       matchedCount += i;
+
+                       // Apply set filters to unmatched elements
+                       // NOTE: This can be skipped if there are no unmatched elements (i.e., `matchedCount`
+                       // equals `i`), unless we didn't visit _any_ elements in the above loop because we have
+                       // no element matchers and no seed.
+                       // Incrementing an initially-string "0" `i` allows `i` to remain a string only in that
+                       // case, which will result in a "00" `matchedCount` that differs from `i` but is also
+                       // numerically zero.
+                       if ( bySet && i !== matchedCount ) {
+                               j = 0;
+                               while ( (matcher = setMatchers[j++]) ) {
+                                       matcher( unmatched, setMatched, context, xml );
+                               }
+
+                               if ( seed ) {
+                                       // Reintegrate element matches to eliminate the need for sorting
+                                       if ( matchedCount > 0 ) {
+                                               while ( i-- ) {
+                                                       if ( !(unmatched[i] || setMatched[i]) ) {
+                                                               setMatched[i] = pop.call( results );
+                                                       }
+                                               }
+                                       }
+
+                                       // Discard index placeholder values to get only actual matches
+                                       setMatched = condense( setMatched );
+                               }
+
+                               // Add matches to results
+                               push.apply( results, setMatched );
+
+                               // Seedless set matches succeeding multiple successful matchers stipulate sorting
+                               if ( outermost && !seed && setMatched.length > 0 &&
+                                       ( matchedCount + setMatchers.length ) > 1 ) {
+
+                                       Sizzle.uniqueSort( results );
+                               }
+                       }
+
+                       // Override manipulation of globals by nested matchers
+                       if ( outermost ) {
+                               dirruns = dirrunsUnique;
+                               outermostContext = contextBackup;
+                       }
+
+                       return unmatched;
+               };
+
+       return bySet ?
+               markFunction( superMatcher ) :
+               superMatcher;
+}
+
+compile = Sizzle.compile = function( selector, match /* Internal Use Only */ ) {
+       var i,
+               setMatchers = [],
+               elementMatchers = [],
+               cached = compilerCache[ selector + " " ];
+
+       if ( !cached ) {
+               // Generate a function of recursive functions that can be used to check each element
+               if ( !match ) {
+                       match = tokenize( selector );
+               }
+               i = match.length;
+               while ( i-- ) {
+                       cached = matcherFromTokens( match[i] );
+                       if ( cached[ expando ] ) {
+                               setMatchers.push( cached );
+                       } else {
+                               elementMatchers.push( cached );
+                       }
+               }
+
+               // Cache the compiled function
+               cached = compilerCache( selector, matcherFromGroupMatchers( elementMatchers, setMatchers ) );
+
+               // Save selector and tokenization
+               cached.selector = selector;
+       }
+       return cached;
+};
+
+/**
+ * A low-level selection function that works with Sizzle's compiled
+ *  selector functions
+ * @param {String|Function} selector A selector or a pre-compiled
+ *  selector function built with Sizzle.compile
+ * @param {Element} context
+ * @param {Array} [results]
+ * @param {Array} [seed] A set of elements to match against
+ */
+select = Sizzle.select = function( selector, context, results, seed ) {
+       var i, tokens, token, type, find,
+               compiled = typeof selector === "function" && selector,
+               match = !seed && tokenize( (selector = compiled.selector || selector) );
+
+       results = results || [];
+
+       // Try to minimize operations if there is only one selector in the list and no seed
+       // (the latter of which guarantees us context)
+       if ( match.length === 1 ) {
+
+               // Reduce context if the leading compound selector is an ID
+               tokens = match[0] = match[0].slice( 0 );
+               if ( tokens.length > 2 && (token = tokens[0]).type === "ID" &&
+                               context.nodeType === 9 && documentIsHTML && Expr.relative[ tokens[1].type ] ) {
+
+                       context = ( Expr.find["ID"]( token.matches[0].replace(runescape, funescape), context ) || [] )[0];
+                       if ( !context ) {
+                               return results;
+
+                       // Precompiled matchers will still verify ancestry, so step up a level
+                       } else if ( compiled ) {
+                               context = context.parentNode;
+                       }
+
+                       selector = selector.slice( tokens.shift().value.length );
+               }
+
+               // Fetch a seed set for right-to-left matching
+               i = matchExpr["needsContext"].test( selector ) ? 0 : tokens.length;
+               while ( i-- ) {
+                       token = tokens[i];
+
+                       // Abort if we hit a combinator
+                       if ( Expr.relative[ (type = token.type) ] ) {
+                               break;
+                       }
+                       if ( (find = Expr.find[ type ]) ) {
+                               // Search, expanding context for leading sibling combinators
+                               if ( (seed = find(
+                                       token.matches[0].replace( runescape, funescape ),
+                                       rsibling.test( tokens[0].type ) && testContext( context.parentNode ) || context
+                               )) ) {
+
+                                       // If seed is empty or no tokens remain, we can return early
+                                       tokens.splice( i, 1 );
+                                       selector = seed.length && toSelector( tokens );
+                                       if ( !selector ) {
+                                               push.apply( results, seed );
+                                               return results;
+                                       }
+
+                                       break;
+                               }
+                       }
+               }
+       }
+
+       // Compile and execute a filtering function if one is not provided
+       // Provide `match` to avoid retokenization if we modified the selector above
+       ( compiled || compile( selector, match ) )(
+               seed,
+               context,
+               !documentIsHTML,
+               results,
+               !context || rsibling.test( selector ) && testContext( context.parentNode ) || context
+       );
+       return results;
+};
+
+// One-time assignments
+
+// Sort stability
+support.sortStable = expando.split("").sort( sortOrder ).join("") === expando;
+
+// Support: Chrome 14-35+
+// Always assume duplicates if they aren't passed to the comparison function
+support.detectDuplicates = !!hasDuplicate;
+
+// Initialize against the default document
+setDocument();
+
+// Support: Webkit<537.32 - Safari 6.0.3/Chrome 25 (fixed in Chrome 27)
+// Detached nodes confoundingly follow *each other*
+support.sortDetached = assert(function( el ) {
+       // Should return 1, but returns 4 (following)
+       return el.compareDocumentPosition( document.createElement("fieldset") ) & 1;
+});
+
+// Support: IE<8
+// Prevent attribute/property "interpolation"
+// https://msdn.microsoft.com/en-us/library/ms536429%28VS.85%29.aspx
+if ( !assert(function( el ) {
+       el.innerHTML = "<a href='#'></a>";
+       return el.firstChild.getAttribute("href") === "#" ;
+}) ) {
+       addHandle( "type|href|height|width", function( elem, name, isXML ) {
+               if ( !isXML ) {
+                       return elem.getAttribute( name, name.toLowerCase() === "type" ? 1 : 2 );
+               }
+       });
+}
+
+// Support: IE<9
+// Use defaultValue in place of getAttribute("value")
+if ( !support.attributes || !assert(function( el ) {
+       el.innerHTML = "<input/>";
+       el.firstChild.setAttribute( "value", "" );
+       return el.firstChild.getAttribute( "value" ) === "";
+}) ) {
+       addHandle( "value", function( elem, name, isXML ) {
+               if ( !isXML && elem.nodeName.toLowerCase() === "input" ) {
+                       return elem.defaultValue;
+               }
+       });
+}
+
+// Support: IE<9
+// Use getAttributeNode to fetch booleans when getAttribute lies
+if ( !assert(function( el ) {
+       return el.getAttribute("disabled") == null;
+}) ) {
+       addHandle( booleans, function( elem, name, isXML ) {
+               var val;
+               if ( !isXML ) {
+                       return elem[ name ] === true ? name.toLowerCase() :
+                                       (val = elem.getAttributeNode( name )) && val.specified ?
+                                       val.value :
+                               null;
+               }
+       });
+}
+
+return Sizzle;
+
+})( window );
+
+
+
+jQuery.find = Sizzle;
+jQuery.expr = Sizzle.selectors;
+
+// Deprecated
+jQuery.expr[ ":" ] = jQuery.expr.pseudos;
+jQuery.uniqueSort = jQuery.unique = Sizzle.uniqueSort;
+jQuery.text = Sizzle.getText;
+jQuery.isXMLDoc = Sizzle.isXML;
+jQuery.contains = Sizzle.contains;
+jQuery.escapeSelector = Sizzle.escape;
+
+
+
+
+var dir = function( elem, dir, until ) {
+       var matched = [],
+               truncate = until !== undefined;
+
+       while ( ( elem = elem[ dir ] ) && elem.nodeType !== 9 ) {
+               if ( elem.nodeType === 1 ) {
+                       if ( truncate && jQuery( elem ).is( until ) ) {
+                               break;
+                       }
+                       matched.push( elem );
+               }
+       }
+       return matched;
+};
+
+
+var siblings = function( n, elem ) {
+       var matched = [];
+
+       for ( ; n; n = n.nextSibling ) {
+               if ( n.nodeType === 1 && n !== elem ) {
+                       matched.push( n );
+               }
+       }
+
+       return matched;
+};
+
+
+var rneedsContext = jQuery.expr.match.needsContext;
+
+
+
+function nodeName( elem, name ) {
+
+  return elem.nodeName && elem.nodeName.toLowerCase() === name.toLowerCase();
+
+};
+var rsingleTag = ( /^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i );
+
+
+
+// Implement the identical functionality for filter and not
+function winnow( elements, qualifier, not ) {
+       if ( isFunction( qualifier ) ) {
+               return jQuery.grep( elements, function( elem, i ) {
+                       return !!qualifier.call( elem, i, elem ) !== not;
+               } );
+       }
+
+       // Single element
+       if ( qualifier.nodeType ) {
+               return jQuery.grep( elements, function( elem ) {
+                       return ( elem === qualifier ) !== not;
+               } );
+       }
+
+       // Arraylike of elements (jQuery, arguments, Array)
+       if ( typeof qualifier !== "string" ) {
+               return jQuery.grep( elements, function( elem ) {
+                       return ( indexOf.call( qualifier, elem ) > -1 ) !== not;
+               } );
+       }
+
+       // Filtered directly for both simple and complex selectors
+       return jQuery.filter( qualifier, elements, not );
+}
+
+jQuery.filter = function( expr, elems, not ) {
+       var elem = elems[ 0 ];
+
+       if ( not ) {
+               expr = ":not(" + expr + ")";
+       }
+
+       if ( elems.length === 1 && elem.nodeType === 1 ) {
+               return jQuery.find.matchesSelector( elem, expr ) ? [ elem ] : [];
+       }
+
+       return jQuery.find.matches( expr, jQuery.grep( elems, function( elem ) {
+               return elem.nodeType === 1;
+       } ) );
+};
+
+jQuery.fn.extend( {
+       find: function( selector ) {
+               var i, ret,
+                       len = this.length,
+                       self = this;
+
+               if ( typeof selector !== "string" ) {
+                       return this.pushStack( jQuery( selector ).filter( function() {
+                               for ( i = 0; i < len; i++ ) {
+                                       if ( jQuery.contains( self[ i ], this ) ) {
+                                               return true;
+                                       }
+                               }
+                       } ) );
+               }
+
+               ret = this.pushStack( [] );
+
+               for ( i = 0; i < len; i++ ) {
+                       jQuery.find( selector, self[ i ], ret );
+               }
+
+               return len > 1 ? jQuery.uniqueSort( ret ) : ret;
+       },
+       filter: function( selector ) {
+               return this.pushStack( winnow( this, selector || [], false ) );
+       },
+       not: function( selector ) {
+               return this.pushStack( winnow( this, selector || [], true ) );
+       },
+       is: function( selector ) {
+               return !!winnow(
+                       this,
+
+                       // If this is a positional/relative selector, check membership in the returned set
+                       // so $("p:first").is("p:last") won't return true for a doc with two "p".
+                       typeof selector === "string" && rneedsContext.test( selector ) ?
+                               jQuery( selector ) :
+                               selector || [],
+                       false
+               ).length;
+       }
+} );
+
+
+// Initialize a jQuery object
+
+
+// A central reference to the root jQuery(document)
+var rootjQuery,
+
+       // A simple way to check for HTML strings
+       // Prioritize #id over <tag> to avoid XSS via location.hash (#9521)
+       // Strict HTML recognition (#11290: must start with <)
+       // Shortcut simple #id case for speed
+       rquickExpr = /^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/,
+
+       init = jQuery.fn.init = function( selector, context, root ) {
+               var match, elem;
+
+               // HANDLE: $(""), $(null), $(undefined), $(false)
+               if ( !selector ) {
+                       return this;
+               }
+
+               // Method init() accepts an alternate rootjQuery
+               // so migrate can support jQuery.sub (gh-2101)
+               root = root || rootjQuery;
+
+               // Handle HTML strings
+               if ( typeof selector === "string" ) {
+                       if ( selector[ 0 ] === "<" &&
+                               selector[ selector.length - 1 ] === ">" &&
+                               selector.length >= 3 ) {
+
+                               // Assume that strings that start and end with <> are HTML and skip the regex check
+                               match = [ null, selector, null ];
+
+                       } else {
+                               match = rquickExpr.exec( selector );
+                       }
+
+                       // Match html or make sure no context is specified for #id
+                       if ( match && ( match[ 1 ] || !context ) ) {
+
+                               // HANDLE: $(html) -> $(array)
+                               if ( match[ 1 ] ) {
+                                       context = context instanceof jQuery ? context[ 0 ] : context;
+
+                                       // Option to run scripts is true for back-compat
+                                       // Intentionally let the error be thrown if parseHTML is not present
+                                       jQuery.merge( this, jQuery.parseHTML(
+                                               match[ 1 ],
+                                               context && context.nodeType ? context.ownerDocument || context : document,
+                                               true
+                                       ) );
+
+                                       // HANDLE: $(html, props)
+                                       if ( rsingleTag.test( match[ 1 ] ) && jQuery.isPlainObject( context ) ) {
+                                               for ( match in context ) {
+
+                                                       // Properties of context are called as methods if possible
+                                                       if ( isFunction( this[ match ] ) ) {
+                                                               this[ match ]( context[ match ] );
+
+                                                       // ...and otherwise set as attributes
+                                                       } else {
+                                                               this.attr( match, context[ match ] );
+                                                       }
+                                               }
+                                       }
+
+                                       return this;
+
+                               // HANDLE: $(#id)
+                               } else {
+                                       elem = document.getElementById( match[ 2 ] );
+
+                                       if ( elem ) {
+
+                                               // Inject the element directly into the jQuery object
+                                               this[ 0 ] = elem;
+                                               this.length = 1;
+                                       }
+                                       return this;
+                               }
+
+                       // HANDLE: $(expr, $(...))
+                       } else if ( !context || context.jquery ) {
+                               return ( context || root ).find( selector );
+
+                       // HANDLE: $(expr, context)
+                       // (which is just equivalent to: $(context).find(expr)
+                       } else {
+                               return this.constructor( context ).find( selector );
+                       }
+
+               // HANDLE: $(DOMElement)
+               } else if ( selector.nodeType ) {
+                       this[ 0 ] = selector;
+                       this.length = 1;
+                       return this;
+
+               // HANDLE: $(function)
+               // Shortcut for document ready
+               } else if ( isFunction( selector ) ) {
+                       return root.ready !== undefined ?
+                               root.ready( selector ) :
+
+                               // Execute immediately if ready is not present
+                               selector( jQuery );
+               }
+
+               return jQuery.makeArray( selector, this );
+       };
+
+// Give the init function the jQuery prototype for later instantiation
+init.prototype = jQuery.fn;
+
+// Initialize central reference
+rootjQuery = jQuery( document );
+
+
+var rparentsprev = /^(?:parents|prev(?:Until|All))/,
+
+       // Methods guaranteed to produce a unique set when starting from a unique set
+       guaranteedUnique = {
+               children: true,
+               contents: true,
+               next: true,
+               prev: true
+       };
+
+jQuery.fn.extend( {
+       has: function( target ) {
+               var targets = jQuery( target, this ),
+                       l = targets.length;
+
+               return this.filter( function() {
+                       var i = 0;
+                       for ( ; i < l; i++ ) {
+                               if ( jQuery.contains( this, targets[ i ] ) ) {
+                                       return true;
+                               }
+                       }
+               } );
+       },
+
+       closest: function( selectors, context ) {
+               var cur,
+                       i = 0,
+                       l = this.length,
+                       matched = [],
+                       targets = typeof selectors !== "string" && jQuery( selectors );
+
+               // Positional selectors never match, since there's no _selection_ context
+               if ( !rneedsContext.test( selectors ) ) {
+                       for ( ; i < l; i++ ) {
+                               for ( cur = this[ i ]; cur && cur !== context; cur = cur.parentNode ) {
+
+                                       // Always skip document fragments
+                                       if ( cur.nodeType < 11 && ( targets ?
+                                               targets.index( cur ) > -1 :
+
+                                               // Don't pass non-elements to Sizzle
+                                               cur.nodeType === 1 &&
+                                                       jQuery.find.matchesSelector( cur, selectors ) ) ) {
+
+                                               matched.push( cur );
+                                               break;
+                                       }
+                               }
+                       }
+               }
+
+               return this.pushStack( matched.length > 1 ? jQuery.uniqueSort( matched ) : matched );
+       },
+
+       // Determine the position of an element within the set
+       index: function( elem ) {
+
+               // No argument, return index in parent
+               if ( !elem ) {
+                       return ( this[ 0 ] && this[ 0 ].parentNode ) ? this.first().prevAll().length : -1;
+               }
+
+               // Index in selector
+               if ( typeof elem === "string" ) {
+                       return indexOf.call( jQuery( elem ), this[ 0 ] );
+               }
+
+               // Locate the position of the desired element
+               return indexOf.call( this,
+
+                       // If it receives a jQuery object, the first element is used
+                       elem.jquery ? elem[ 0 ] : elem
+               );
+       },
+
+       add: function( selector, context ) {
+               return this.pushStack(
+                       jQuery.uniqueSort(
+                               jQuery.merge( this.get(), jQuery( selector, context ) )
+                       )
+               );
+       },
+
+       addBack: function( selector ) {
+               return this.add( selector == null ?
+                       this.prevObject : this.prevObject.filter( selector )
+               );
+       }
+} );
+
+function sibling( cur, dir ) {
+       while ( ( cur = cur[ dir ] ) && cur.nodeType !== 1 ) {}
+       return cur;
+}
+
+jQuery.each( {
+       parent: function( elem ) {
+               var parent = elem.parentNode;
+               return parent && parent.nodeType !== 11 ? parent : null;
+       },
+       parents: function( elem ) {
+               return dir( elem, "parentNode" );
+       },
+       parentsUntil: function( elem, i, until ) {
+               return dir( elem, "parentNode", until );
+       },
+       next: function( elem ) {
+               return sibling( elem, "nextSibling" );
+       },
+       prev: function( elem ) {
+               return sibling( elem, "previousSibling" );
+       },
+       nextAll: function( elem ) {
+               return dir( elem, "nextSibling" );
+       },
+       prevAll: function( elem ) {
+               return dir( elem, "previousSibling" );
+       },
+       nextUntil: function( elem, i, until ) {
+               return dir( elem, "nextSibling", until );
+       },
+       prevUntil: function( elem, i, until ) {
+               return dir( elem, "previousSibling", until );
+       },
+       siblings: function( elem ) {
+               return siblings( ( elem.parentNode || {} ).firstChild, elem );
+       },
+       children: function( elem ) {
+               return siblings( elem.firstChild );
+       },
+       contents: function( elem ) {
+               if ( typeof elem.contentDocument !== "undefined" ) {
+                       return elem.contentDocument;
+               }
+
+               // Support: IE 9 - 11 only, iOS 7 only, Android Browser <=4.3 only
+               // Treat the template element as a regular one in browsers that
+               // don't support it.
+               if ( nodeName( elem, "template" ) ) {
+                       elem = elem.content || elem;
+               }
+
+               return jQuery.merge( [], elem.childNodes );
+       }
+}, function( name, fn ) {
+       jQuery.fn[ name ] = function( until, selector ) {
+               var matched = jQuery.map( this, fn, until );
+
+               if ( name.slice( -5 ) !== "Until" ) {
+                       selector = until;
+               }
+
+               if ( selector && typeof selector === "string" ) {
+                       matched = jQuery.filter( selector, matched );
+               }
+
+               if ( this.length > 1 ) {
+
+                       // Remove duplicates
+                       if ( !guaranteedUnique[ name ] ) {
+                               jQuery.uniqueSort( matched );
+                       }
+
+                       // Reverse order for parents* and prev-derivatives
+                       if ( rparentsprev.test( name ) ) {
+                               matched.reverse();
+                       }
+               }
+
+               return this.pushStack( matched );
+       };
+} );
+var rnothtmlwhite = ( /[^\x20\t\r\n\f]+/g );
+
+
+
+// Convert String-formatted options into Object-formatted ones
+function createOptions( options ) {
+       var object = {};
+       jQuery.each( options.match( rnothtmlwhite ) || [], function( _, flag ) {
+               object[ flag ] = true;
+       } );
+       return object;
+}
+
+/*
+ * Create a callback list using the following parameters:
+ *
+ *     options: an optional list of space-separated options that will change how
+ *                     the callback list behaves or a more traditional option object
+ *
+ * By default a callback list will act like an event callback list and can be
+ * "fired" multiple times.
+ *
+ * Possible options:
+ *
+ *     once:                   will ensure the callback list can only be fired once (like a Deferred)
+ *
+ *     memory:                 will keep track of previous values and will call any callback added
+ *                                     after the list has been fired right away with the latest "memorized"
+ *                                     values (like a Deferred)
+ *
+ *     unique:                 will ensure a callback can only be added once (no duplicate in the list)
+ *
+ *     stopOnFalse:    interrupt callings when a callback returns false
+ *
+ */
+jQuery.Callbacks = function( options ) {
+
+       // Convert options from String-formatted to Object-formatted if needed
+       // (we check in cache first)
+       options = typeof options === "string" ?
+               createOptions( options ) :
+               jQuery.extend( {}, options );
+
+       var // Flag to know if list is currently firing
+               firing,
+
+               // Last fire value for non-forgettable lists
+               memory,
+
+               // Flag to know if list was already fired
+               fired,
+
+               // Flag to prevent firing
+               locked,
+
+               // Actual callback list
+               list = [],
+
+               // Queue of execution data for repeatable lists
+               queue = [],
+
+               // Index of currently firing callback (modified by add/remove as needed)
+               firingIndex = -1,
+
+               // Fire callbacks
+               fire = function() {
+
+                       // Enforce single-firing
+                       locked = locked || options.once;
+
+                       // Execute callbacks for all pending executions,
+                       // respecting firingIndex overrides and runtime changes
+                       fired = firing = true;
+                       for ( ; queue.length; firingIndex = -1 ) {
+                               memory = queue.shift();
+                               while ( ++firingIndex < list.length ) {
+
+                                       // Run callback and check for early termination
+                                       if ( list[ firingIndex ].apply( memory[ 0 ], memory[ 1 ] ) === false &&
+                                               options.stopOnFalse ) {
+
+                                               // Jump to end and forget the data so .add doesn't re-fire
+                                               firingIndex = list.length;
+                                               memory = false;
+                                       }
+                               }
+                       }
+
+                       // Forget the data if we're done with it
+                       if ( !options.memory ) {
+                               memory = false;
+                       }
+
+                       firing = false;
+
+                       // Clean up if we're done firing for good
+                       if ( locked ) {
+
+                               // Keep an empty list if we have data for future add calls
+                               if ( memory ) {
+                                       list = [];
+
+                               // Otherwise, this object is spent
+                               } else {
+                                       list = "";
+                               }
+                       }
+               },
+
+               // Actual Callbacks object
+               self = {
+
+                       // Add a callback or a collection of callbacks to the list
+                       add: function() {
+                               if ( list ) {
+
+                                       // If we have memory from a past run, we should fire after adding
+                                       if ( memory && !firing ) {
+                                               firingIndex = list.length - 1;
+                                               queue.push( memory );
+                                       }
+
+                                       ( function add( args ) {
+                                               jQuery.each( args, function( _, arg ) {
+                                                       if ( isFunction( arg ) ) {
+                                                               if ( !options.unique || !self.has( arg ) ) {
+                                                                       list.push( arg );
+                                                               }
+                                                       } else if ( arg && arg.length && toType( arg ) !== "string" ) {
+
+                                                               // Inspect recursively
+                                                               add( arg );
+                                                       }
+                                               } );
+                                       } )( arguments );
+
+                                       if ( memory && !firing ) {
+                                               fire();
+                                       }
+                               }
+                               return this;
+                       },
+
+                       // Remove a callback from the list
+                       remove: function() {
+                               jQuery.each( arguments, function( _, arg ) {
+                                       var index;
+                                       while ( ( index = jQuery.inArray( arg, list, index ) ) > -1 ) {
+                                               list.splice( index, 1 );
+
+                                               // Handle firing indexes
+                                               if ( index <= firingIndex ) {
+                                                       firingIndex--;
+                                               }
+                                       }
+                               } );
+                               return this;
+                       },
+
+                       // Check if a given callback is in the list.
+                       // If no argument is given, return whether or not list has callbacks attached.
+                       has: function( fn ) {
+                               return fn ?
+                                       jQuery.inArray( fn, list ) > -1 :
+                                       list.length > 0;
+                       },
+
+                       // Remove all callbacks from the list
+                       empty: function() {
+                               if ( list ) {
+                                       list = [];
+                               }
+                               return this;
+                       },
+
+                       // Disable .fire and .add
+                       // Abort any current/pending executions
+                       // Clear all callbacks and values
+                       disable: function() {
+                               locked = queue = [];
+                               list = memory = "";
+                               return this;
+                       },
+                       disabled: function() {
+                               return !list;
+                       },
+
+                       // Disable .fire
+                       // Also disable .add unless we have memory (since it would have no effect)
+                       // Abort any pending executions
+                       lock: function() {
+                               locked = queue = [];
+                               if ( !memory && !firing ) {
+                                       list = memory = "";
+                               }
+                               return this;
+                       },
+                       locked: function() {
+                               return !!locked;
+                       },
+
+                       // Call all callbacks with the given context and arguments
+                       fireWith: function( context, args ) {
+                               if ( !locked ) {
+                                       args = args || [];
+                                       args = [ context, args.slice ? args.slice() : args ];
+                                       queue.push( args );
+                                       if ( !firing ) {
+                                               fire();
+                                       }
+                               }
+                               return this;
+                       },
+
+                       // Call all the callbacks with the given arguments
+                       fire: function() {
+                               self.fireWith( this, arguments );
+                               return this;
+                       },
+
+                       // To know if the callbacks have already been called at least once
+                       fired: function() {
+                               return !!fired;
+                       }
+               };
+
+       return self;
+};
+
+
+function Identity( v ) {
+       return v;
+}
+function Thrower( ex ) {
+       throw ex;
+}
+
+function adoptValue( value, resolve, reject, noValue ) {
+       var method;
+
+       try {
+
+               // Check for promise aspect first to privilege synchronous behavior
+               if ( value && isFunction( ( method = value.promise ) ) ) {
+                       method.call( value ).done( resolve ).fail( reject );
+
+               // Other thenables
+               } else if ( value && isFunction( ( method = value.then ) ) ) {
+                       method.call( value, resolve, reject );
+
+               // Other non-thenables
+               } else {
+
+                       // Control `resolve` arguments by letting Array#slice cast boolean `noValue` to integer:
+                       // * false: [ value ].slice( 0 ) => resolve( value )
+                       // * true: [ value ].slice( 1 ) => resolve()
+                       resolve.apply( undefined, [ value ].slice( noValue ) );
+               }
+
+       // For Promises/A+, convert exceptions into rejections
+       // Since jQuery.when doesn't unwrap thenables, we can skip the extra checks appearing in
+       // Deferred#then to conditionally suppress rejection.
+       } catch ( value ) {
+
+               // Support: Android 4.0 only
+               // Strict mode functions invoked without .call/.apply get global-object context
+               reject.apply( undefined, [ value ] );
+       }
+}
+
+jQuery.extend( {
+
+       Deferred: function( func ) {
+               var tuples = [
+
+                               // action, add listener, callbacks,
+                               // ... .then handlers, argument index, [final state]
+                               [ "notify", "progress", jQuery.Callbacks( "memory" ),
+                                       jQuery.Callbacks( "memory" ), 2 ],
+                               [ "resolve", "done", jQuery.Callbacks( "once memory" ),
+                                       jQuery.Callbacks( "once memory" ), 0, "resolved" ],
+                               [ "reject", "fail", jQuery.Callbacks( "once memory" ),
+                                       jQuery.Callbacks( "once memory" ), 1, "rejected" ]
+                       ],
+                       state = "pending",
+                       promise = {
+                               state: function() {
+                                       return state;
+                               },
+                               always: function() {
+                                       deferred.done( arguments ).fail( arguments );
+                                       return this;
+                               },
+                               "catch": function( fn ) {
+                                       return promise.then( null, fn );
+                               },
+
+                               // Keep pipe for back-compat
+                               pipe: function( /* fnDone, fnFail, fnProgress */ ) {
+                                       var fns = arguments;
+
+                                       return jQuery.Deferred( function( newDefer ) {
+                                               jQuery.each( tuples, function( i, tuple ) {
+
+                                                       // Map tuples (progress, done, fail) to arguments (done, fail, progress)
+                                                       var fn = isFunction( fns[ tuple[ 4 ] ] ) && fns[ tuple[ 4 ] ];
+
+                                                       // deferred.progress(function() { bind to newDefer or newDefer.notify })
+                                                       // deferred.done(function() { bind to newDefer or newDefer.resolve })
+                                                       // deferred.fail(function() { bind to newDefer or newDefer.reject })
+                                                       deferred[ tuple[ 1 ] ]( function() {
+                                                               var returned = fn && fn.apply( this, arguments );
+                                                               if ( returned && isFunction( returned.promise ) ) {
+                                                                       returned.promise()
+                                                                               .progress( newDefer.notify )
+                                                                               .done( newDefer.resolve )
+                                                                               .fail( newDefer.reject );
+                                                               } else {
+                                                                       newDefer[ tuple[ 0 ] + "With" ](
+                                                                               this,
+                                                                               fn ? [ returned ] : arguments
+                                                                       );
+                                                               }
+                                                       } );
+                                               } );
+                                               fns = null;
+                                       } ).promise();
+                               },
+                               then: function( onFulfilled, onRejected, onProgress ) {
+                                       var maxDepth = 0;
+                                       function resolve( depth, deferred, handler, special ) {
+                                               return function() {
+                                                       var that = this,
+                                                               args = arguments,
+                                                               mightThrow = function() {
+                                                                       var returned, then;
+
+                                                                       // Support: Promises/A+ section 2.3.3.3.3
+                                                                       // https://promisesaplus.com/#point-59
+                                                                       // Ignore double-resolution attempts
+                                                                       if ( depth < maxDepth ) {
+                                                                               return;
+                                                                       }
+
+                                                                       returned = handler.apply( that, args );
+
+                                                                       // Support: Promises/A+ section 2.3.1
+                                                                       // https://promisesaplus.com/#point-48
+                                                                       if ( returned === deferred.promise() ) {
+                                                                               throw new TypeError( "Thenable self-resolution" );
+                                                                       }
+
+                                                                       // Support: Promises/A+ sections 2.3.3.1, 3.5
+                                                                       // https://promisesaplus.com/#point-54
+                                                                       // https://promisesaplus.com/#point-75
+                                                                       // Retrieve `then` only once
+                                                                       then = returned &&
+
+                                                                               // Support: Promises/A+ section 2.3.4
+                                                                               // https://promisesaplus.com/#point-64
+                                                                               // Only check objects and functions for thenability
+                                                                               ( typeof returned === "object" ||
+                                                                                       typeof returned === "function" ) &&
+                                                                               returned.then;
+
+                                                                       // Handle a returned thenable
+                                                                       if ( isFunction( then ) ) {
+
+                                                                               // Special processors (notify) just wait for resolution
+                                                                               if ( special ) {
+                                                                                       then.call(
+                                                                                               returned,
+                                                                                               resolve( maxDepth, deferred, Identity, special ),
+                                                                                               resolve( maxDepth, deferred, Thrower, special )
+                                                                                       );
+
+                                                                               // Normal processors (resolve) also hook into progress
+                                                                               } else {
+
+                                                                                       // ...and disregard older resolution values
+                                                                                       maxDepth++;
+
+                                                                                       then.call(
+                                                                                               returned,
+                                                                                               resolve( maxDepth, deferred, Identity, special ),
+                                                                                               resolve( maxDepth, deferred, Thrower, special ),
+                                                                                               resolve( maxDepth, deferred, Identity,
+                                                                                                       deferred.notifyWith )
+                                                                                       );
+                                                                               }
+
+                                                                       // Handle all other returned values
+                                                                       } else {
+
+                                                                               // Only substitute handlers pass on context
+                                                                               // and multiple values (non-spec behavior)
+                                                                               if ( handler !== Identity ) {
+                                                                                       that = undefined;
+                                                                                       args = [ returned ];
+                                                                               }
+
+                                                                               // Process the value(s)
+                                                                               // Default process is resolve
+                                                                               ( special || deferred.resolveWith )( that, args );
+                                                                       }
+                                                               },
+
+                                                               // Only normal processors (resolve) catch and reject exceptions
+                                                               process = special ?
+                                                                       mightThrow :
+                                                                       function() {
+                                                                               try {
+                                                                                       mightThrow();
+                                                                               } catch ( e ) {
+
+                                                                                       if ( jQuery.Deferred.exceptionHook ) {
+                                                                                               jQuery.Deferred.exceptionHook( e,
+                                                                                                       process.stackTrace );
+                                                                                       }
+
+                                                                                       // Support: Promises/A+ section 2.3.3.3.4.1
+                                                                                       // https://promisesaplus.com/#point-61
+                                                                                       // Ignore post-resolution exceptions
+                                                                                       if ( depth + 1 >= maxDepth ) {
+
+                                                                                               // Only substitute handlers pass on context
+                                                                                               // and multiple values (non-spec behavior)
+                                                                                               if ( handler !== Thrower ) {
+                                                                                                       that = undefined;
+                                                                                                       args = [ e ];
+                                                                                               }
+
+                                                                                               deferred.rejectWith( that, args );
+                                                                                       }
+                                                                               }
+                                                                       };
+
+                                                       // Support: Promises/A+ section 2.3.3.3.1
+                                                       // https://promisesaplus.com/#point-57
+                                                       // Re-resolve promises immediately to dodge false rejection from
+                                                       // subsequent errors
+                                                       if ( depth ) {
+                                                               process();
+                                                       } else {
+
+                                                               // Call an optional hook to record the stack, in case of exception
+                                                               // since it's otherwise lost when execution goes async
+                                                               if ( jQuery.Deferred.getStackHook ) {
+                                                                       process.stackTrace = jQuery.Deferred.getStackHook();
+                                                               }
+                                                               window.setTimeout( process );
+                                                       }
+                                               };
+                                       }
+
+                                       return jQuery.Deferred( function( newDefer ) {
+
+                                               // progress_handlers.add( ... )
+                                               tuples[ 0 ][ 3 ].add(
+                                                       resolve(
+                                                               0,
+                                                               newDefer,
+                                                               isFunction( onProgress ) ?
+                                                                       onProgress :
+                                                                       Identity,
+                                                               newDefer.notifyWith
+                                                       )
+                                               );
+
+                                               // fulfilled_handlers.add( ... )
+                                               tuples[ 1 ][ 3 ].add(
+                                                       resolve(
+                                                               0,
+                                                               newDefer,
+                                                               isFunction( onFulfilled ) ?
+                                                                       onFulfilled :
+                                                                       Identity
+                                                       )
+                                               );
+
+                                               // rejected_handlers.add( ... )
+                                               tuples[ 2 ][ 3 ].add(
+                                                       resolve(
+                                                               0,
+                                                               newDefer,
+                                                               isFunction( onRejected ) ?
+                                                                       onRejected :
+                                                                       Thrower
+                                                       )
+                                               );
+                                       } ).promise();
+                               },
+
+                               // Get a promise for this deferred
+                               // If obj is provided, the promise aspect is added to the object
+                               promise: function( obj ) {
+                                       return obj != null ? jQuery.extend( obj, promise ) : promise;
+                               }
+                       },
+                       deferred = {};
+
+               // Add list-specific methods
+               jQuery.each( tuples, function( i, tuple ) {
+                       var list = tuple[ 2 ],
+                               stateString = tuple[ 5 ];
+
+                       // promise.progress = list.add
+                       // promise.done = list.add
+                       // promise.fail = list.add
+                       promise[ tuple[ 1 ] ] = list.add;
+
+                       // Handle state
+                       if ( stateString ) {
+                               list.add(
+                                       function() {
+
+                                               // state = "resolved" (i.e., fulfilled)
+                                               // state = "rejected"
+                                               state = stateString;
+                                       },
+
+                                       // rejected_callbacks.disable
+                                       // fulfilled_callbacks.disable
+                                       tuples[ 3 - i ][ 2 ].disable,
+
+                                       // rejected_handlers.disable
+                                       // fulfilled_handlers.disable
+                                       tuples[ 3 - i ][ 3 ].disable,
+
+                                       // progress_callbacks.lock
+                                       tuples[ 0 ][ 2 ].lock,
+
+                                       // progress_handlers.lock
+                                       tuples[ 0 ][ 3 ].lock
+                               );
+                       }
+
+                       // progress_handlers.fire
+                       // fulfilled_handlers.fire
+                       // rejected_handlers.fire
+                       list.add( tuple[ 3 ].fire );
+
+                       // deferred.notify = function() { deferred.notifyWith(...) }
+                       // deferred.resolve = function() { deferred.resolveWith(...) }
+                       // deferred.reject = function() { deferred.rejectWith(...) }
+                       deferred[ tuple[ 0 ] ] = function() {
+                               deferred[ tuple[ 0 ] + "With" ]( this === deferred ? undefined : this, arguments );
+                               return this;
+                       };
+
+                       // deferred.notifyWith = list.fireWith
+                       // deferred.resolveWith = list.fireWith
+                       // deferred.rejectWith = list.fireWith
+                       deferred[ tuple[ 0 ] + "With" ] = list.fireWith;
+               } );
+
+               // Make the deferred a promise
+               promise.promise( deferred );
+
+               // Call given func if any
+               if ( func ) {
+                       func.call( deferred, deferred );
+               }
+
+               // All done!
+               return deferred;
+       },
+
+       // Deferred helper
+       when: function( singleValue ) {
+               var
+
+                       // count of uncompleted subordinates
+                       remaining = arguments.length,
+
+                       // count of unprocessed arguments
+                       i = remaining,
+
+                       // subordinate fulfillment data
+                       resolveContexts = Array( i ),
+                       resolveValues = slice.call( arguments ),
+
+                       // the master Deferred
+                       master = jQuery.Deferred(),
+
+                       // subordinate callback factory
+                       updateFunc = function( i ) {
+                               return function( value ) {
+                                       resolveContexts[ i ] = this;
+                                       resolveValues[ i ] = arguments.length > 1 ? slice.call( arguments ) : value;
+                                       if ( !( --remaining ) ) {
+                                               master.resolveWith( resolveContexts, resolveValues );
+                                       }
+                               };
+                       };
+
+               // Single- and empty arguments are adopted like Promise.resolve
+               if ( remaining <= 1 ) {
+                       adoptValue( singleValue, master.done( updateFunc( i ) ).resolve, master.reject,
+                               !remaining );
+
+                       // Use .then() to unwrap secondary thenables (cf. gh-3000)
+                       if ( master.state() === "pending" ||
+                               isFunction( resolveValues[ i ] && resolveValues[ i ].then ) ) {
+
+                               return master.then();
+                       }
+               }
+
+               // Multiple arguments are aggregated like Promise.all array elements
+               while ( i-- ) {
+                       adoptValue( resolveValues[ i ], updateFunc( i ), master.reject );
+               }
+
+               return master.promise();
+       }
+} );
+
+
+// These usually indicate a programmer mistake during development,
+// warn about them ASAP rather than swallowing them by default.
+var rerrorNames = /^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;
+
+jQuery.Deferred.exceptionHook = function( error, stack ) {
+
+       // Support: IE 8 - 9 only
+       // Console exists when dev tools are open, which can happen at any time
+       if ( window.console && window.console.warn && error && rerrorNames.test( error.name ) ) {
+               window.console.warn( "jQuery.Deferred exception: " + error.message, error.stack, stack );
+       }
+};
+
+
+
+
+jQuery.readyException = function( error ) {
+       window.setTimeout( function() {
+               throw error;
+       } );
+};
+
+
+
+
+// The deferred used on DOM ready
+var readyList = jQuery.Deferred();
+
+jQuery.fn.ready = function( fn ) {
+
+       readyList
+               .then( fn )
+
+               // Wrap jQuery.readyException in a function so that the lookup
+               // happens at the time of error handling instead of callback
+               // registration.
+               .catch( function( error ) {
+                       jQuery.readyException( error );
+               } );
+
+       return this;
+};
+
+jQuery.extend( {
+
+       // Is the DOM ready to be used? Set to true once it occurs.
+       isReady: false,
+
+       // A counter to track how many items to wait for before
+       // the ready event fires. See #6781
+       readyWait: 1,
+
+       // Handle when the DOM is ready
+       ready: function( wait ) {
+
+               // Abort if there are pending holds or we're already ready
+               if ( wait === true ? --jQuery.readyWait : jQuery.isReady ) {
+                       return;
+               }
+
+               // Remember that the DOM is ready
+               jQuery.isReady = true;
+
+               // If a normal DOM Ready event fired, decrement, and wait if need be
+               if ( wait !== true && --jQuery.readyWait > 0 ) {
+                       return;
+               }
+
+               // If there are functions bound, to execute
+               readyList.resolveWith( document, [ jQuery ] );
+       }
+} );
+
+jQuery.ready.then = readyList.then;
+
+// The ready event handler and self cleanup method
+function completed() {
+       document.removeEventListener( "DOMContentLoaded", completed );
+       window.removeEventListener( "load", completed );
+       jQuery.ready();
+}
+
+// Catch cases where $(document).ready() is called
+// after the browser event has already occurred.
+// Support: IE <=9 - 10 only
+// Older IE sometimes signals "interactive" too soon
+if ( document.readyState === "complete" ||
+       ( document.readyState !== "loading" && !document.documentElement.doScroll ) ) {
+
+       // Handle it asynchronously to allow scripts the opportunity to delay ready
+       window.setTimeout( jQuery.ready );
+
+} else {
+
+       // Use the handy event callback
+       document.addEventListener( "DOMContentLoaded", completed );
+
+       // A fallback to window.onload, that will always work
+       window.addEventListener( "load", completed );
+}
+
+
+
+
+// Multifunctional method to get and set values of a collection
+// The value/s can optionally be executed if it's a function
+var access = function( elems, fn, key, value, chainable, emptyGet, raw ) {
+       var i = 0,
+               len = elems.length,
+               bulk = key == null;
+
+       // Sets many values
+       if ( toType( key ) === "object" ) {
+               chainable = true;
+               for ( i in key ) {
+                       access( elems, fn, i, key[ i ], true, emptyGet, raw );
+               }
+
+       // Sets one value
+       } else if ( value !== undefined ) {
+               chainable = true;
+
+               if ( !isFunction( value ) ) {
+                       raw = true;
+               }
+
+               if ( bulk ) {
+
+                       // Bulk operations run against the entire set
+                       if ( raw ) {
+                               fn.call( elems, value );
+                               fn = null;
+
+                       // ...except when executing function values
+                       } else {
+                               bulk = fn;
+                               fn = function( elem, key, value ) {
+                                       return bulk.call( jQuery( elem ), value );
+                               };
+                       }
+               }
+
+               if ( fn ) {
+                       for ( ; i < len; i++ ) {
+                               fn(
+                                       elems[ i ], key, raw ?
+                                       value :
+                                       value.call( elems[ i ], i, fn( elems[ i ], key ) )
+                               );
+                       }
+               }
+       }
+
+       if ( chainable ) {
+               return elems;
+       }
+
+       // Gets
+       if ( bulk ) {
+               return fn.call( elems );
+       }
+
+       return len ? fn( elems[ 0 ], key ) : emptyGet;
+};
+
+
+// Matches dashed string for camelizing
+var rmsPrefix = /^-ms-/,
+       rdashAlpha = /-([a-z])/g;
+
+// Used by camelCase as callback to replace()
+function fcamelCase( all, letter ) {
+       return letter.toUpperCase();
+}
+
+// Convert dashed to camelCase; used by the css and data modules
+// Support: IE <=9 - 11, Edge 12 - 15
+// Microsoft forgot to hump their vendor prefix (#9572)
+function camelCase( string ) {
+       return string.replace( rmsPrefix, "ms-" ).replace( rdashAlpha, fcamelCase );
+}
+var acceptData = function( owner ) {
+
+       // Accepts only:
+       //  - Node
+       //    - Node.ELEMENT_NODE
+       //    - Node.DOCUMENT_NODE
+       //  - Object
+       //    - Any
+       return owner.nodeType === 1 || owner.nodeType === 9 || !( +owner.nodeType );
+};
+
+
+
+
+function Data() {
+       this.expando = jQuery.expando + Data.uid++;
+}
+
+Data.uid = 1;
+
+Data.prototype = {
+
+       cache: function( owner ) {
+
+               // Check if the owner object already has a cache
+               var value = owner[ this.expando ];
+
+               // If not, create one
+               if ( !value ) {
+                       value = {};
+
+                       // We can accept data for non-element nodes in modern browsers,
+                       // but we should not, see #8335.
+                       // Always return an empty object.
+                       if ( acceptData( owner ) ) {
+
+                               // If it is a node unlikely to be stringify-ed or looped over
+                               // use plain assignment
+                               if ( owner.nodeType ) {
+                                       owner[ this.expando ] = value;
+
+                               // Otherwise secure it in a non-enumerable property
+                               // configurable must be true to allow the property to be
+                               // deleted when data is removed
+                               } else {
+                                       Object.defineProperty( owner, this.expando, {
+                                               value: value,
+                                               configurable: true
+                                       } );
+                               }
+                       }
+               }
+
+               return value;
+       },
+       set: function( owner, data, value ) {
+               var prop,
+                       cache = this.cache( owner );
+
+               // Handle: [ owner, key, value ] args
+               // Always use camelCase key (gh-2257)
+               if ( typeof data === "string" ) {
+                       cache[ camelCase( data ) ] = value;
+
+               // Handle: [ owner, { properties } ] args
+               } else {
+
+                       // Copy the properties one-by-one to the cache object
+                       for ( prop in data ) {
+                               cache[ camelCase( prop ) ] = data[ prop ];
+                       }
+               }
+               return cache;
+       },
+       get: function( owner, key ) {
+               return key === undefined ?
+                       this.cache( owner ) :
+
+                       // Always use camelCase key (gh-2257)
+                       owner[ this.expando ] && owner[ this.expando ][ camelCase( key ) ];
+       },
+       access: function( owner, key, value ) {
+
+               // In cases where either:
+               //
+               //   1. No key was specified
+               //   2. A string key was specified, but no value provided
+               //
+               // Take the "read" path and allow the get method to determine
+               // which value to return, respectively either:
+               //
+               //   1. The entire cache object
+               //   2. The data stored at the key
+               //
+               if ( key === undefined ||
+                               ( ( key && typeof key === "string" ) && value === undefined ) ) {
+
+                       return this.get( owner, key );
+               }
+
+               // When the key is not a string, or both a key and value
+               // are specified, set or extend (existing objects) with either:
+               //
+               //   1. An object of properties
+               //   2. A key and value
+               //
+               this.set( owner, key, value );
+
+               // Since the "set" path can have two possible entry points
+               // return the expected data based on which path was taken[*]
+               return value !== undefined ? value : key;
+       },
+       remove: function( owner, key ) {
+               var i,
+                       cache = owner[ this.expando ];
+
+               if ( cache === undefined ) {
+                       return;
+               }
+
+               if ( key !== undefined ) {
+
+                       // Support array or space separated string of keys
+                       if ( Array.isArray( key ) ) {
+
+                               // If key is an array of keys...
+                               // We always set camelCase keys, so remove that.
+                               key = key.map( camelCase );
+                       } else {
+                               key = camelCase( key );
+
+                               // If a key with the spaces exists, use it.
+                               // Otherwise, create an array by matching non-whitespace
+                               key = key in cache ?
+                                       [ key ] :
+                                       ( key.match( rnothtmlwhite ) || [] );
+                       }
+
+                       i = key.length;
+
+                       while ( i-- ) {
+                               delete cache[ key[ i ] ];
+                       }
+               }
+
+               // Remove the expando if there's no more data
+               if ( key === undefined || jQuery.isEmptyObject( cache ) ) {
+
+                       // Support: Chrome <=35 - 45
+                       // Webkit & Blink performance suffers when deleting properties
+                       // from DOM nodes, so set to undefined instead
+                       // https://bugs.chromium.org/p/chromium/issues/detail?id=378607 (bug restricted)
+                       if ( owner.nodeType ) {
+                               owner[ this.expando ] = undefined;
+                       } else {
+                               delete owner[ this.expando ];
+                       }
+               }
+       },
+       hasData: function( owner ) {
+               var cache = owner[ this.expando ];
+               return cache !== undefined && !jQuery.isEmptyObject( cache );
+       }
+};
+var dataPriv = new Data();
+
+var dataUser = new Data();
+
+
+
+//     Implementation Summary
+//
+//     1. Enforce API surface and semantic compatibility with 1.9.x branch
+//     2. Improve the module's maintainability by reducing the storage
+//             paths to a single mechanism.
+//     3. Use the same single mechanism to support "private" and "user" data.
+//     4. _Never_ expose "private" data to user code (TODO: Drop _data, _removeData)
+//     5. Avoid exposing implementation details on user objects (eg. expando properties)
+//     6. Provide a clear path for implementation upgrade to WeakMap in 2014
+
+var rbrace = /^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,
+       rmultiDash = /[A-Z]/g;
+
+function getData( data ) {
+       if ( data === "true" ) {
+               return true;
+       }
+
+       if ( data === "false" ) {
+               return false;
+       }
+
+       if ( data === "null" ) {
+               return null;
+       }
+
+       // Only convert to a number if it doesn't change the string
+       if ( data === +data + "" ) {
+               return +data;
+       }
+
+       if ( rbrace.test( data ) ) {
+               return JSON.parse( data );
+       }
+
+       return data;
+}
+
+function dataAttr( elem, key, data ) {
+       var name;
+
+       // If nothing was found internally, try to fetch any
+       // data from the HTML5 data-* attribute
+       if ( data === undefined && elem.nodeType === 1 ) {
+               name = "data-" + key.replace( rmultiDash, "-$&" ).toLowerCase();
+               data = elem.getAttribute( name );
+
+               if ( typeof data === "string" ) {
+                       try {
+                               data = getData( data );
+                       } catch ( e ) {}
+
+                       // Make sure we set the data so it isn't changed later
+                       dataUser.set( elem, key, data );
+               } else {
+                       data = undefined;
+               }
+       }
+       return data;
+}
+
+jQuery.extend( {
+       hasData: function( elem ) {
+               return dataUser.hasData( elem ) || dataPriv.hasData( elem );
+       },
+
+       data: function( elem, name, data ) {
+               return dataUser.access( elem, name, data );
+       },
+
+       removeData: function( elem, name ) {
+               dataUser.remove( elem, name );
+       },
+
+       // TODO: Now that all calls to _data and _removeData have been replaced
+       // with direct calls to dataPriv methods, these can be deprecated.
+       _data: function( elem, name, data ) {
+               return dataPriv.access( elem, name, data );
+       },
+
+       _removeData: function( elem, name ) {
+               dataPriv.remove( elem, name );
+       }
+} );
+
+jQuery.fn.extend( {
+       data: function( key, value ) {
+               var i, name, data,
+                       elem = this[ 0 ],
+                       attrs = elem && elem.attributes;
+
+               // Gets all values
+               if ( key === undefined ) {
+                       if ( this.length ) {
+                               data = dataUser.get( elem );
+
+                               if ( elem.nodeType === 1 && !dataPriv.get( elem, "hasDataAttrs" ) ) {
+                                       i = attrs.length;
+                                       while ( i-- ) {
+
+                                               // Support: IE 11 only
+                                               // The attrs elements can be null (#14894)
+                                               if ( attrs[ i ] ) {
+                                                       name = attrs[ i ].name;
+                                                       if ( name.indexOf( "data-" ) === 0 ) {
+                                                               name = camelCase( name.slice( 5 ) );
+                                                               dataAttr( elem, name, data[ name ] );
+                                                       }
+                                               }
+                                       }
+                                       dataPriv.set( elem, "hasDataAttrs", true );
+                               }
+                       }
+
+                       return data;
+               }
+
+               // Sets multiple values
+               if ( typeof key === "object" ) {
+                       return this.each( function() {
+                               dataUser.set( this, key );
+                       } );
+               }
+
+               return access( this, function( value ) {
+                       var data;
+
+                       // The calling jQuery object (element matches) is not empty
+                       // (and therefore has an element appears at this[ 0 ]) and the
+                       // `value` parameter was not undefined. An empty jQuery object
+                       // will result in `undefined` for elem = this[ 0 ] which will
+                       // throw an exception if an attempt to read a data cache is made.
+                       if ( elem && value === undefined ) {
+
+                               // Attempt to get data from the cache
+                               // The key will always be camelCased in Data
+                               data = dataUser.get( elem, key );
+                               if ( data !== undefined ) {
+                                       return data;
+                               }
+
+                               // Attempt to "discover" the data in
+                               // HTML5 custom data-* attrs
+                               data = dataAttr( elem, key );
+                               if ( data !== undefined ) {
+                                       return data;
+                               }
+
+                               // We tried really hard, but the data doesn't exist.
+                               return;
+                       }
+
+                       // Set the data...
+                       this.each( function() {
+
+                               // We always store the camelCased key
+                               dataUser.set( this, key, value );
+                       } );
+               }, null, value, arguments.length > 1, null, true );
+       },
+
+       removeData: function( key ) {
+               return this.each( function() {
+                       dataUser.remove( this, key );
+               } );
+       }
+} );
+
+
+jQuery.extend( {
+       queue: function( elem, type, data ) {
+               var queue;
+
+               if ( elem ) {
+                       type = ( type || "fx" ) + "queue";
+                       queue = dataPriv.get( elem, type );
+
+                       // Speed up dequeue by getting out quickly if this is just a lookup
+                       if ( data ) {
+                               if ( !queue || Array.isArray( data ) ) {
+                                       queue = dataPriv.access( elem, type, jQuery.makeArray( data ) );
+                               } else {
+                                       queue.push( data );
+                               }
+                       }
+                       return queue || [];
+               }
+       },
+
+       dequeue: function( elem, type ) {
+               type = type || "fx";
+
+               var queue = jQuery.queue( elem, type ),
+                       startLength = queue.length,
+                       fn = queue.shift(),
+                       hooks = jQuery._queueHooks( elem, type ),
+                       next = function() {
+                               jQuery.dequeue( elem, type );
+                       };
+
+               // If the fx queue is dequeued, always remove the progress sentinel
+               if ( fn === "inprogress" ) {
+                       fn = queue.shift();
+                       startLength--;
+               }
+
+               if ( fn ) {
+
+                       // Add a progress sentinel to prevent the fx queue from being
+                       // automatically dequeued
+                       if ( type === "fx" ) {
+                               queue.unshift( "inprogress" );
+                       }
+
+                       // Clear up the last queue stop function
+                       delete hooks.stop;
+                       fn.call( elem, next, hooks );
+               }
+
+               if ( !startLength && hooks ) {
+                       hooks.empty.fire();
+               }
+       },
+
+       // Not public - generate a queueHooks object, or return the current one
+       _queueHooks: function( elem, type ) {
+               var key = type + "queueHooks";
+               return dataPriv.get( elem, key ) || dataPriv.access( elem, key, {
+                       empty: jQuery.Callbacks( "once memory" ).add( function() {
+                               dataPriv.remove( elem, [ type + "queue", key ] );
+                       } )
+               } );
+       }
+} );
+
+jQuery.fn.extend( {
+       queue: function( type, data ) {
+               var setter = 2;
+
+               if ( typeof type !== "string" ) {
+                       data = type;
+                       type = "fx";
+                       setter--;
+               }
+
+               if ( arguments.length < setter ) {
+                       return jQuery.queue( this[ 0 ], type );
+               }
+
+               return data === undefined ?
+                       this :
+                       this.each( function() {
+                               var queue = jQuery.queue( this, type, data );
+
+                               // Ensure a hooks for this queue
+                               jQuery._queueHooks( this, type );
+
+                               if ( type === "fx" && queue[ 0 ] !== "inprogress" ) {
+                                       jQuery.dequeue( this, type );
+                               }
+                       } );
+       },
+       dequeue: function( type ) {
+               return this.each( function() {
+                       jQuery.dequeue( this, type );
+               } );
+       },
+       clearQueue: function( type ) {
+               return this.queue( type || "fx", [] );
+       },
+
+       // Get a promise resolved when queues of a certain type
+       // are emptied (fx is the type by default)
+       promise: function( type, obj ) {
+               var tmp,
+                       count = 1,
+                       defer = jQuery.Deferred(),
+                       elements = this,
+                       i = this.length,
+                       resolve = function() {
+                               if ( !( --count ) ) {
+                                       defer.resolveWith( elements, [ elements ] );
+                               }
+                       };
+
+               if ( typeof type !== "string" ) {
+                       obj = type;
+                       type = undefined;
+               }
+               type = type || "fx";
+
+               while ( i-- ) {
+                       tmp = dataPriv.get( elements[ i ], type + "queueHooks" );
+                       if ( tmp && tmp.empty ) {
+                               count++;
+                               tmp.empty.add( resolve );
+                       }
+               }
+               resolve();
+               return defer.promise( obj );
+       }
+} );
+var pnum = ( /[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/ ).source;
+
+var rcssNum = new RegExp( "^(?:([+-])=|)(" + pnum + ")([a-z%]*)$", "i" );
+
+
+var cssExpand = [ "Top", "Right", "Bottom", "Left" ];
+
+var documentElement = document.documentElement;
+
+
+
+       var isAttached = function( elem ) {
+                       return jQuery.contains( elem.ownerDocument, elem );
+               },
+               composed = { composed: true };
+
+       // Support: IE 9 - 11+, Edge 12 - 18+, iOS 10.0 - 10.2 only
+       // Check attachment across shadow DOM boundaries when possible (gh-3504)
+       // Support: iOS 10.0-10.2 only
+       // Early iOS 10 versions support `attachShadow` but not `getRootNode`,
+       // leading to errors. We need to check for `getRootNode`.
+       if ( documentElement.getRootNode ) {
+               isAttached = function( elem ) {
+                       return jQuery.contains( elem.ownerDocument, elem ) ||
+                               elem.getRootNode( composed ) === elem.ownerDocument;
+               };
+       }
+var isHiddenWithinTree = function( elem, el ) {
+
+               // isHiddenWithinTree might be called from jQuery#filter function;
+               // in that case, element will be second argument
+               elem = el || elem;
+
+               // Inline style trumps all
+               return elem.style.display === "none" ||
+                       elem.style.display === "" &&
+
+                       // Otherwise, check computed style
+                       // Support: Firefox <=43 - 45
+                       // Disconnected elements can have computed display: none, so first confirm that elem is
+                       // in the document.
+                       isAttached( elem ) &&
+
+                       jQuery.css( elem, "display" ) === "none";
+       };
+
+var swap = function( elem, options, callback, args ) {
+       var ret, name,
+               old = {};
+
+       // Remember the old values, and insert the new ones
+       for ( name in options ) {
+               old[ name ] = elem.style[ name ];
+               elem.style[ name ] = options[ name ];
+       }
+
+       ret = callback.apply( elem, args || [] );
+
+       // Revert the old values
+       for ( name in options ) {
+               elem.style[ name ] = old[ name ];
+       }
+
+       return ret;
+};
+
+
+
+
+function adjustCSS( elem, prop, valueParts, tween ) {
+       var adjusted, scale,
+               maxIterations = 20,
+               currentValue = tween ?
+                       function() {
+                               return tween.cur();
+                       } :
+                       function() {
+                               return jQuery.css( elem, prop, "" );
+                       },
+               initial = currentValue(),
+               unit = valueParts && valueParts[ 3 ] || ( jQuery.cssNumber[ prop ] ? "" : "px" ),
+
+               // Starting value computation is required for potential unit mismatches
+               initialInUnit = elem.nodeType &&
+                       ( jQuery.cssNumber[ prop ] || unit !== "px" && +initial ) &&
+                       rcssNum.exec( jQuery.css( elem, prop ) );
+
+       if ( initialInUnit && initialInUnit[ 3 ] !== unit ) {
+
+               // Support: Firefox <=54
+               // Halve the iteration target value to prevent interference from CSS upper bounds (gh-2144)
+               initial = initial / 2;
+
+               // Trust units reported by jQuery.css
+               unit = unit || initialInUnit[ 3 ];
+
+               // Iteratively approximate from a nonzero starting point
+               initialInUnit = +initial || 1;
+
+               while ( maxIterations-- ) {
+
+                       // Evaluate and update our best guess (doubling guesses that zero out).
+                       // Finish if the scale equals or crosses 1 (making the old*new product non-positive).
+                       jQuery.style( elem, prop, initialInUnit + unit );
+                       if ( ( 1 - scale ) * ( 1 - ( scale = currentValue() / initial || 0.5 ) ) <= 0 ) {
+                               maxIterations = 0;
+                       }
+                       initialInUnit = initialInUnit / scale;
+
+               }
+
+               initialInUnit = initialInUnit * 2;
+               jQuery.style( elem, prop, initialInUnit + unit );
+
+               // Make sure we update the tween properties later on
+               valueParts = valueParts || [];
+       }
+
+       if ( valueParts ) {
+               initialInUnit = +initialInUnit || +initial || 0;
+
+               // Apply relative offset (+=/-=) if specified
+               adjusted = valueParts[ 1 ] ?
+                       initialInUnit + ( valueParts[ 1 ] + 1 ) * valueParts[ 2 ] :
+                       +valueParts[ 2 ];
+               if ( tween ) {
+                       tween.unit = unit;
+                       tween.start = initialInUnit;
+                       tween.end = adjusted;
+               }
+       }
+       return adjusted;
+}
+
+
+var defaultDisplayMap = {};
+
+function getDefaultDisplay( elem ) {
+       var temp,
+               doc = elem.ownerDocument,
+               nodeName = elem.nodeName,
+               display = defaultDisplayMap[ nodeName ];
+
+       if ( display ) {
+               return display;
+       }
+
+       temp = doc.body.appendChild( doc.createElement( nodeName ) );
+       display = jQuery.css( temp, "display" );
+
+       temp.parentNode.removeChild( temp );
+
+       if ( display === "none" ) {
+               display = "block";
+       }
+       defaultDisplayMap[ nodeName ] = display;
+
+       return display;
+}
+
+function showHide( elements, show ) {
+       var display, elem,
+               values = [],
+               index = 0,
+               length = elements.length;
+
+       // Determine new display value for elements that need to change
+       for ( ; index < length; index++ ) {
+               elem = elements[ index ];
+               if ( !elem.style ) {
+                       continue;
+               }
+
+               display = elem.style.display;
+               if ( show ) {
+
+                       // Since we force visibility upon cascade-hidden elements, an immediate (and slow)
+                       // check is required in this first loop unless we have a nonempty display value (either
+                       // inline or about-to-be-restored)
+                       if ( display === "none" ) {
+                               values[ index ] = dataPriv.get( elem, "display" ) || null;
+                               if ( !values[ index ] ) {
+                                       elem.style.display = "";
+                               }
+                       }
+                       if ( elem.style.display === "" && isHiddenWithinTree( elem ) ) {
+                               values[ index ] = getDefaultDisplay( elem );
+                       }
+               } else {
+                       if ( display !== "none" ) {
+                               values[ index ] = "none";
+
+                               // Remember what we're overwriting
+                               dataPriv.set( elem, "display", display );
+                       }
+               }
+       }
+
+       // Set the display of the elements in a second loop to avoid constant reflow
+       for ( index = 0; index < length; index++ ) {
+               if ( values[ index ] != null ) {
+                       elements[ index ].style.display = values[ index ];
+               }
+       }
+
+       return elements;
+}
+
+jQuery.fn.extend( {
+       show: function() {
+               return showHide( this, true );
+       },
+       hide: function() {
+               return showHide( this );
+       },
+       toggle: function( state ) {
+               if ( typeof state === "boolean" ) {
+                       return state ? this.show() : this.hide();
+               }
+
+               return this.each( function() {
+                       if ( isHiddenWithinTree( this ) ) {
+                               jQuery( this ).show();
+                       } else {
+                               jQuery( this ).hide();
+                       }
+               } );
+       }
+} );
+var rcheckableType = ( /^(?:checkbox|radio)$/i );
+
+var rtagName = ( /<([a-z][^\/\0>\x20\t\r\n\f]*)/i );
+
+var rscriptType = ( /^$|^module$|\/(?:java|ecma)script/i );
+
+
+
+// We have to close these tags to support XHTML (#13200)
+var wrapMap = {
+
+       // Support: IE <=9 only
+       option: [ 1, "<select multiple='multiple'>", "</select>" ],
+
+       // XHTML parsers do not magically insert elements in the
+       // same way that tag soup parsers do. So we cannot shorten
+       // this by omitting <tbody> or other required elements.
+       thead: [ 1, "<table>", "</table>" ],
+       col: [ 2, "<table><colgroup>", "</colgroup></table>" ],
+       tr: [ 2, "<table><tbody>", "</tbody></table>" ],
+       td: [ 3, "<table><tbody><tr>", "</tr></tbody></table>" ],
+
+       _default: [ 0, "", "" ]
+};
+
+// Support: IE <=9 only
+wrapMap.optgroup = wrapMap.option;
+
+wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead;
+wrapMap.th = wrapMap.td;
+
+
+function getAll( context, tag ) {
+
+       // Support: IE <=9 - 11 only
+       // Use typeof to avoid zero-argument method invocation on host objects (#15151)
+       var ret;
+
+       if ( typeof context.getElementsByTagName !== "undefined" ) {
+               ret = context.getElementsByTagName( tag || "*" );
+
+       } else if ( typeof context.querySelectorAll !== "undefined" ) {
+               ret = context.querySelectorAll( tag || "*" );
+
+       } else {
+               ret = [];
+       }
+
+       if ( tag === undefined || tag && nodeName( context, tag ) ) {
+               return jQuery.merge( [ context ], ret );
+       }
+
+       return ret;
+}
+
+
+// Mark scripts as having already been evaluated
+function setGlobalEval( elems, refElements ) {
+       var i = 0,
+               l = elems.length;
+
+       for ( ; i < l; i++ ) {
+               dataPriv.set(
+                       elems[ i ],
+                       "globalEval",
+                       !refElements || dataPriv.get( refElements[ i ], "globalEval" )
+               );
+       }
+}
+
+
+var rhtml = /<|&#?\w+;/;
+
+function buildFragment( elems, context, scripts, selection, ignored ) {
+       var elem, tmp, tag, wrap, attached, j,
+               fragment = context.createDocumentFragment(),
+               nodes = [],
+               i = 0,
+               l = elems.length;
+
+       for ( ; i < l; i++ ) {
+               elem = elems[ i ];
+
+               if ( elem || elem === 0 ) {
+
+                       // Add nodes directly
+                       if ( toType( elem ) === "object" ) {
+
+                               // Support: Android <=4.0 only, PhantomJS 1 only
+                               // push.apply(_, arraylike) throws on ancient WebKit
+                               jQuery.merge( nodes, elem.nodeType ? [ elem ] : elem );
+
+                       // Convert non-html into a text node
+                       } else if ( !rhtml.test( elem ) ) {
+                               nodes.push( context.createTextNode( elem ) );
+
+                       // Convert html into DOM nodes
+                       } else {
+                               tmp = tmp || fragment.appendChild( context.createElement( "div" ) );
+
+                               // Deserialize a standard representation
+                               tag = ( rtagName.exec( elem ) || [ "", "" ] )[ 1 ].toLowerCase();
+                               wrap = wrapMap[ tag ] || wrapMap._default;
+                               tmp.innerHTML = wrap[ 1 ] + jQuery.htmlPrefilter( elem ) + wrap[ 2 ];
+
+                               // Descend through wrappers to the right content
+                               j = wrap[ 0 ];
+                               while ( j-- ) {
+                                       tmp = tmp.lastChild;
+                               }
+
+                               // Support: Android <=4.0 only, PhantomJS 1 only
+                               // push.apply(_, arraylike) throws on ancient WebKit
+                               jQuery.merge( nodes, tmp.childNodes );
+
+                               // Remember the top-level container
+                               tmp = fragment.firstChild;
+
+                               // Ensure the created nodes are orphaned (#12392)
+                               tmp.textContent = "";
+                       }
+               }
+       }
+
+       // Remove wrapper from fragment
+       fragment.textContent = "";
+
+       i = 0;
+       while ( ( elem = nodes[ i++ ] ) ) {
+
+               // Skip elements already in the context collection (trac-4087)
+               if ( selection && jQuery.inArray( elem, selection ) > -1 ) {
+                       if ( ignored ) {
+                               ignored.push( elem );
+                       }
+                       continue;
+               }
+
+               attached = isAttached( elem );
+
+               // Append to fragment
+               tmp = getAll( fragment.appendChild( elem ), "script" );
+
+               // Preserve script evaluation history
+               if ( attached ) {
+                       setGlobalEval( tmp );
+               }
+
+               // Capture executables
+               if ( scripts ) {
+                       j = 0;
+                       while ( ( elem = tmp[ j++ ] ) ) {
+                               if ( rscriptType.test( elem.type || "" ) ) {
+                                       scripts.push( elem );
+                               }
+                       }
+               }
+       }
+
+       return fragment;
+}
+
+
+( function() {
+       var fragment = document.createDocumentFragment(),
+               div = fragment.appendChild( document.createElement( "div" ) ),
+               input = document.createElement( "input" );
+
+       // Support: Android 4.0 - 4.3 only
+       // Check state lost if the name is set (#11217)
+       // Support: Windows Web Apps (WWA)
+       // `name` and `type` must use .setAttribute for WWA (#14901)
+       input.setAttribute( "type", "radio" );
+       input.setAttribute( "checked", "checked" );
+       input.setAttribute( "name", "t" );
+
+       div.appendChild( input );
+
+       // Support: Android <=4.1 only
+       // Older WebKit doesn't clone checked state correctly in fragments
+       support.checkClone = div.cloneNode( true ).cloneNode( true ).lastChild.checked;
+
+       // Support: IE <=11 only
+       // Make sure textarea (and checkbox) defaultValue is properly cloned
+       div.innerHTML = "<textarea>x</textarea>";
+       support.noCloneChecked = !!div.cloneNode( true ).lastChild.defaultValue;
+} )();
+
+
+var
+       rkeyEvent = /^key/,
+       rmouseEvent = /^(?:mouse|pointer|contextmenu|drag|drop)|click/,
+       rtypenamespace = /^([^.]*)(?:\.(.+)|)/;
+
+function returnTrue() {
+       return true;
+}
+
+function returnFalse() {
+       return false;
+}
+
+// Support: IE <=9 - 11+
+// focus() and blur() are asynchronous, except when they are no-op.
+// So expect focus to be synchronous when the element is already active,
+// and blur to be synchronous when the element is not already active.
+// (focus and blur are always synchronous in other supported browsers,
+// this just defines when we can count on it).
+function expectSync( elem, type ) {
+       return ( elem === safeActiveElement() ) === ( type === "focus" );
+}
+
+// Support: IE <=9 only
+// Accessing document.activeElement can throw unexpectedly
+// https://bugs.jquery.com/ticket/13393
+function safeActiveElement() {
+       try {
+               return document.activeElement;
+       } catch ( err ) { }
+}
+
+function on( elem, types, selector, data, fn, one ) {
+       var origFn, type;
+
+       // Types can be a map of types/handlers
+       if ( typeof types === "object" ) {
+
+               // ( types-Object, selector, data )
+               if ( typeof selector !== "string" ) {
+
+                       // ( types-Object, data )
+                       data = data || selector;
+                       selector = undefined;
+               }
+               for ( type in types ) {
+                       on( elem, type, selector, data, types[ type ], one );
+               }
+               return elem;
+       }
+
+       if ( data == null && fn == null ) {
+
+               // ( types, fn )
+               fn = selector;
+               data = selector = undefined;
+       } else if ( fn == null ) {
+               if ( typeof selector === "string" ) {
+
+                       // ( types, selector, fn )
+                       fn = data;
+                       data = undefined;
+               } else {
+
+                       // ( types, data, fn )
+                       fn = data;
+                       data = selector;
+                       selector = undefined;
+               }
+       }
+       if ( fn === false ) {
+               fn = returnFalse;
+       } else if ( !fn ) {
+               return elem;
+       }
+
+       if ( one === 1 ) {
+               origFn = fn;
+               fn = function( event ) {
+
+                       // Can use an empty set, since event contains the info
+                       jQuery().off( event );
+                       return origFn.apply( this, arguments );
+               };
+
+               // Use same guid so caller can remove using origFn
+               fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ );
+       }
+       return elem.each( function() {
+               jQuery.event.add( this, types, fn, data, selector );
+       } );
+}
+
+/*
+ * Helper functions for managing events -- not part of the public interface.
+ * Props to Dean Edwards' addEvent library for many of the ideas.
+ */
+jQuery.event = {
+
+       global: {},
+
+       add: function( elem, types, handler, data, selector ) {
+
+               var handleObjIn, eventHandle, tmp,
+                       events, t, handleObj,
+                       special, handlers, type, namespaces, origType,
+                       elemData = dataPriv.get( elem );
+
+               // Don't attach events to noData or text/comment nodes (but allow plain objects)
+               if ( !elemData ) {
+                       return;
+               }
+
+               // Caller can pass in an object of custom data in lieu of the handler
+               if ( handler.handler ) {
+                       handleObjIn = handler;
+                       handler = handleObjIn.handler;
+                       selector = handleObjIn.selector;
+               }
+
+               // Ensure that invalid selectors throw exceptions at attach time
+               // Evaluate against documentElement in case elem is a non-element node (e.g., document)
+               if ( selector ) {
+                       jQuery.find.matchesSelector( documentElement, selector );
+               }
+
+               // Make sure that the handler has a unique ID, used to find/remove it later
+               if ( !handler.guid ) {
+                       handler.guid = jQuery.guid++;
+               }
+
+               // Init the element's event structure and main handler, if this is the first
+               if ( !( events = elemData.events ) ) {
+                       events = elemData.events = {};
+               }
+               if ( !( eventHandle = elemData.handle ) ) {
+                       eventHandle = elemData.handle = function( e ) {
+
+                               // Discard the second event of a jQuery.event.trigger() and
+                               // when an event is called after a page has unloaded
+                               return typeof jQuery !== "undefined" && jQuery.event.triggered !== e.type ?
+                                       jQuery.event.dispatch.apply( elem, arguments ) : undefined;
+                       };
+               }
+
+               // Handle multiple events separated by a space
+               types = ( types || "" ).match( rnothtmlwhite ) || [ "" ];
+               t = types.length;
+               while ( t-- ) {
+                       tmp = rtypenamespace.exec( types[ t ] ) || [];
+                       type = origType = tmp[ 1 ];
+                       namespaces = ( tmp[ 2 ] || "" ).split( "." ).sort();
+
+                       // There *must* be a type, no attaching namespace-only handlers
+                       if ( !type ) {
+                               continue;
+                       }
+
+                       // If event changes its type, use the special event handlers for the changed type
+                       special = jQuery.event.special[ type ] || {};
+
+                       // If selector defined, determine special event api type, otherwise given type
+                       type = ( selector ? special.delegateType : special.bindType ) || type;
+
+                       // Update special based on newly reset type
+                       special = jQuery.event.special[ type ] || {};
+
+                       // handleObj is passed to all event handlers
+                       handleObj = jQuery.extend( {
+                               type: type,
+                               origType: origType,
+                               data: data,
+                               handler: handler,
+                               guid: handler.guid,
+                               selector: selector,
+                               needsContext: selector && jQuery.expr.match.needsContext.test( selector ),
+                               namespace: namespaces.join( "." )
+                       }, handleObjIn );
+
+                       // Init the event handler queue if we're the first
+                       if ( !( handlers = events[ type ] ) ) {
+                               handlers = events[ type ] = [];
+                               handlers.delegateCount = 0;
+
+                               // Only use addEventListener if the special events handler returns false
+                               if ( !special.setup ||
+                                       special.setup.call( elem, data, namespaces, eventHandle ) === false ) {
+
+                                       if ( elem.addEventListener ) {
+                                               elem.addEventListener( type, eventHandle );
+                                       }
+                               }
+                       }
+
+                       if ( special.add ) {
+                               special.add.call( elem, handleObj );
+
+                               if ( !handleObj.handler.guid ) {
+                                       handleObj.handler.guid = handler.guid;
+                               }
+                       }
+
+                       // Add to the element's handler list, delegates in front
+                       if ( selector ) {
+                               handlers.splice( handlers.delegateCount++, 0, handleObj );
+                       } else {
+                               handlers.push( handleObj );
+                       }
+
+                       // Keep track of which events have ever been used, for event optimization
+                       jQuery.event.global[ type ] = true;
+               }
+
+       },
+
+       // Detach an event or set of events from an element
+       remove: function( elem, types, handler, selector, mappedTypes ) {
+
+               var j, origCount, tmp,
+                       events, t, handleObj,
+                       special, handlers, type, namespaces, origType,
+                       elemData = dataPriv.hasData( elem ) && dataPriv.get( elem );
+
+               if ( !elemData || !( events = elemData.events ) ) {
+                       return;
+               }
+
+               // Once for each type.namespace in types; type may be omitted
+               types = ( types || "" ).match( rnothtmlwhite ) || [ "" ];
+               t = types.length;
+               while ( t-- ) {
+                       tmp = rtypenamespace.exec( types[ t ] ) || [];
+                       type = origType = tmp[ 1 ];
+                       namespaces = ( tmp[ 2 ] || "" ).split( "." ).sort();
+
+                       // Unbind all events (on this namespace, if provided) for the element
+                       if ( !type ) {
+                               for ( type in events ) {
+                                       jQuery.event.remove( elem, type + types[ t ], handler, selector, true );
+                               }
+                               continue;
+                       }
+
+                       special = jQuery.event.special[ type ] || {};
+                       type = ( selector ? special.delegateType : special.bindType ) || type;
+                       handlers = events[ type ] || [];
+                       tmp = tmp[ 2 ] &&
+                               new RegExp( "(^|\\.)" + namespaces.join( "\\.(?:.*\\.|)" ) + "(\\.|$)" );
+
+                       // Remove matching events
+                       origCount = j = handlers.length;
+                       while ( j-- ) {
+                               handleObj = handlers[ j ];
+
+                               if ( ( mappedTypes || origType === handleObj.origType ) &&
+                                       ( !handler || handler.guid === handleObj.guid ) &&
+                                       ( !tmp || tmp.test( handleObj.namespace ) ) &&
+                                       ( !selector || selector === handleObj.selector ||
+                                               selector === "**" && handleObj.selector ) ) {
+                                       handlers.splice( j, 1 );
+
+                                       if ( handleObj.selector ) {
+                                               handlers.delegateCount--;
+                                       }
+                                       if ( special.remove ) {
+                                               special.remove.call( elem, handleObj );
+                                       }
+                               }
+                       }
+
+                       // Remove generic event handler if we removed something and no more handlers exist
+                       // (avoids potential for endless recursion during removal of special event handlers)
+                       if ( origCount && !handlers.length ) {
+                               if ( !special.teardown ||
+                                       special.teardown.call( elem, namespaces, elemData.handle ) === false ) {
+
+                                       jQuery.removeEvent( elem, type, elemData.handle );
+                               }
+
+                               delete events[ type ];
+                       }
+               }
+
+               // Remove data and the expando if it's no longer used
+               if ( jQuery.isEmptyObject( events ) ) {
+                       dataPriv.remove( elem, "handle events" );
+               }
+       },
+
+       dispatch: function( nativeEvent ) {
+
+               // Make a writable jQuery.Event from the native event object
+               var event = jQuery.event.fix( nativeEvent );
+
+               var i, j, ret, matched, handleObj, handlerQueue,
+                       args = new Array( arguments.length ),
+                       handlers = ( dataPriv.get( this, "events" ) || {} )[ event.type ] || [],
+                       special = jQuery.event.special[ event.type ] || {};
+
+               // Use the fix-ed jQuery.Event rather than the (read-only) native event
+               args[ 0 ] = event;
+
+               for ( i = 1; i < arguments.length; i++ ) {
+                       args[ i ] = arguments[ i ];
+               }
+
+               event.delegateTarget = this;
+
+               // Call the preDispatch hook for the mapped type, and let it bail if desired
+               if ( special.preDispatch && special.preDispatch.call( this, event ) === false ) {
+                       return;
+               }
+
+               // Determine handlers
+               handlerQueue = jQuery.event.handlers.call( this, event, handlers );
+
+               // Run delegates first; they may want to stop propagation beneath us
+               i = 0;
+               while ( ( matched = handlerQueue[ i++ ] ) && !event.isPropagationStopped() ) {
+                       event.currentTarget = matched.elem;
+
+                       j = 0;
+                       while ( ( handleObj = matched.handlers[ j++ ] ) &&
+                               !event.isImmediatePropagationStopped() ) {
+
+                               // If the event is namespaced, then each handler is only invoked if it is
+                               // specially universal or its namespaces are a superset of the event's.
+                               if ( !event.rnamespace || handleObj.namespace === false ||
+                                       event.rnamespace.test( handleObj.namespace ) ) {
+
+                                       event.handleObj = handleObj;
+                                       event.data = handleObj.data;
+
+                                       ret = ( ( jQuery.event.special[ handleObj.origType ] || {} ).handle ||
+                                               handleObj.handler ).apply( matched.elem, args );
+
+                                       if ( ret !== undefined ) {
+                                               if ( ( event.result = ret ) === false ) {
+                                                       event.preventDefault();
+                                                       event.stopPropagation();
+                                               }
+                                       }
+                               }
+                       }
+               }
+
+               // Call the postDispatch hook for the mapped type
+               if ( special.postDispatch ) {
+                       special.postDispatch.call( this, event );
+               }
+
+               return event.result;
+       },
+
+       handlers: function( event, handlers ) {
+               var i, handleObj, sel, matchedHandlers, matchedSelectors,
+                       handlerQueue = [],
+                       delegateCount = handlers.delegateCount,
+                       cur = event.target;
+
+               // Find delegate handlers
+               if ( delegateCount &&
+
+                       // Support: IE <=9
+                       // Black-hole SVG <use> instance trees (trac-13180)
+                       cur.nodeType &&
+
+                       // Support: Firefox <=42
+                       // Suppress spec-violating clicks indicating a non-primary pointer button (trac-3861)
+                       // https://www.w3.org/TR/DOM-Level-3-Events/#event-type-click
+                       // Support: IE 11 only
+                       // ...but not arrow key "clicks" of radio inputs, which can have `button` -1 (gh-2343)
+                       !( event.type === "click" && event.button >= 1 ) ) {
+
+                       for ( ; cur !== this; cur = cur.parentNode || this ) {
+
+                               // Don't check non-elements (#13208)
+                               // Don't process clicks on disabled elements (#6911, #8165, #11382, #11764)
+                               if ( cur.nodeType === 1 && !( event.type === "click" && cur.disabled === true ) ) {
+                                       matchedHandlers = [];
+                                       matchedSelectors = {};
+                                       for ( i = 0; i < delegateCount; i++ ) {
+                                               handleObj = handlers[ i ];
+
+                                               // Don't conflict with Object.prototype properties (#13203)
+                                               sel = handleObj.selector + " ";
+
+                                               if ( matchedSelectors[ sel ] === undefined ) {
+                                                       matchedSelectors[ sel ] = handleObj.needsContext ?
+                                                               jQuery( sel, this ).index( cur ) > -1 :
+                                                               jQuery.find( sel, this, null, [ cur ] ).length;
+                                               }
+                                               if ( matchedSelectors[ sel ] ) {
+                                                       matchedHandlers.push( handleObj );
+                                               }
+                                       }
+                                       if ( matchedHandlers.length ) {
+                                               handlerQueue.push( { elem: cur, handlers: matchedHandlers } );
+                                       }
+                               }
+                       }
+               }
+
+               // Add the remaining (directly-bound) handlers
+               cur = this;
+               if ( delegateCount < handlers.length ) {
+                       handlerQueue.push( { elem: cur, handlers: handlers.slice( delegateCount ) } );
+               }
+
+               return handlerQueue;
+       },
+
+       addProp: function( name, hook ) {
+               Object.defineProperty( jQuery.Event.prototype, name, {
+                       enumerable: true,
+                       configurable: true,
+
+                       get: isFunction( hook ) ?
+                               function() {
+                                       if ( this.originalEvent ) {
+                                                       return hook( this.originalEvent );
+                                       }
+                               } :
+                               function() {
+                                       if ( this.originalEvent ) {
+                                                       return this.originalEvent[ name ];
+                                       }
+                               },
+
+                       set: function( value ) {
+                               Object.defineProperty( this, name, {
+                                       enumerable: true,
+                                       configurable: true,
+                                       writable: true,
+                                       value: value
+                               } );
+                       }
+               } );
+       },
+
+       fix: function( originalEvent ) {
+               return originalEvent[ jQuery.expando ] ?
+                       originalEvent :
+                       new jQuery.Event( originalEvent );
+       },
+
+       special: {
+               load: {
+
+                       // Prevent triggered image.load events from bubbling to window.load
+                       noBubble: true
+               },
+               click: {
+
+                       // Utilize native event to ensure correct state for checkable inputs
+                       setup: function( data ) {
+
+                               // For mutual compressibility with _default, replace `this` access with a local var.
+                               // `|| data` is dead code meant only to preserve the variable through minification.
+                               var el = this || data;
+
+                               // Claim the first handler
+                               if ( rcheckableType.test( el.type ) &&
+                                       el.click && nodeName( el, "input" ) ) {
+
+                                       // dataPriv.set( el, "click", ... )
+                                       leverageNative( el, "click", returnTrue );
+                               }
+
+                               // Return false to allow normal processing in the caller
+                               return false;
+                       },
+                       trigger: function( data ) {
+
+                               // For mutual compressibility with _default, replace `this` access with a local var.
+                               // `|| data` is dead code meant only to preserve the variable through minification.
+                               var el = this || data;
+
+                               // Force setup before triggering a click
+                               if ( rcheckableType.test( el.type ) &&
+                                       el.click && nodeName( el, "input" ) ) {
+
+                                       leverageNative( el, "click" );
+                               }
+
+                               // Return non-false to allow normal event-path propagation
+                               return true;
+                       },
+
+                       // For cross-browser consistency, suppress native .click() on links
+                       // Also prevent it if we're currently inside a leveraged native-event stack
+                       _default: function( event ) {
+                               var target = event.target;
+                               return rcheckableType.test( target.type ) &&
+                                       target.click && nodeName( target, "input" ) &&
+                                       dataPriv.get( target, "click" ) ||
+                                       nodeName( target, "a" );
+                       }
+               },
+
+               beforeunload: {
+                       postDispatch: function( event ) {
+
+                               // Support: Firefox 20+
+                               // Firefox doesn't alert if the returnValue field is not set.
+                               if ( event.result !== undefined && event.originalEvent ) {
+                                       event.originalEvent.returnValue = event.result;
+                               }
+                       }
+               }
+       }
+};
+
+// Ensure the presence of an event listener that handles manually-triggered
+// synthetic events by interrupting progress until reinvoked in response to
+// *native* events that it fires directly, ensuring that state changes have
+// already occurred before other listeners are invoked.
+function leverageNative( el, type, expectSync ) {
+
+       // Missing expectSync indicates a trigger call, which must force setup through jQuery.event.add
+       if ( !expectSync ) {
+               if ( dataPriv.get( el, type ) === undefined ) {
+                       jQuery.event.add( el, type, returnTrue );
+               }
+               return;
+       }
+
+       // Register the controller as a special universal handler for all event namespaces
+       dataPriv.set( el, type, false );
+       jQuery.event.add( el, type, {
+               namespace: false,
+               handler: function( event ) {
+                       var notAsync, result,
+                               saved = dataPriv.get( this, type );
+
+                       if ( ( event.isTrigger & 1 ) && this[ type ] ) {
+
+                               // Interrupt processing of the outer synthetic .trigger()ed event
+                               // Saved data should be false in such cases, but might be a leftover capture object
+                               // from an async native handler (gh-4350)
+                               if ( !saved.length ) {
+
+                                       // Store arguments for use when handling the inner native event
+                                       // There will always be at least one argument (an event object), so this array
+                                       // will not be confused with a leftover capture object.
+                                       saved = slice.call( arguments );
+                                       dataPriv.set( this, type, saved );
+
+                                       // Trigger the native event and capture its result
+                                       // Support: IE <=9 - 11+
+                                       // focus() and blur() are asynchronous
+                                       notAsync = expectSync( this, type );
+                                       this[ type ]();
+                                       result = dataPriv.get( this, type );
+                                       if ( saved !== result || notAsync ) {
+                                               dataPriv.set( this, type, false );
+                                       } else {
+                                               result = {};
+                                       }
+                                       if ( saved !== result ) {
+
+                                               // Cancel the outer synthetic event
+                                               event.stopImmediatePropagation();
+                                               event.preventDefault();
+                                               return result.value;
+                                       }
+
+                               // If this is an inner synthetic event for an event with a bubbling surrogate
+                               // (focus or blur), assume that the surrogate already propagated from triggering the
+                               // native event and prevent that from happening again here.
+                               // This technically gets the ordering wrong w.r.t. to `.trigger()` (in which the
+                               // bubbling surrogate propagates *after* the non-bubbling base), but that seems
+                               // less bad than duplication.
+                               } else if ( ( jQuery.event.special[ type ] || {} ).delegateType ) {
+                                       event.stopPropagation();
+                               }
+
+                       // If this is a native event triggered above, everything is now in order
+                       // Fire an inner synthetic event with the original arguments
+                       } else if ( saved.length ) {
+
+                               // ...and capture the result
+                               dataPriv.set( this, type, {
+                                       value: jQuery.event.trigger(
+
+                                               // Support: IE <=9 - 11+
+                                               // Extend with the prototype to reset the above stopImmediatePropagation()
+                                               jQuery.extend( saved[ 0 ], jQuery.Event.prototype ),
+                                               saved.slice( 1 ),
+                                               this
+                                       )
+                               } );
+
+                               // Abort handling of the native event
+                               event.stopImmediatePropagation();
+                       }
+               }
+       } );
+}
+
+jQuery.removeEvent = function( elem, type, handle ) {
+
+       // This "if" is needed for plain objects
+       if ( elem.removeEventListener ) {
+               elem.removeEventListener( type, handle );
+       }
+};
+
+jQuery.Event = function( src, props ) {
+
+       // Allow instantiation without the 'new' keyword
+       if ( !( this instanceof jQuery.Event ) ) {
+               return new jQuery.Event( src, props );
+       }
+
+       // Event object
+       if ( src && src.type ) {
+               this.originalEvent = src;
+               this.type = src.type;
+
+               // Events bubbling up the document may have been marked as prevented
+               // by a handler lower down the tree; reflect the correct value.
+               this.isDefaultPrevented = src.defaultPrevented ||
+                               src.defaultPrevented === undefined &&
+
+                               // Support: Android <=2.3 only
+                               src.returnValue === false ?
+                       returnTrue :
+                       returnFalse;
+
+               // Create target properties
+               // Support: Safari <=6 - 7 only
+               // Target should not be a text node (#504, #13143)
+               this.target = ( src.target && src.target.nodeType === 3 ) ?
+                       src.target.parentNode :
+                       src.target;
+
+               this.currentTarget = src.currentTarget;
+               this.relatedTarget = src.relatedTarget;
+
+       // Event type
+       } else {
+               this.type = src;
+       }
+
+       // Put explicitly provided properties onto the event object
+       if ( props ) {
+               jQuery.extend( this, props );
+       }
+
+       // Create a timestamp if incoming event doesn't have one
+       this.timeStamp = src && src.timeStamp || Date.now();
+
+       // Mark it as fixed
+       this[ jQuery.expando ] = true;
+};
+
+// jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding
+// https://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html
+jQuery.Event.prototype = {
+       constructor: jQuery.Event,
+       isDefaultPrevented: returnFalse,
+       isPropagationStopped: returnFalse,
+       isImmediatePropagationStopped: returnFalse,
+       isSimulated: false,
+
+       preventDefault: function() {
+               var e = this.originalEvent;
+
+               this.isDefaultPrevented = returnTrue;
+
+               if ( e && !this.isSimulated ) {
+                       e.preventDefault();
+               }
+       },
+       stopPropagation: function() {
+               var e = this.originalEvent;
+
+               this.isPropagationStopped = returnTrue;
+
+               if ( e && !this.isSimulated ) {
+                       e.stopPropagation();
+               }
+       },
+       stopImmediatePropagation: function() {
+               var e = this.originalEvent;
+
+               this.isImmediatePropagationStopped = returnTrue;
+
+               if ( e && !this.isSimulated ) {
+                       e.stopImmediatePropagation();
+               }
+
+               this.stopPropagation();
+       }
+};
+
+// Includes all common event props including KeyEvent and MouseEvent specific props
+jQuery.each( {
+       altKey: true,
+       bubbles: true,
+       cancelable: true,
+       changedTouches: true,
+       ctrlKey: true,
+       detail: true,
+       eventPhase: true,
+       metaKey: true,
+       pageX: true,
+       pageY: true,
+       shiftKey: true,
+       view: true,
+       "char": true,
+       code: true,
+       charCode: true,
+       key: true,
+       keyCode: true,
+       button: true,
+       buttons: true,
+       clientX: true,
+       clientY: true,
+       offsetX: true,
+       offsetY: true,
+       pointerId: true,
+       pointerType: true,
+       screenX: true,
+       screenY: true,
+       targetTouches: true,
+       toElement: true,
+       touches: true,
+
+       which: function( event ) {
+               var button = event.button;
+
+               // Add which for key events
+               if ( event.which == null && rkeyEvent.test( event.type ) ) {
+                       return event.charCode != null ? event.charCode : event.keyCode;
+               }
+
+               // Add which for click: 1 === left; 2 === middle; 3 === right
+               if ( !event.which && button !== undefined && rmouseEvent.test( event.type ) ) {
+                       if ( button & 1 ) {
+                               return 1;
+                       }
+
+                       if ( button & 2 ) {
+                               return 3;
+                       }
+
+                       if ( button & 4 ) {
+                               return 2;
+                       }
+
+                       return 0;
+               }
+
+               return event.which;
+       }
+}, jQuery.event.addProp );
+
+jQuery.each( { focus: "focusin", blur: "focusout" }, function( type, delegateType ) {
+       jQuery.event.special[ type ] = {
+
+               // Utilize native event if possible so blur/focus sequence is correct
+               setup: function() {
+
+                       // Claim the first handler
+                       // dataPriv.set( this, "focus", ... )
+                       // dataPriv.set( this, "blur", ... )
+                       leverageNative( this, type, expectSync );
+
+                       // Return false to allow normal processing in the caller
+                       return false;
+               },
+               trigger: function() {
+
+                       // Force setup before trigger
+                       leverageNative( this, type );
+
+                       // Return non-false to allow normal event-path propagation
+                       return true;
+               },
+
+               delegateType: delegateType
+       };
+} );
+
+// Create mouseenter/leave events using mouseover/out and event-time checks
+// so that event delegation works in jQuery.
+// Do the same for pointerenter/pointerleave and pointerover/pointerout
+//
+// Support: Safari 7 only
+// Safari sends mouseenter too often; see:
+// https://bugs.chromium.org/p/chromium/issues/detail?id=470258
+// for the description of the bug (it existed in older Chrome versions as well).
+jQuery.each( {
+       mouseenter: "mouseover",
+       mouseleave: "mouseout",
+       pointerenter: "pointerover",
+       pointerleave: "pointerout"
+}, function( orig, fix ) {
+       jQuery.event.special[ orig ] = {
+               delegateType: fix,
+               bindType: fix,
+
+               handle: function( event ) {
+                       var ret,
+                               target = this,
+                               related = event.relatedTarget,
+                               handleObj = event.handleObj;
+
+                       // For mouseenter/leave call the handler if related is outside the target.
+                       // NB: No relatedTarget if the mouse left/entered the browser window
+                       if ( !related || ( related !== target && !jQuery.contains( target, related ) ) ) {
+                               event.type = handleObj.origType;
+                               ret = handleObj.handler.apply( this, arguments );
+                               event.type = fix;
+                       }
+                       return ret;
+               }
+       };
+} );
+
+jQuery.fn.extend( {
+
+       on: function( types, selector, data, fn ) {
+               return on( this, types, selector, data, fn );
+       },
+       one: function( types, selector, data, fn ) {
+               return on( this, types, selector, data, fn, 1 );
+       },
+       off: function( types, selector, fn ) {
+               var handleObj, type;
+               if ( types && types.preventDefault && types.handleObj ) {
+
+                       // ( event )  dispatched jQuery.Event
+                       handleObj = types.handleObj;
+                       jQuery( types.delegateTarget ).off(
+                               handleObj.namespace ?
+                                       handleObj.origType + "." + handleObj.namespace :
+                                       handleObj.origType,
+                               handleObj.selector,
+                               handleObj.handler
+                       );
+                       return this;
+               }
+               if ( typeof types === "object" ) {
+
+                       // ( types-object [, selector] )
+                       for ( type in types ) {
+                               this.off( type, selector, types[ type ] );
+                       }
+                       return this;
+               }
+               if ( selector === false || typeof selector === "function" ) {
+
+                       // ( types [, fn] )
+                       fn = selector;
+                       selector = undefined;
+               }
+               if ( fn === false ) {
+                       fn = returnFalse;
+               }
+               return this.each( function() {
+                       jQuery.event.remove( this, types, fn, selector );
+               } );
+       }
+} );
+
+
+var
+
+       /* eslint-disable max-len */
+
+       // See https://github.com/eslint/eslint/issues/3229
+       rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([a-z][^\/\0>\x20\t\r\n\f]*)[^>]*)\/>/gi,
+
+       /* eslint-enable */
+
+       // Support: IE <=10 - 11, Edge 12 - 13 only
+       // In IE/Edge using regex groups here causes severe slowdowns.
+       // See https://connect.microsoft.com/IE/feedback/details/1736512/
+       rnoInnerhtml = /<script|<style|<link/i,
+
+       // checked="checked" or checked
+       rchecked = /checked\s*(?:[^=]|=\s*.checked.)/i,
+       rcleanScript = /^\s*<!(?:\[CDATA\[|--)|(?:\]\]|--)>\s*$/g;
+
+// Prefer a tbody over its parent table for containing new rows
+function manipulationTarget( elem, content ) {
+       if ( nodeName( elem, "table" ) &&
+               nodeName( content.nodeType !== 11 ? content : content.firstChild, "tr" ) ) {
+
+               return jQuery( elem ).children( "tbody" )[ 0 ] || elem;
+       }
+
+       return elem;
+}
+
+// Replace/restore the type attribute of script elements for safe DOM manipulation
+function disableScript( elem ) {
+       elem.type = ( elem.getAttribute( "type" ) !== null ) + "/" + elem.type;
+       return elem;
+}
+function restoreScript( elem ) {
+       if ( ( elem.type || "" ).slice( 0, 5 ) === "true/" ) {
+               elem.type = elem.type.slice( 5 );
+       } else {
+               elem.removeAttribute( "type" );
+       }
+
+       return elem;
+}
+
+function cloneCopyEvent( src, dest ) {
+       var i, l, type, pdataOld, pdataCur, udataOld, udataCur, events;
+
+       if ( dest.nodeType !== 1 ) {
+               return;
+       }
+
+       // 1. Copy private data: events, handlers, etc.
+       if ( dataPriv.hasData( src ) ) {
+               pdataOld = dataPriv.access( src );
+               pdataCur = dataPriv.set( dest, pdataOld );
+               events = pdataOld.events;
+
+               if ( events ) {
+                       delete pdataCur.handle;
+                       pdataCur.events = {};
+
+                       for ( type in events ) {
+                               for ( i = 0, l = events[ type ].length; i < l; i++ ) {
+                                       jQuery.event.add( dest, type, events[ type ][ i ] );
+                               }
+                       }
+               }
+       }
+
+       // 2. Copy user data
+       if ( dataUser.hasData( src ) ) {
+               udataOld = dataUser.access( src );
+               udataCur = jQuery.extend( {}, udataOld );
+
+               dataUser.set( dest, udataCur );
+       }
+}
+
+// Fix IE bugs, see support tests
+function fixInput( src, dest ) {
+       var nodeName = dest.nodeName.toLowerCase();
+
+       // Fails to persist the checked state of a cloned checkbox or radio button.
+       if ( nodeName === "input" && rcheckableType.test( src.type ) ) {
+               dest.checked = src.checked;
+
+       // Fails to return the selected option to the default selected state when cloning options
+       } else if ( nodeName === "input" || nodeName === "textarea" ) {
+               dest.defaultValue = src.defaultValue;
+       }
+}
+
+function domManip( collection, args, callback, ignored ) {
+
+       // Flatten any nested arrays
+       args = concat.apply( [], args );
+
+       var fragment, first, scripts, hasScripts, node, doc,
+               i = 0,
+               l = collection.length,
+               iNoClone = l - 1,
+               value = args[ 0 ],
+               valueIsFunction = isFunction( value );
+
+       // We can't cloneNode fragments that contain checked, in WebKit
+       if ( valueIsFunction ||
+                       ( l > 1 && typeof value === "string" &&
+                               !support.checkClone && rchecked.test( value ) ) ) {
+               return collection.each( function( index ) {
+                       var self = collection.eq( index );
+                       if ( valueIsFunction ) {
+                               args[ 0 ] = value.call( this, index, self.html() );
+                       }
+                       domManip( self, args, callback, ignored );
+               } );
+       }
+
+       if ( l ) {
+               fragment = buildFragment( args, collection[ 0 ].ownerDocument, false, collection, ignored );
+               first = fragment.firstChild;
+
+               if ( fragment.childNodes.length === 1 ) {
+                       fragment = first;
+               }
+
+               // Require either new content or an interest in ignored elements to invoke the callback
+               if ( first || ignored ) {
+                       scripts = jQuery.map( getAll( fragment, "script" ), disableScript );
+                       hasScripts = scripts.length;
+
+                       // Use the original fragment for the last item
+                       // instead of the first because it can end up
+                       // being emptied incorrectly in certain situations (#8070).
+                       for ( ; i < l; i++ ) {
+                               node = fragment;
+
+                               if ( i !== iNoClone ) {
+                                       node = jQuery.clone( node, true, true );
+
+                                       // Keep references to cloned scripts for later restoration
+                                       if ( hasScripts ) {
+
+                                               // Support: Android <=4.0 only, PhantomJS 1 only
+                                               // push.apply(_, arraylike) throws on ancient WebKit
+                                               jQuery.merge( scripts, getAll( node, "script" ) );
+                                       }
+                               }
+
+                               callback.call( collection[ i ], node, i );
+                       }
+
+                       if ( hasScripts ) {
+                               doc = scripts[ scripts.length - 1 ].ownerDocument;
+
+                               // Reenable scripts
+                               jQuery.map( scripts, restoreScript );
+
+                               // Evaluate executable scripts on first document insertion
+                               for ( i = 0; i < hasScripts; i++ ) {
+                                       node = scripts[ i ];
+                                       if ( rscriptType.test( node.type || "" ) &&
+                                               !dataPriv.access( node, "globalEval" ) &&
+                                               jQuery.contains( doc, node ) ) {
+
+                                               if ( node.src && ( node.type || "" ).toLowerCase()  !== "module" ) {
+
+                                                       // Optional AJAX dependency, but won't run scripts if not present
+                                                       if ( jQuery._evalUrl && !node.noModule ) {
+                                                               jQuery._evalUrl( node.src, {
+                                                                       nonce: node.nonce || node.getAttribute( "nonce" )
+                                                               } );
+                                                       }
+                                               } else {
+                                                       DOMEval( node.textContent.replace( rcleanScript, "" ), node, doc );
+                                               }
+                                       }
+                               }
+                       }
+               }
+       }
+
+       return collection;
+}
+
+function remove( elem, selector, keepData ) {
+       var node,
+               nodes = selector ? jQuery.filter( selector, elem ) : elem,
+               i = 0;
+
+       for ( ; ( node = nodes[ i ] ) != null; i++ ) {
+               if ( !keepData && node.nodeType === 1 ) {
+                       jQuery.cleanData( getAll( node ) );
+               }
+
+               if ( node.parentNode ) {
+                       if ( keepData && isAttached( node ) ) {
+                               setGlobalEval( getAll( node, "script" ) );
+                       }
+                       node.parentNode.removeChild( node );
+               }
+       }
+
+       return elem;
+}
+
+jQuery.extend( {
+       htmlPrefilter: function( html ) {
+               return html.replace( rxhtmlTag, "<$1></$2>" );
+       },
+
+       clone: function( elem, dataAndEvents, deepDataAndEvents ) {
+               var i, l, srcElements, destElements,
+                       clone = elem.cloneNode( true ),
+                       inPage = isAttached( elem );
+
+               // Fix IE cloning issues
+               if ( !support.noCloneChecked && ( elem.nodeType === 1 || elem.nodeType === 11 ) &&
+                               !jQuery.isXMLDoc( elem ) ) {
+
+                       // We eschew Sizzle here for performance reasons: https://jsperf.com/getall-vs-sizzle/2
+                       destElements = getAll( clone );
+                       srcElements = getAll( elem );
+
+                       for ( i = 0, l = srcElements.length; i < l; i++ ) {
+                               fixInput( srcElements[ i ], destElements[ i ] );
+                       }
+               }
+
+               // Copy the events from the original to the clone
+               if ( dataAndEvents ) {
+                       if ( deepDataAndEvents ) {
+                               srcElements = srcElements || getAll( elem );
+                               destElements = destElements || getAll( clone );
+
+                               for ( i = 0, l = srcElements.length; i < l; i++ ) {
+                                       cloneCopyEvent( srcElements[ i ], destElements[ i ] );
+                               }
+                       } else {
+                               cloneCopyEvent( elem, clone );
+                       }
+               }
+
+               // Preserve script evaluation history
+               destElements = getAll( clone, "script" );
+               if ( destElements.length > 0 ) {
+                       setGlobalEval( destElements, !inPage && getAll( elem, "script" ) );
+               }
+
+               // Return the cloned set
+               return clone;
+       },
+
+       cleanData: function( elems ) {
+               var data, elem, type,
+                       special = jQuery.event.special,
+                       i = 0;
+
+               for ( ; ( elem = elems[ i ] ) !== undefined; i++ ) {
+                       if ( acceptData( elem ) ) {
+                               if ( ( data = elem[ dataPriv.expando ] ) ) {
+                                       if ( data.events ) {
+                                               for ( type in data.events ) {
+                                                       if ( special[ type ] ) {
+                                                               jQuery.event.remove( elem, type );
+
+                                                       // This is a shortcut to avoid jQuery.event.remove's overhead
+                                                       } else {
+                                                               jQuery.removeEvent( elem, type, data.handle );
+                                                       }
+                                               }
+                                       }
+
+                                       // Support: Chrome <=35 - 45+
+                                       // Assign undefined instead of using delete, see Data#remove
+                                       elem[ dataPriv.expando ] = undefined;
+                               }
+                               if ( elem[ dataUser.expando ] ) {
+
+                                       // Support: Chrome <=35 - 45+
+                                       // Assign undefined instead of using delete, see Data#remove
+                                       elem[ dataUser.expando ] = undefined;
+                               }
+                       }
+               }
+       }
+} );
+
+jQuery.fn.extend( {
+       detach: function( selector ) {
+               return remove( this, selector, true );
+       },
+
+       remove: function( selector ) {
+               return remove( this, selector );
+       },
+
+       text: function( value ) {
+               return access( this, function( value ) {
+                       return value === undefined ?
+                               jQuery.text( this ) :
+                               this.empty().each( function() {
+                                       if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) {
+                                               this.textContent = value;
+                                       }
+                               } );
+               }, null, value, arguments.length );
+       },
+
+       append: function() {
+               return domManip( this, arguments, function( elem ) {
+                       if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) {
+                               var target = manipulationTarget( this, elem );
+                               target.appendChild( elem );
+                       }
+               } );
+       },
+
+       prepend: function() {
+               return domManip( this, arguments, function( elem ) {
+                       if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) {
+                               var target = manipulationTarget( this, elem );
+                               target.insertBefore( elem, target.firstChild );
+                       }
+               } );
+       },
+
+       before: function() {
+               return domManip( this, arguments, function( elem ) {
+                       if ( this.parentNode ) {
+                               this.parentNode.insertBefore( elem, this );
+                       }
+               } );
+       },
+
+       after: function() {
+               return domManip( this, arguments, function( elem ) {
+                       if ( this.parentNode ) {
+                               this.parentNode.insertBefore( elem, this.nextSibling );
+                       }
+               } );
+       },
+
+       empty: function() {
+               var elem,
+                       i = 0;
+
+               for ( ; ( elem = this[ i ] ) != null; i++ ) {
+                       if ( elem.nodeType === 1 ) {
+
+                               // Prevent memory leaks
+                               jQuery.cleanData( getAll( elem, false ) );
+
+                               // Remove any remaining nodes
+                               elem.textContent = "";
+                       }
+               }
+
+               return this;
+       },
+
+       clone: function( dataAndEvents, deepDataAndEvents ) {
+               dataAndEvents = dataAndEvents == null ? false : dataAndEvents;
+               deepDataAndEvents = deepDataAndEvents == null ? dataAndEvents : deepDataAndEvents;
+
+               return this.map( function() {
+                       return jQuery.clone( this, dataAndEvents, deepDataAndEvents );
+               } );
+       },
+
+       html: function( value ) {
+               return access( this, function( value ) {
+                       var elem = this[ 0 ] || {},
+                               i = 0,
+                               l = this.length;
+
+                       if ( value === undefined && elem.nodeType === 1 ) {
+                               return elem.innerHTML;
+                       }
+
+                       // See if we can take a shortcut and just use innerHTML
+                       if ( typeof value === "string" && !rnoInnerhtml.test( value ) &&
+                               !wrapMap[ ( rtagName.exec( value ) || [ "", "" ] )[ 1 ].toLowerCase() ] ) {
+
+                               value = jQuery.htmlPrefilter( value );
+
+                               try {
+                                       for ( ; i < l; i++ ) {
+                                               elem = this[ i ] || {};
+
+                                               // Remove element nodes and prevent memory leaks
+                                               if ( elem.nodeType === 1 ) {
+                                                       jQuery.cleanData( getAll( elem, false ) );
+                                                       elem.innerHTML = value;
+                                               }
+                                       }
+
+                                       elem = 0;
+
+                               // If using innerHTML throws an exception, use the fallback method
+                               } catch ( e ) {}
+                       }
+
+                       if ( elem ) {
+                               this.empty().append( value );
+                       }
+               }, null, value, arguments.length );
+       },
+
+       replaceWith: function() {
+               var ignored = [];
+
+               // Make the changes, replacing each non-ignored context element with the new content
+               return domManip( this, arguments, function( elem ) {
+                       var parent = this.parentNode;
+
+                       if ( jQuery.inArray( this, ignored ) < 0 ) {
+                               jQuery.cleanData( getAll( this ) );
+                               if ( parent ) {
+                                       parent.replaceChild( elem, this );
+                               }
+                       }
+
+               // Force callback invocation
+               }, ignored );
+       }
+} );
+
+jQuery.each( {
+       appendTo: "append",
+       prependTo: "prepend",
+       insertBefore: "before",
+       insertAfter: "after",
+       replaceAll: "replaceWith"
+}, function( name, original ) {
+       jQuery.fn[ name ] = function( selector ) {
+               var elems,
+                       ret = [],
+                       insert = jQuery( selector ),
+                       last = insert.length - 1,
+                       i = 0;
+
+               for ( ; i <= last; i++ ) {
+                       elems = i === last ? this : this.clone( true );
+                       jQuery( insert[ i ] )[ original ]( elems );
+
+                       // Support: Android <=4.0 only, PhantomJS 1 only
+                       // .get() because push.apply(_, arraylike) throws on ancient WebKit
+                       push.apply( ret, elems.get() );
+               }
+
+               return this.pushStack( ret );
+       };
+} );
+var rnumnonpx = new RegExp( "^(" + pnum + ")(?!px)[a-z%]+$", "i" );
+
+var getStyles = function( elem ) {
+
+               // Support: IE <=11 only, Firefox <=30 (#15098, #14150)
+               // IE throws on elements created in popups
+               // FF meanwhile throws on frame elements through "defaultView.getComputedStyle"
+               var view = elem.ownerDocument.defaultView;
+
+               if ( !view || !view.opener ) {
+                       view = window;
+               }
+
+               return view.getComputedStyle( elem );
+       };
+
+var rboxStyle = new RegExp( cssExpand.join( "|" ), "i" );
+
+
+
+( function() {
+
+       // Executing both pixelPosition & boxSizingReliable tests require only one layout
+       // so they're executed at the same time to save the second computation.
+       function computeStyleTests() {
+
+               // This is a singleton, we need to execute it only once
+               if ( !div ) {
+                       return;
+               }
+
+               container.style.cssText = "position:absolute;left:-11111px;width:60px;" +
+                       "margin-top:1px;padding:0;border:0";
+               div.style.cssText =
+                       "position:relative;display:block;box-sizing:border-box;overflow:scroll;" +
+                       "margin:auto;border:1px;padding:1px;" +
+                       "width:60%;top:1%";
+               documentElement.appendChild( container ).appendChild( div );
+
+               var divStyle = window.getComputedStyle( div );
+               pixelPositionVal = divStyle.top !== "1%";
+
+               // Support: Android 4.0 - 4.3 only, Firefox <=3 - 44
+               reliableMarginLeftVal = roundPixelMeasures( divStyle.marginLeft ) === 12;
+
+               // Support: Android 4.0 - 4.3 only, Safari <=9.1 - 10.1, iOS <=7.0 - 9.3
+               // Some styles come back with percentage values, even though they shouldn't
+               div.style.right = "60%";
+               pixelBoxStylesVal = roundPixelMeasures( divStyle.right ) === 36;
+
+               // Support: IE 9 - 11 only
+               // Detect misreporting of content dimensions for box-sizing:border-box elements
+               boxSizingReliableVal = roundPixelMeasures( divStyle.width ) === 36;
+
+               // Support: IE 9 only
+               // Detect overflow:scroll screwiness (gh-3699)
+               // Support: Chrome <=64
+               // Don't get tricked when zoom affects offsetWidth (gh-4029)
+               div.style.position = "absolute";
+               scrollboxSizeVal = roundPixelMeasures( div.offsetWidth / 3 ) === 12;
+
+               documentElement.removeChild( container );
+
+               // Nullify the div so it wouldn't be stored in the memory and
+               // it will also be a sign that checks already performed
+               div = null;
+       }
+
+       function roundPixelMeasures( measure ) {
+               return Math.round( parseFloat( measure ) );
+       }
+
+       var pixelPositionVal, boxSizingReliableVal, scrollboxSizeVal, pixelBoxStylesVal,
+               reliableMarginLeftVal,
+               container = document.createElement( "div" ),
+               div = document.createElement( "div" );
+
+       // Finish early in limited (non-browser) environments
+       if ( !div.style ) {
+               return;
+       }
+
+       // Support: IE <=9 - 11 only
+       // Style of cloned element affects source element cloned (#8908)
+       div.style.backgroundClip = "content-box";
+       div.cloneNode( true ).style.backgroundClip = "";
+       support.clearCloneStyle = div.style.backgroundClip === "content-box";
+
+       jQuery.extend( support, {
+               boxSizingReliable: function() {
+                       computeStyleTests();
+                       return boxSizingReliableVal;
+               },
+               pixelBoxStyles: function() {
+                       computeStyleTests();
+                       return pixelBoxStylesVal;
+               },
+               pixelPosition: function() {
+                       computeStyleTests();
+                       return pixelPositionVal;
+               },
+               reliableMarginLeft: function() {
+                       computeStyleTests();
+                       return reliableMarginLeftVal;
+               },
+               scrollboxSize: function() {
+                       computeStyleTests();
+                       return scrollboxSizeVal;
+               }
+       } );
+} )();
+
+
+function curCSS( elem, name, computed ) {
+       var width, minWidth, maxWidth, ret,
+
+               // Support: Firefox 51+
+               // Retrieving style before computed somehow
+               // fixes an issue with getting wrong values
+               // on detached elements
+               style = elem.style;
+
+       computed = computed || getStyles( elem );
+
+       // getPropertyValue is needed for:
+       //   .css('filter') (IE 9 only, #12537)
+       //   .css('--customProperty) (#3144)
+       if ( computed ) {
+               ret = computed.getPropertyValue( name ) || computed[ name ];
+
+               if ( ret === "" && !isAttached( elem ) ) {
+                       ret = jQuery.style( elem, name );
+               }
+
+               // A tribute to the "awesome hack by Dean Edwards"
+               // Android Browser returns percentage for some values,
+               // but width seems to be reliably pixels.
+               // This is against the CSSOM draft spec:
+               // https://drafts.csswg.org/cssom/#resolved-values
+               if ( !support.pixelBoxStyles() && rnumnonpx.test( ret ) && rboxStyle.test( name ) ) {
+
+                       // Remember the original values
+                       width = style.width;
+                       minWidth = style.minWidth;
+                       maxWidth = style.maxWidth;
+
+                       // Put in the new values to get a computed value out
+                       style.minWidth = style.maxWidth = style.width = ret;
+                       ret = computed.width;
+
+                       // Revert the changed values
+                       style.width = width;
+                       style.minWidth = minWidth;
+                       style.maxWidth = maxWidth;
+               }
+       }
+
+       return ret !== undefined ?
+
+               // Support: IE <=9 - 11 only
+               // IE returns zIndex value as an integer.
+               ret + "" :
+               ret;
+}
+
+
+function addGetHookIf( conditionFn, hookFn ) {
+
+       // Define the hook, we'll check on the first run if it's really needed.
+       return {
+               get: function() {
+                       if ( conditionFn() ) {
+
+                               // Hook not needed (or it's not possible to use it due
+                               // to missing dependency), remove it.
+                               delete this.get;
+                               return;
+                       }
+
+                       // Hook needed; redefine it so that the support test is not executed again.
+                       return ( this.get = hookFn ).apply( this, arguments );
+               }
+       };
+}
+
+
+var cssPrefixes = [ "Webkit", "Moz", "ms" ],
+       emptyStyle = document.createElement( "div" ).style,
+       vendorProps = {};
+
+// Return a vendor-prefixed property or undefined
+function vendorPropName( name ) {
+
+       // Check for vendor prefixed names
+       var capName = name[ 0 ].toUpperCase() + name.slice( 1 ),
+               i = cssPrefixes.length;
+
+       while ( i-- ) {
+               name = cssPrefixes[ i ] + capName;
+               if ( name in emptyStyle ) {
+                       return name;
+               }
+       }
+}
+
+// Return a potentially-mapped jQuery.cssProps or vendor prefixed property
+function finalPropName( name ) {
+       var final = jQuery.cssProps[ name ] || vendorProps[ name ];
+
+       if ( final ) {
+               return final;
+       }
+       if ( name in emptyStyle ) {
+               return name;
+       }
+       return vendorProps[ name ] = vendorPropName( name ) || name;
+}
+
+
+var
+
+       // Swappable if display is none or starts with table
+       // except "table", "table-cell", or "table-caption"
+       // See here for display values: https://developer.mozilla.org/en-US/docs/CSS/display
+       rdisplayswap = /^(none|table(?!-c[ea]).+)/,
+       rcustomProp = /^--/,
+       cssShow = { position: "absolute", visibility: "hidden", display: "block" },
+       cssNormalTransform = {
+               letterSpacing: "0",
+               fontWeight: "400"
+       };
+
+function setPositiveNumber( elem, value, subtract ) {
+
+       // Any relative (+/-) values have already been
+       // normalized at this point
+       var matches = rcssNum.exec( value );
+       return matches ?
+
+               // Guard against undefined "subtract", e.g., when used as in cssHooks
+               Math.max( 0, matches[ 2 ] - ( subtract || 0 ) ) + ( matches[ 3 ] || "px" ) :
+               value;
+}
+
+function boxModelAdjustment( elem, dimension, box, isBorderBox, styles, computedVal ) {
+       var i = dimension === "width" ? 1 : 0,
+               extra = 0,
+               delta = 0;
+
+       // Adjustment may not be necessary
+       if ( box === ( isBorderBox ? "border" : "content" ) ) {
+               return 0;
+       }
+
+       for ( ; i < 4; i += 2 ) {
+
+               // Both box models exclude margin
+               if ( box === "margin" ) {
+                       delta += jQuery.css( elem, box + cssExpand[ i ], true, styles );
+               }
+
+               // If we get here with a content-box, we're seeking "padding" or "border" or "margin"
+               if ( !isBorderBox ) {
+
+                       // Add padding
+                       delta += jQuery.css( elem, "padding" + cssExpand[ i ], true, styles );
+
+                       // For "border" or "margin", add border
+                       if ( box !== "padding" ) {
+                               delta += jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles );
+
+                       // But still keep track of it otherwise
+                       } else {
+                               extra += jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles );
+                       }
+
+               // If we get here with a border-box (content + padding + border), we're seeking "content" or
+               // "padding" or "margin"
+               } else {
+
+                       // For "content", subtract padding
+                       if ( box === "content" ) {
+                               delta -= jQuery.css( elem, "padding" + cssExpand[ i ], true, styles );
+                       }
+
+                       // For "content" or "padding", subtract border
+                       if ( box !== "margin" ) {
+                               delta -= jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles );
+                       }
+               }
+       }
+
+       // Account for positive content-box scroll gutter when requested by providing computedVal
+       if ( !isBorderBox && computedVal >= 0 ) {
+
+               // offsetWidth/offsetHeight is a rounded sum of content, padding, scroll gutter, and border
+               // Assuming integer scroll gutter, subtract the rest and round down
+               delta += Math.max( 0, Math.ceil(
+                       elem[ "offset" + dimension[ 0 ].toUpperCase() + dimension.slice( 1 ) ] -
+                       computedVal -
+                       delta -
+                       extra -
+                       0.5
+
+               // If offsetWidth/offsetHeight is unknown, then we can't determine content-box scroll gutter
+               // Use an explicit zero to avoid NaN (gh-3964)
+               ) ) || 0;
+       }
+
+       return delta;
+}
+
+function getWidthOrHeight( elem, dimension, extra ) {
+
+       // Start with computed style
+       var styles = getStyles( elem ),
+
+               // To avoid forcing a reflow, only fetch boxSizing if we need it (gh-4322).
+               // Fake content-box until we know it's needed to know the true value.
+               boxSizingNeeded = !support.boxSizingReliable() || extra,
+               isBorderBox = boxSizingNeeded &&
+                       jQuery.css( elem, "boxSizing", false, styles ) === "border-box",
+               valueIsBorderBox = isBorderBox,
+
+               val = curCSS( elem, dimension, styles ),
+               offsetProp = "offset" + dimension[ 0 ].toUpperCase() + dimension.slice( 1 );
+
+       // Support: Firefox <=54
+       // Return a confounding non-pixel value or feign ignorance, as appropriate.
+       if ( rnumnonpx.test( val ) ) {
+               if ( !extra ) {
+                       return val;
+               }
+               val = "auto";
+       }
+
+
+       // Fall back to offsetWidth/offsetHeight when value is "auto"
+       // This happens for inline elements with no explicit setting (gh-3571)
+       // Support: Android <=4.1 - 4.3 only
+       // Also use offsetWidth/offsetHeight for misreported inline dimensions (gh-3602)
+       // Support: IE 9-11 only
+       // Also use offsetWidth/offsetHeight for when box sizing is unreliable
+       // We use getClientRects() to check for hidden/disconnected.
+       // In those cases, the computed value can be trusted to be border-box
+       if ( ( !support.boxSizingReliable() && isBorderBox ||
+               val === "auto" ||
+               !parseFloat( val ) && jQuery.css( elem, "display", false, styles ) === "inline" ) &&
+               elem.getClientRects().length ) {
+
+               isBorderBox = jQuery.css( elem, "boxSizing", false, styles ) === "border-box";
+
+               // Where available, offsetWidth/offsetHeight approximate border box dimensions.
+               // Where not available (e.g., SVG), assume unreliable box-sizing and interpret the
+               // retrieved value as a content box dimension.
+               valueIsBorderBox = offsetProp in elem;
+               if ( valueIsBorderBox ) {
+                       val = elem[ offsetProp ];
+               }
+       }
+
+       // Normalize "" and auto
+       val = parseFloat( val ) || 0;
+
+       // Adjust for the element's box model
+       return ( val +
+               boxModelAdjustment(
+                       elem,
+                       dimension,
+                       extra || ( isBorderBox ? "border" : "content" ),
+                       valueIsBorderBox,
+                       styles,
+
+                       // Provide the current computed size to request scroll gutter calculation (gh-3589)
+                       val
+               )
+       ) + "px";
+}
+
+jQuery.extend( {
+
+       // Add in style property hooks for overriding the default
+       // behavior of getting and setting a style property
+       cssHooks: {
+               opacity: {
+                       get: function( elem, computed ) {
+                               if ( computed ) {
+
+                                       // We should always get a number back from opacity
+                                       var ret = curCSS( elem, "opacity" );
+                                       return ret === "" ? "1" : ret;
+                               }
+                       }
+               }
+       },
+
+       // Don't automatically add "px" to these possibly-unitless properties
+       cssNumber: {
+               "animationIterationCount": true,
+               "columnCount": true,
+               "fillOpacity": true,
+               "flexGrow": true,
+               "flexShrink": true,
+               "fontWeight": true,
+               "gridArea": true,
+               "gridColumn": true,
+               "gridColumnEnd": true,
+               "gridColumnStart": true,
+               "gridRow": true,
+               "gridRowEnd": true,
+               "gridRowStart": true,
+               "lineHeight": true,
+               "opacity": true,
+               "order": true,
+               "orphans": true,
+               "widows": true,
+               "zIndex": true,
+               "zoom": true
+       },
+
+       // Add in properties whose names you wish to fix before
+       // setting or getting the value
+       cssProps: {},
+
+       // Get and set the style property on a DOM Node
+       style: function( elem, name, value, extra ) {
+
+               // Don't set styles on text and comment nodes
+               if ( !elem || elem.nodeType === 3 || elem.nodeType === 8 || !elem.style ) {
+                       return;
+               }
+
+               // Make sure that we're working with the right name
+               var ret, type, hooks,
+                       origName = camelCase( name ),
+                       isCustomProp = rcustomProp.test( name ),
+                       style = elem.style;
+
+               // Make sure that we're working with the right name. We don't
+               // want to query the value if it is a CSS custom property
+               // since they are user-defined.
+               if ( !isCustomProp ) {
+                       name = finalPropName( origName );
+               }
+
+               // Gets hook for the prefixed version, then unprefixed version
+               hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ];
+
+               // Check if we're setting a value
+               if ( value !== undefined ) {
+                       type = typeof value;
+
+                       // Convert "+=" or "-=" to relative numbers (#7345)
+                       if ( type === "string" && ( ret = rcssNum.exec( value ) ) && ret[ 1 ] ) {
+                               value = adjustCSS( elem, name, ret );
+
+                               // Fixes bug #9237
+                               type = "number";
+                       }
+
+                       // Make sure that null and NaN values aren't set (#7116)
+                       if ( value == null || value !== value ) {
+                               return;
+                       }
+
+                       // If a number was passed in, add the unit (except for certain CSS properties)
+                       // The isCustomProp check can be removed in jQuery 4.0 when we only auto-append
+                       // "px" to a few hardcoded values.
+                       if ( type === "number" && !isCustomProp ) {
+                               value += ret && ret[ 3 ] || ( jQuery.cssNumber[ origName ] ? "" : "px" );
+                       }
+
+                       // background-* props affect original clone's values
+                       if ( !support.clearCloneStyle && value === "" && name.indexOf( "background" ) === 0 ) {
+                               style[ name ] = "inherit";
+                       }
+
+                       // If a hook was provided, use that value, otherwise just set the specified value
+                       if ( !hooks || !( "set" in hooks ) ||
+                               ( value = hooks.set( elem, value, extra ) ) !== undefined ) {
+
+                               if ( isCustomProp ) {
+                                       style.setProperty( name, value );
+                               } else {
+                                       style[ name ] = value;
+                               }
+                       }
+
+               } else {
+
+                       // If a hook was provided get the non-computed value from there
+                       if ( hooks && "get" in hooks &&
+                               ( ret = hooks.get( elem, false, extra ) ) !== undefined ) {
+
+                               return ret;
+                       }
+
+                       // Otherwise just get the value from the style object
+                       return style[ name ];
+               }
+       },
+
+       css: function( elem, name, extra, styles ) {
+               var val, num, hooks,
+                       origName = camelCase( name ),
+                       isCustomProp = rcustomProp.test( name );
+
+               // Make sure that we're working with the right name. We don't
+               // want to modify the value if it is a CSS custom property
+               // since they are user-defined.
+               if ( !isCustomProp ) {
+                       name = finalPropName( origName );
+               }
+
+               // Try prefixed name followed by the unprefixed name
+               hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ];
+
+               // If a hook was provided get the computed value from there
+               if ( hooks && "get" in hooks ) {
+                       val = hooks.get( elem, true, extra );
+               }
+
+               // Otherwise, if a way to get the computed value exists, use that
+               if ( val === undefined ) {
+                       val = curCSS( elem, name, styles );
+               }
+
+               // Convert "normal" to computed value
+               if ( val === "normal" && name in cssNormalTransform ) {
+                       val = cssNormalTransform[ name ];
+               }
+
+               // Make numeric if forced or a qualifier was provided and val looks numeric
+               if ( extra === "" || extra ) {
+                       num = parseFloat( val );
+                       return extra === true || isFinite( num ) ? num || 0 : val;
+               }
+
+               return val;
+       }
+} );
+
+jQuery.each( [ "height", "width" ], function( i, dimension ) {
+       jQuery.cssHooks[ dimension ] = {
+               get: function( elem, computed, extra ) {
+                       if ( computed ) {
+
+                               // Certain elements can have dimension info if we invisibly show them
+                               // but it must have a current display style that would benefit
+                               return rdisplayswap.test( jQuery.css( elem, "display" ) ) &&
+
+                                       // Support: Safari 8+
+                                       // Table columns in Safari have non-zero offsetWidth & zero
+                                       // getBoundingClientRect().width unless display is changed.
+                                       // Support: IE <=11 only
+                                       // Running getBoundingClientRect on a disconnected node
+                                       // in IE throws an error.
+                                       ( !elem.getClientRects().length || !elem.getBoundingClientRect().width ) ?
+                                               swap( elem, cssShow, function() {
+                                                       return getWidthOrHeight( elem, dimension, extra );
+                                               } ) :
+                                               getWidthOrHeight( elem, dimension, extra );
+                       }
+               },
+
+               set: function( elem, value, extra ) {
+                       var matches,
+                               styles = getStyles( elem ),
+
+                               // Only read styles.position if the test has a chance to fail
+                               // to avoid forcing a reflow.
+                               scrollboxSizeBuggy = !support.scrollboxSize() &&
+                                       styles.position === "absolute",
+
+                               // To avoid forcing a reflow, only fetch boxSizing if we need it (gh-3991)
+                               boxSizingNeeded = scrollboxSizeBuggy || extra,
+                               isBorderBox = boxSizingNeeded &&
+                                       jQuery.css( elem, "boxSizing", false, styles ) === "border-box",
+                               subtract = extra ?
+                                       boxModelAdjustment(
+                                               elem,
+                                               dimension,
+                                               extra,
+                                               isBorderBox,
+                                               styles
+                                       ) :
+                                       0;
+
+                       // Account for unreliable border-box dimensions by comparing offset* to computed and
+                       // faking a content-box to get border and padding (gh-3699)
+                       if ( isBorderBox && scrollboxSizeBuggy ) {
+                               subtract -= Math.ceil(
+                                       elem[ "offset" + dimension[ 0 ].toUpperCase() + dimension.slice( 1 ) ] -
+                                       parseFloat( styles[ dimension ] ) -
+                                       boxModelAdjustment( elem, dimension, "border", false, styles ) -
+                                       0.5
+                               );
+                       }
+
+                       // Convert to pixels if value adjustment is needed
+                       if ( subtract && ( matches = rcssNum.exec( value ) ) &&
+                               ( matches[ 3 ] || "px" ) !== "px" ) {
+
+                               elem.style[ dimension ] = value;
+                               value = jQuery.css( elem, dimension );
+                       }
+
+                       return setPositiveNumber( elem, value, subtract );
+               }
+       };
+} );
+
+jQuery.cssHooks.marginLeft = addGetHookIf( support.reliableMarginLeft,
+       function( elem, computed ) {
+               if ( computed ) {
+                       return ( parseFloat( curCSS( elem, "marginLeft" ) ) ||
+                               elem.getBoundingClientRect().left -
+                                       swap( elem, { marginLeft: 0 }, function() {
+                                               return elem.getBoundingClientRect().left;
+                                       } )
+                               ) + "px";
+               }
+       }
+);
+
+// These hooks are used by animate to expand properties
+jQuery.each( {
+       margin: "",
+       padding: "",
+       border: "Width"
+}, function( prefix, suffix ) {
+       jQuery.cssHooks[ prefix + suffix ] = {
+               expand: function( value ) {
+                       var i = 0,
+                               expanded = {},
+
+                               // Assumes a single number if not a string
+                               parts = typeof value === "string" ? value.split( " " ) : [ value ];
+
+                       for ( ; i < 4; i++ ) {
+                               expanded[ prefix + cssExpand[ i ] + suffix ] =
+                                       parts[ i ] || parts[ i - 2 ] || parts[ 0 ];
+                       }
+
+                       return expanded;
+               }
+       };
+
+       if ( prefix !== "margin" ) {
+               jQuery.cssHooks[ prefix + suffix ].set = setPositiveNumber;
+       }
+} );
+
+jQuery.fn.extend( {
+       css: function( name, value ) {
+               return access( this, function( elem, name, value ) {
+                       var styles, len,
+                               map = {},
+                               i = 0;
+
+                       if ( Array.isArray( name ) ) {
+                               styles = getStyles( elem );
+                               len = name.length;
+
+                               for ( ; i < len; i++ ) {
+                                       map[ name[ i ] ] = jQuery.css( elem, name[ i ], false, styles );
+                               }
+
+                               return map;
+                       }
+
+                       return value !== undefined ?
+                               jQuery.style( elem, name, value ) :
+                               jQuery.css( elem, name );
+               }, name, value, arguments.length > 1 );
+       }
+} );
+
+
+function Tween( elem, options, prop, end, easing ) {
+       return new Tween.prototype.init( elem, options, prop, end, easing );
+}
+jQuery.Tween = Tween;
+
+Tween.prototype = {
+       constructor: Tween,
+       init: function( elem, options, prop, end, easing, unit ) {
+               this.elem = elem;
+               this.prop = prop;
+               this.easing = easing || jQuery.easing._default;
+               this.options = options;
+               this.start = this.now = this.cur();
+               this.end = end;
+               this.unit = unit || ( jQuery.cssNumber[ prop ] ? "" : "px" );
+       },
+       cur: function() {
+               var hooks = Tween.propHooks[ this.prop ];
+
+               return hooks && hooks.get ?
+                       hooks.get( this ) :
+                       Tween.propHooks._default.get( this );
+       },
+       run: function( percent ) {
+               var eased,
+                       hooks = Tween.propHooks[ this.prop ];
+
+               if ( this.options.duration ) {
+                       this.pos = eased = jQuery.easing[ this.easing ](
+                               percent, this.options.duration * percent, 0, 1, this.options.duration
+                       );
+               } else {
+                       this.pos = eased = percent;
+               }
+               this.now = ( this.end - this.start ) * eased + this.start;
+
+               if ( this.options.step ) {
+                       this.options.step.call( this.elem, this.now, this );
+               }
+
+               if ( hooks && hooks.set ) {
+                       hooks.set( this );
+               } else {
+                       Tween.propHooks._default.set( this );
+               }
+               return this;
+       }
+};
+
+Tween.prototype.init.prototype = Tween.prototype;
+
+Tween.propHooks = {
+       _default: {
+               get: function( tween ) {
+                       var result;
+
+                       // Use a property on the element directly when it is not a DOM element,
+                       // or when there is no matching style property that exists.
+                       if ( tween.elem.nodeType !== 1 ||
+                               tween.elem[ tween.prop ] != null && tween.elem.style[ tween.prop ] == null ) {
+                               return tween.elem[ tween.prop ];
+                       }
+
+                       // Passing an empty string as a 3rd parameter to .css will automatically
+                       // attempt a parseFloat and fallback to a string if the parse fails.
+                       // Simple values such as "10px" are parsed to Float;
+                       // complex values such as "rotate(1rad)" are returned as-is.
+                       result = jQuery.css( tween.elem, tween.prop, "" );
+
+                       // Empty strings, null, undefined and "auto" are converted to 0.
+                       return !result || result === "auto" ? 0 : result;
+               },
+               set: function( tween ) {
+
+                       // Use step hook for back compat.
+                       // Use cssHook if its there.
+                       // Use .style if available and use plain properties where available.
+                       if ( jQuery.fx.step[ tween.prop ] ) {
+                               jQuery.fx.step[ tween.prop ]( tween );
+                       } else if ( tween.elem.nodeType === 1 && (
+                                       jQuery.cssHooks[ tween.prop ] ||
+                                       tween.elem.style[ finalPropName( tween.prop ) ] != null ) ) {
+                               jQuery.style( tween.elem, tween.prop, tween.now + tween.unit );
+                       } else {
+                               tween.elem[ tween.prop ] = tween.now;
+                       }
+               }
+       }
+};
+
+// Support: IE <=9 only
+// Panic based approach to setting things on disconnected nodes
+Tween.propHooks.scrollTop = Tween.propHooks.scrollLeft = {
+       set: function( tween ) {
+               if ( tween.elem.nodeType && tween.elem.parentNode ) {
+                       tween.elem[ tween.prop ] = tween.now;
+               }
+       }
+};
+
+jQuery.easing = {
+       linear: function( p ) {
+               return p;
+       },
+       swing: function( p ) {
+               return 0.5 - Math.cos( p * Math.PI ) / 2;
+       },
+       _default: "swing"
+};
+
+jQuery.fx = Tween.prototype.init;
+
+// Back compat <1.8 extension point
+jQuery.fx.step = {};
+
+
+
+
+var
+       fxNow, inProgress,
+       rfxtypes = /^(?:toggle|show|hide)$/,
+       rrun = /queueHooks$/;
+
+function schedule() {
+       if ( inProgress ) {
+               if ( document.hidden === false && window.requestAnimationFrame ) {
+                       window.requestAnimationFrame( schedule );
+               } else {
+                       window.setTimeout( schedule, jQuery.fx.interval );
+               }
+
+               jQuery.fx.tick();
+       }
+}
+
+// Animations created synchronously will run synchronously
+function createFxNow() {
+       window.setTimeout( function() {
+               fxNow = undefined;
+       } );
+       return ( fxNow = Date.now() );
+}
+
+// Generate parameters to create a standard animation
+function genFx( type, includeWidth ) {
+       var which,
+               i = 0,
+               attrs = { height: type };
+
+       // If we include width, step value is 1 to do all cssExpand values,
+       // otherwise step value is 2 to skip over Left and Right
+       includeWidth = includeWidth ? 1 : 0;
+       for ( ; i < 4; i += 2 - includeWidth ) {
+               which = cssExpand[ i ];
+               attrs[ "margin" + which ] = attrs[ "padding" + which ] = type;
+       }
+
+       if ( includeWidth ) {
+               attrs.opacity = attrs.width = type;
+       }
+
+       return attrs;
+}
+
+function createTween( value, prop, animation ) {
+       var tween,
+               collection = ( Animation.tweeners[ prop ] || [] ).concat( Animation.tweeners[ "*" ] ),
+               index = 0,
+               length = collection.length;
+       for ( ; index < length; index++ ) {
+               if ( ( tween = collection[ index ].call( animation, prop, value ) ) ) {
+
+                       // We're done with this property
+                       return tween;
+               }
+       }
+}
+
+function defaultPrefilter( elem, props, opts ) {
+       var prop, value, toggle, hooks, oldfire, propTween, restoreDisplay, display,
+               isBox = "width" in props || "height" in props,
+               anim = this,
+               orig = {},
+               style = elem.style,
+               hidden = elem.nodeType && isHiddenWithinTree( elem ),
+               dataShow = dataPriv.get( elem, "fxshow" );
+
+       // Queue-skipping animations hijack the fx hooks
+       if ( !opts.queue ) {
+               hooks = jQuery._queueHooks( elem, "fx" );
+               if ( hooks.unqueued == null ) {
+                       hooks.unqueued = 0;
+                       oldfire = hooks.empty.fire;
+                       hooks.empty.fire = function() {
+                               if ( !hooks.unqueued ) {
+                                       oldfire();
+                               }
+                       };
+               }
+               hooks.unqueued++;
+
+               anim.always( function() {
+
+                       // Ensure the complete handler is called before this completes
+                       anim.always( function() {
+                               hooks.unqueued--;
+                               if ( !jQuery.queue( elem, "fx" ).length ) {
+                                       hooks.empty.fire();
+                               }
+                       } );
+               } );
+       }
+
+       // Detect show/hide animations
+       for ( prop in props ) {
+               value = props[ prop ];
+               if ( rfxtypes.test( value ) ) {
+                       delete props[ prop ];
+                       toggle = toggle || value === "toggle";
+                       if ( value === ( hidden ? "hide" : "show" ) ) {
+
+                               // Pretend to be hidden if this is a "show" and
+                               // there is still data from a stopped show/hide
+                               if ( value === "show" && dataShow && dataShow[ prop ] !== undefined ) {
+                                       hidden = true;
+
+                               // Ignore all other no-op show/hide data
+                               } else {
+                                       continue;
+                               }
+                       }
+                       orig[ prop ] = dataShow && dataShow[ prop ] || jQuery.style( elem, prop );
+               }
+       }
+
+       // Bail out if this is a no-op like .hide().hide()
+       propTween = !jQuery.isEmptyObject( props );
+       if ( !propTween && jQuery.isEmptyObject( orig ) ) {
+               return;
+       }
+
+       // Restrict "overflow" and "display" styles during box animations
+       if ( isBox && elem.nodeType === 1 ) {
+
+               // Support: IE <=9 - 11, Edge 12 - 15
+               // Record all 3 overflow attributes because IE does not infer the shorthand
+               // from identically-valued overflowX and overflowY and Edge just mirrors
+               // the overflowX value there.
+               opts.overflow = [ style.overflow, style.overflowX, style.overflowY ];
+
+               // Identify a display type, preferring old show/hide data over the CSS cascade
+               restoreDisplay = dataShow && dataShow.display;
+               if ( restoreDisplay == null ) {
+                       restoreDisplay = dataPriv.get( elem, "display" );
+               }
+               display = jQuery.css( elem, "display" );
+               if ( display === "none" ) {
+                       if ( restoreDisplay ) {
+                               display = restoreDisplay;
+                       } else {
+
+                               // Get nonempty value(s) by temporarily forcing visibility
+                               showHide( [ elem ], true );
+                               restoreDisplay = elem.style.display || restoreDisplay;
+                               display = jQuery.css( elem, "display" );
+                               showHide( [ elem ] );
+                       }
+               }
+
+               // Animate inline elements as inline-block
+               if ( display === "inline" || display === "inline-block" && restoreDisplay != null ) {
+                       if ( jQuery.css( elem, "float" ) === "none" ) {
+
+                               // Restore the original display value at the end of pure show/hide animations
+                               if ( !propTween ) {
+                                       anim.done( function() {
+                                               style.display = restoreDisplay;
+                                       } );
+                                       if ( restoreDisplay == null ) {
+                                               display = style.display;
+                                               restoreDisplay = display === "none" ? "" : display;
+                                       }
+                               }
+                               style.display = "inline-block";
+                       }
+               }
+       }
+
+       if ( opts.overflow ) {
+               style.overflow = "hidden";
+               anim.always( function() {
+                       style.overflow = opts.overflow[ 0 ];
+                       style.overflowX = opts.overflow[ 1 ];
+                       style.overflowY = opts.overflow[ 2 ];
+               } );
+       }
+
+       // Implement show/hide animations
+       propTween = false;
+       for ( prop in orig ) {
+
+               // General show/hide setup for this element animation
+               if ( !propTween ) {
+                       if ( dataShow ) {
+                               if ( "hidden" in dataShow ) {
+                                       hidden = dataShow.hidden;
+                               }
+                       } else {
+                               dataShow = dataPriv.access( elem, "fxshow", { display: restoreDisplay } );
+                       }
+
+                       // Store hidden/visible for toggle so `.stop().toggle()` "reverses"
+                       if ( toggle ) {
+                               dataShow.hidden = !hidden;
+                       }
+
+                       // Show elements before animating them
+                       if ( hidden ) {
+                               showHide( [ elem ], true );
+                       }
+
+                       /* eslint-disable no-loop-func */
+
+                       anim.done( function() {
+
+                       /* eslint-enable no-loop-func */
+
+                               // The final step of a "hide" animation is actually hiding the element
+                               if ( !hidden ) {
+                                       showHide( [ elem ] );
+                               }
+                               dataPriv.remove( elem, "fxshow" );
+                               for ( prop in orig ) {
+                                       jQuery.style( elem, prop, orig[ prop ] );
+                               }
+                       } );
+               }
+
+               // Per-property setup
+               propTween = createTween( hidden ? dataShow[ prop ] : 0, prop, anim );
+               if ( !( prop in dataShow ) ) {
+                       dataShow[ prop ] = propTween.start;
+                       if ( hidden ) {
+                               propTween.end = propTween.start;
+                               propTween.start = 0;
+                       }
+               }
+       }
+}
+
+function propFilter( props, specialEasing ) {
+       var index, name, easing, value, hooks;
+
+       // camelCase, specialEasing and expand cssHook pass
+       for ( index in props ) {
+               name = camelCase( index );
+               easing = specialEasing[ name ];
+               value = props[ index ];
+               if ( Array.isArray( value ) ) {
+                       easing = value[ 1 ];
+                       value = props[ index ] = value[ 0 ];
+               }
+
+               if ( index !== name ) {
+                       props[ name ] = value;
+                       delete props[ index ];
+               }
+
+               hooks = jQuery.cssHooks[ name ];
+               if ( hooks && "expand" in hooks ) {
+                       value = hooks.expand( value );
+                       delete props[ name ];
+
+                       // Not quite $.extend, this won't overwrite existing keys.
+                       // Reusing 'index' because we have the correct "name"
+                       for ( index in value ) {
+                               if ( !( index in props ) ) {
+                                       props[ index ] = value[ index ];
+                                       specialEasing[ index ] = easing;
+                               }
+                       }
+               } else {
+                       specialEasing[ name ] = easing;
+               }
+       }
+}
+
+function Animation( elem, properties, options ) {
+       var result,
+               stopped,
+               index = 0,
+               length = Animation.prefilters.length,
+               deferred = jQuery.Deferred().always( function() {
+
+                       // Don't match elem in the :animated selector
+                       delete tick.elem;
+               } ),
+               tick = function() {
+                       if ( stopped ) {
+                               return false;
+                       }
+                       var currentTime = fxNow || createFxNow(),
+                               remaining = Math.max( 0, animation.startTime + animation.duration - currentTime ),
+
+                               // Support: Android 2.3 only
+                               // Archaic crash bug won't allow us to use `1 - ( 0.5 || 0 )` (#12497)
+                               temp = remaining / animation.duration || 0,
+                               percent = 1 - temp,
+                               index = 0,
+                               length = animation.tweens.length;
+
+                       for ( ; index < length; index++ ) {
+                               animation.tweens[ index ].run( percent );
+                       }
+
+                       deferred.notifyWith( elem, [ animation, percent, remaining ] );
+
+                       // If there's more to do, yield
+                       if ( percent < 1 && length ) {
+                               return remaining;
+                       }
+
+                       // If this was an empty animation, synthesize a final progress notification
+                       if ( !length ) {
+                               deferred.notifyWith( elem, [ animation, 1, 0 ] );
+                       }
+
+                       // Resolve the animation and report its conclusion
+                       deferred.resolveWith( elem, [ animation ] );
+                       return false;
+               },
+               animation = deferred.promise( {
+                       elem: elem,
+                       props: jQuery.extend( {}, properties ),
+                       opts: jQuery.extend( true, {
+                               specialEasing: {},
+                               easing: jQuery.easing._default
+                       }, options ),
+                       originalProperties: properties,
+                       originalOptions: options,
+                       startTime: fxNow || createFxNow(),
+                       duration: options.duration,
+                       tweens: [],
+                       createTween: function( prop, end ) {
+                               var tween = jQuery.Tween( elem, animation.opts, prop, end,
+                                               animation.opts.specialEasing[ prop ] || animation.opts.easing );
+                               animation.tweens.push( tween );
+                               return tween;
+                       },
+                       stop: function( gotoEnd ) {
+                               var index = 0,
+
+                                       // If we are going to the end, we want to run all the tweens
+                                       // otherwise we skip this part
+                                       length = gotoEnd ? animation.tweens.length : 0;
+                               if ( stopped ) {
+                                       return this;
+                               }
+                               stopped = true;
+                               for ( ; index < length; index++ ) {
+                                       animation.tweens[ index ].run( 1 );
+                               }
+
+                               // Resolve when we played the last frame; otherwise, reject
+                               if ( gotoEnd ) {
+                                       deferred.notifyWith( elem, [ animation, 1, 0 ] );
+                                       deferred.resolveWith( elem, [ animation, gotoEnd ] );
+                               } else {
+                                       deferred.rejectWith( elem, [ animation, gotoEnd ] );
+                               }
+                               return this;
+                       }
+               } ),
+               props = animation.props;
+
+       propFilter( props, animation.opts.specialEasing );
+
+       for ( ; index < length; index++ ) {
+               result = Animation.prefilters[ index ].call( animation, elem, props, animation.opts );
+               if ( result ) {
+                       if ( isFunction( result.stop ) ) {
+                               jQuery._queueHooks( animation.elem, animation.opts.queue ).stop =
+                                       result.stop.bind( result );
+                       }
+                       return result;
+               }
+       }
+
+       jQuery.map( props, createTween, animation );
+
+       if ( isFunction( animation.opts.start ) ) {
+               animation.opts.start.call( elem, animation );
+       }
+
+       // Attach callbacks from options
+       animation
+               .progress( animation.opts.progress )
+               .done( animation.opts.done, animation.opts.complete )
+               .fail( animation.opts.fail )
+               .always( animation.opts.always );
+
+       jQuery.fx.timer(
+               jQuery.extend( tick, {
+                       elem: elem,
+                       anim: animation,
+                       queue: animation.opts.queue
+               } )
+       );
+
+       return animation;
+}
+
+jQuery.Animation = jQuery.extend( Animation, {
+
+       tweeners: {
+               "*": [ function( prop, value ) {
+                       var tween = this.createTween( prop, value );
+                       adjustCSS( tween.elem, prop, rcssNum.exec( value ), tween );
+                       return tween;
+               } ]
+       },
+
+       tweener: function( props, callback ) {
+               if ( isFunction( props ) ) {
+                       callback = props;
+                       props = [ "*" ];
+               } else {
+                       props = props.match( rnothtmlwhite );
+               }
+
+               var prop,
+                       index = 0,
+                       length = props.length;
+
+               for ( ; index < length; index++ ) {
+                       prop = props[ index ];
+                       Animation.tweeners[ prop ] = Animation.tweeners[ prop ] || [];
+                       Animation.tweeners[ prop ].unshift( callback );
+               }
+       },
+
+       prefilters: [ defaultPrefilter ],
+
+       prefilter: function( callback, prepend ) {
+               if ( prepend ) {
+                       Animation.prefilters.unshift( callback );
+               } else {
+                       Animation.prefilters.push( callback );
+               }
+       }
+} );
+
+jQuery.speed = function( speed, easing, fn ) {
+       var opt = speed && typeof speed === "object" ? jQuery.extend( {}, speed ) : {
+               complete: fn || !fn && easing ||
+                       isFunction( speed ) && speed,
+               duration: speed,
+               easing: fn && easing || easing && !isFunction( easing ) && easing
+       };
+
+       // Go to the end state if fx are off
+       if ( jQuery.fx.off ) {
+               opt.duration = 0;
+
+       } else {
+               if ( typeof opt.duration !== "number" ) {
+                       if ( opt.duration in jQuery.fx.speeds ) {
+                               opt.duration = jQuery.fx.speeds[ opt.duration ];
+
+                       } else {
+                               opt.duration = jQuery.fx.speeds._default;
+                       }
+               }
+       }
+
+       // Normalize opt.queue - true/undefined/null -> "fx"
+       if ( opt.queue == null || opt.queue === true ) {
+               opt.queue = "fx";
+       }
+
+       // Queueing
+       opt.old = opt.complete;
+
+       opt.complete = function() {
+               if ( isFunction( opt.old ) ) {
+                       opt.old.call( this );
+               }
+
+               if ( opt.queue ) {
+                       jQuery.dequeue( this, opt.queue );
+               }
+       };
+
+       return opt;
+};
+
+jQuery.fn.extend( {
+       fadeTo: function( speed, to, easing, callback ) {
+
+               // Show any hidden elements after setting opacity to 0
+               return this.filter( isHiddenWithinTree ).css( "opacity", 0 ).show()
+
+                       // Animate to the value specified
+                       .end().animate( { opacity: to }, speed, easing, callback );
+       },
+       animate: function( prop, speed, easing, callback ) {
+               var empty = jQuery.isEmptyObject( prop ),
+                       optall = jQuery.speed( speed, easing, callback ),
+                       doAnimation = function() {
+
+                               // Operate on a copy of prop so per-property easing won't be lost
+                               var anim = Animation( this, jQuery.extend( {}, prop ), optall );
+
+                               // Empty animations, or finishing resolves immediately
+                               if ( empty || dataPriv.get( this, "finish" ) ) {
+                                       anim.stop( true );
+                               }
+                       };
+                       doAnimation.finish = doAnimation;
+
+               return empty || optall.queue === false ?
+                       this.each( doAnimation ) :
+                       this.queue( optall.queue, doAnimation );
+       },
+       stop: function( type, clearQueue, gotoEnd ) {
+               var stopQueue = function( hooks ) {
+                       var stop = hooks.stop;
+                       delete hooks.stop;
+                       stop( gotoEnd );
+               };
+
+               if ( typeof type !== "string" ) {
+                       gotoEnd = clearQueue;
+                       clearQueue = type;
+                       type = undefined;
+               }
+               if ( clearQueue && type !== false ) {
+                       this.queue( type || "fx", [] );
+               }
+
+               return this.each( function() {
+                       var dequeue = true,
+                               index = type != null && type + "queueHooks",
+                               timers = jQuery.timers,
+                               data = dataPriv.get( this );
+
+                       if ( index ) {
+                               if ( data[ index ] && data[ index ].stop ) {
+                                       stopQueue( data[ index ] );
+                               }
+                       } else {
+                               for ( index in data ) {
+                                       if ( data[ index ] && data[ index ].stop && rrun.test( index ) ) {
+                                               stopQueue( data[ index ] );
+                                       }
+                               }
+                       }
+
+                       for ( index = timers.length; index--; ) {
+                               if ( timers[ index ].elem === this &&
+                                       ( type == null || timers[ index ].queue === type ) ) {
+
+                                       timers[ index ].anim.stop( gotoEnd );
+                                       dequeue = false;
+                                       timers.splice( index, 1 );
+                               }
+                       }
+
+                       // Start the next in the queue if the last step wasn't forced.
+                       // Timers currently will call their complete callbacks, which
+                       // will dequeue but only if they were gotoEnd.
+                       if ( dequeue || !gotoEnd ) {
+                               jQuery.dequeue( this, type );
+                       }
+               } );
+       },
+       finish: function( type ) {
+               if ( type !== false ) {
+                       type = type || "fx";
+               }
+               return this.each( function() {
+                       var index,
+                               data = dataPriv.get( this ),
+                               queue = data[ type + "queue" ],
+                               hooks = data[ type + "queueHooks" ],
+                               timers = jQuery.timers,
+                               length = queue ? queue.length : 0;
+
+                       // Enable finishing flag on private data
+                       data.finish = true;
+
+                       // Empty the queue first
+                       jQuery.queue( this, type, [] );
+
+                       if ( hooks && hooks.stop ) {
+                               hooks.stop.call( this, true );
+                       }
+
+                       // Look for any active animations, and finish them
+                       for ( index = timers.length; index--; ) {
+                               if ( timers[ index ].elem === this && timers[ index ].queue === type ) {
+                                       timers[ index ].anim.stop( true );
+                                       timers.splice( index, 1 );
+                               }
+                       }
+
+                       // Look for any animations in the old queue and finish them
+                       for ( index = 0; index < length; index++ ) {
+                               if ( queue[ index ] && queue[ index ].finish ) {
+                                       queue[ index ].finish.call( this );
+                               }
+                       }
+
+                       // Turn off finishing flag
+                       delete data.finish;
+               } );
+       }
+} );
+
+jQuery.each( [ "toggle", "show", "hide" ], function( i, name ) {
+       var cssFn = jQuery.fn[ name ];
+       jQuery.fn[ name ] = function( speed, easing, callback ) {
+               return speed == null || typeof speed === "boolean" ?
+                       cssFn.apply( this, arguments ) :
+                       this.animate( genFx( name, true ), speed, easing, callback );
+       };
+} );
+
+// Generate shortcuts for custom animations
+jQuery.each( {
+       slideDown: genFx( "show" ),
+       slideUp: genFx( "hide" ),
+       slideToggle: genFx( "toggle" ),
+       fadeIn: { opacity: "show" },
+       fadeOut: { opacity: "hide" },
+       fadeToggle: { opacity: "toggle" }
+}, function( name, props ) {
+       jQuery.fn[ name ] = function( speed, easing, callback ) {
+               return this.animate( props, speed, easing, callback );
+       };
+} );
+
+jQuery.timers = [];
+jQuery.fx.tick = function() {
+       var timer,
+               i = 0,
+               timers = jQuery.timers;
+
+       fxNow = Date.now();
+
+       for ( ; i < timers.length; i++ ) {
+               timer = timers[ i ];
+
+               // Run the timer and safely remove it when done (allowing for external removal)
+               if ( !timer() && timers[ i ] === timer ) {
+                       timers.splice( i--, 1 );
+               }
+       }
+
+       if ( !timers.length ) {
+               jQuery.fx.stop();
+       }
+       fxNow = undefined;
+};
+
+jQuery.fx.timer = function( timer ) {
+       jQuery.timers.push( timer );
+       jQuery.fx.start();
+};
+
+jQuery.fx.interval = 13;
+jQuery.fx.start = function() {
+       if ( inProgress ) {
+               return;
+       }
+
+       inProgress = true;
+       schedule();
+};
+
+jQuery.fx.stop = function() {
+       inProgress = null;
+};
+
+jQuery.fx.speeds = {
+       slow: 600,
+       fast: 200,
+
+       // Default speed
+       _default: 400
+};
+
+
+// Based off of the plugin by Clint Helfers, with permission.
+// https://web.archive.org/web/20100324014747/http://blindsignals.com/index.php/2009/07/jquery-delay/
+jQuery.fn.delay = function( time, type ) {
+       time = jQuery.fx ? jQuery.fx.speeds[ time ] || time : time;
+       type = type || "fx";
+
+       return this.queue( type, function( next, hooks ) {
+               var timeout = window.setTimeout( next, time );
+               hooks.stop = function() {
+                       window.clearTimeout( timeout );
+               };
+       } );
+};
+
+
+( function() {
+       var input = document.createElement( "input" ),
+               select = document.createElement( "select" ),
+               opt = select.appendChild( document.createElement( "option" ) );
+
+       input.type = "checkbox";
+
+       // Support: Android <=4.3 only
+       // Default value for a checkbox should be "on"
+       support.checkOn = input.value !== "";
+
+       // Support: IE <=11 only
+       // Must access selectedIndex to make default options select
+       support.optSelected = opt.selected;
+
+       // Support: IE <=11 only
+       // An input loses its value after becoming a radio
+       input = document.createElement( "input" );
+       input.value = "t";
+       input.type = "radio";
+       support.radioValue = input.value === "t";
+} )();
+
+
+var boolHook,
+       attrHandle = jQuery.expr.attrHandle;
+
+jQuery.fn.extend( {
+       attr: function( name, value ) {
+               return access( this, jQuery.attr, name, value, arguments.length > 1 );
+       },
+
+       removeAttr: function( name ) {
+               return this.each( function() {
+                       jQuery.removeAttr( this, name );
+               } );
+       }
+} );
+
+jQuery.extend( {
+       attr: function( elem, name, value ) {
+               var ret, hooks,
+                       nType = elem.nodeType;
+
+               // Don't get/set attributes on text, comment and attribute nodes
+               if ( nType === 3 || nType === 8 || nType === 2 ) {
+                       return;
+               }
+
+               // Fallback to prop when attributes are not supported
+               if ( typeof elem.getAttribute === "undefined" ) {
+                       return jQuery.prop( elem, name, value );
+               }
+
+               // Attribute hooks are determined by the lowercase version
+               // Grab necessary hook if one is defined
+               if ( nType !== 1 || !jQuery.isXMLDoc( elem ) ) {
+                       hooks = jQuery.attrHooks[ name.toLowerCase() ] ||
+                               ( jQuery.expr.match.bool.test( name ) ? boolHook : undefined );
+               }
+
+               if ( value !== undefined ) {
+                       if ( value === null ) {
+                               jQuery.removeAttr( elem, name );
+                               return;
+                       }
+
+                       if ( hooks && "set" in hooks &&
+                               ( ret = hooks.set( elem, value, name ) ) !== undefined ) {
+                               return ret;
+                       }
+
+                       elem.setAttribute( name, value + "" );
+                       return value;
+               }
+
+               if ( hooks && "get" in hooks && ( ret = hooks.get( elem, name ) ) !== null ) {
+                       return ret;
+               }
+
+               ret = jQuery.find.attr( elem, name );
+
+               // Non-existent attributes return null, we normalize to undefined
+               return ret == null ? undefined : ret;
+       },
+
+       attrHooks: {
+               type: {
+                       set: function( elem, value ) {
+                               if ( !support.radioValue && value === "radio" &&
+                                       nodeName( elem, "input" ) ) {
+                                       var val = elem.value;
+                                       elem.setAttribute( "type", value );
+                                       if ( val ) {
+                                               elem.value = val;
+                                       }
+                                       return value;
+                               }
+                       }
+               }
+       },
+
+       removeAttr: function( elem, value ) {
+               var name,
+                       i = 0,
+
+                       // Attribute names can contain non-HTML whitespace characters
+                       // https://html.spec.whatwg.org/multipage/syntax.html#attributes-2
+                       attrNames = value && value.match( rnothtmlwhite );
+
+               if ( attrNames && elem.nodeType === 1 ) {
+                       while ( ( name = attrNames[ i++ ] ) ) {
+                               elem.removeAttribute( name );
+                       }
+               }
+       }
+} );
+
+// Hooks for boolean attributes
+boolHook = {
+       set: function( elem, value, name ) {
+               if ( value === false ) {
+
+                       // Remove boolean attributes when set to false
+                       jQuery.removeAttr( elem, name );
+               } else {
+                       elem.setAttribute( name, name );
+               }
+               return name;
+       }
+};
+
+jQuery.each( jQuery.expr.match.bool.source.match( /\w+/g ), function( i, name ) {
+       var getter = attrHandle[ name ] || jQuery.find.attr;
+
+       attrHandle[ name ] = function( elem, name, isXML ) {
+               var ret, handle,
+                       lowercaseName = name.toLowerCase();
+
+               if ( !isXML ) {
+
+                       // Avoid an infinite loop by temporarily removing this function from the getter
+                       handle = attrHandle[ lowercaseName ];
+                       attrHandle[ lowercaseName ] = ret;
+                       ret = getter( elem, name, isXML ) != null ?
+                               lowercaseName :
+                               null;
+                       attrHandle[ lowercaseName ] = handle;
+               }
+               return ret;
+       };
+} );
+
+
+
+
+var rfocusable = /^(?:input|select|textarea|button)$/i,
+       rclickable = /^(?:a|area)$/i;
+
+jQuery.fn.extend( {
+       prop: function( name, value ) {
+               return access( this, jQuery.prop, name, value, arguments.length > 1 );
+       },
+
+       removeProp: function( name ) {
+               return this.each( function() {
+                       delete this[ jQuery.propFix[ name ] || name ];
+               } );
+       }
+} );
+
+jQuery.extend( {
+       prop: function( elem, name, value ) {
+               var ret, hooks,
+                       nType = elem.nodeType;
+
+               // Don't get/set properties on text, comment and attribute nodes
+               if ( nType === 3 || nType === 8 || nType === 2 ) {
+                       return;
+               }
+
+               if ( nType !== 1 || !jQuery.isXMLDoc( elem ) ) {
+
+                       // Fix name and attach hooks
+                       name = jQuery.propFix[ name ] || name;
+                       hooks = jQuery.propHooks[ name ];
+               }
+
+               if ( value !== undefined ) {
+                       if ( hooks && "set" in hooks &&
+                               ( ret = hooks.set( elem, value, name ) ) !== undefined ) {
+                               return ret;
+                       }
+
+                       return ( elem[ name ] = value );
+               }
+
+               if ( hooks && "get" in hooks && ( ret = hooks.get( elem, name ) ) !== null ) {
+                       return ret;
+               }
+
+               return elem[ name ];
+       },
+
+       propHooks: {
+               tabIndex: {
+                       get: function( elem ) {
+
+                               // Support: IE <=9 - 11 only
+                               // elem.tabIndex doesn't always return the
+                               // correct value when it hasn't been explicitly set
+                               // https://web.archive.org/web/20141116233347/http://fluidproject.org/blog/2008/01/09/getting-setting-and-removing-tabindex-values-with-javascript/
+                               // Use proper attribute retrieval(#12072)
+                               var tabindex = jQuery.find.attr( elem, "tabindex" );
+
+                               if ( tabindex ) {
+                                       return parseInt( tabindex, 10 );
+                               }
+
+                               if (
+                                       rfocusable.test( elem.nodeName ) ||
+                                       rclickable.test( elem.nodeName ) &&
+                                       elem.href
+                               ) {
+                                       return 0;
+                               }
+
+                               return -1;
+                       }
+               }
+       },
+
+       propFix: {
+               "for": "htmlFor",
+               "class": "className"
+       }
+} );
+
+// Support: IE <=11 only
+// Accessing the selectedIndex property
+// forces the browser to respect setting selected
+// on the option
+// The getter ensures a default option is selected
+// when in an optgroup
+// eslint rule "no-unused-expressions" is disabled for this code
+// since it considers such accessions noop
+if ( !support.optSelected ) {
+       jQuery.propHooks.selected = {
+               get: function( elem ) {
+
+                       /* eslint no-unused-expressions: "off" */
+
+                       var parent = elem.parentNode;
+                       if ( parent && parent.parentNode ) {
+                               parent.parentNode.selectedIndex;
+                       }
+                       return null;
+               },
+               set: function( elem ) {
+
+                       /* eslint no-unused-expressions: "off" */
+
+                       var parent = elem.parentNode;
+                       if ( parent ) {
+                               parent.selectedIndex;
+
+                               if ( parent.parentNode ) {
+                                       parent.parentNode.selectedIndex;
+                               }
+                       }
+               }
+       };
+}
+
+jQuery.each( [
+       "tabIndex",
+       "readOnly",
+       "maxLength",
+       "cellSpacing",
+       "cellPadding",
+       "rowSpan",
+       "colSpan",
+       "useMap",
+       "frameBorder",
+       "contentEditable"
+], function() {
+       jQuery.propFix[ this.toLowerCase() ] = this;
+} );
+
+
+
+
+       // Strip and collapse whitespace according to HTML spec
+       // https://infra.spec.whatwg.org/#strip-and-collapse-ascii-whitespace
+       function stripAndCollapse( value ) {
+               var tokens = value.match( rnothtmlwhite ) || [];
+               return tokens.join( " " );
+       }
+
+
+function getClass( elem ) {
+       return elem.getAttribute && elem.getAttribute( "class" ) || "";
+}
+
+function classesToArray( value ) {
+       if ( Array.isArray( value ) ) {
+               return value;
+       }
+       if ( typeof value === "string" ) {
+               return value.match( rnothtmlwhite ) || [];
+       }
+       return [];
+}
+
+jQuery.fn.extend( {
+       addClass: function( value ) {
+               var classes, elem, cur, curValue, clazz, j, finalValue,
+                       i = 0;
+
+               if ( isFunction( value ) ) {
+                       return this.each( function( j ) {
+                               jQuery( this ).addClass( value.call( this, j, getClass( this ) ) );
+                       } );
+               }
+
+               classes = classesToArray( value );
+
+               if ( classes.length ) {
+                       while ( ( elem = this[ i++ ] ) ) {
+                               curValue = getClass( elem );
+                               cur = elem.nodeType === 1 && ( " " + stripAndCollapse( curValue ) + " " );
+
+                               if ( cur ) {
+                                       j = 0;
+                                       while ( ( clazz = classes[ j++ ] ) ) {
+                                               if ( cur.indexOf( " " + clazz + " " ) < 0 ) {
+                                                       cur += clazz + " ";
+                                               }
+                                       }
+
+                                       // Only assign if different to avoid unneeded rendering.
+                                       finalValue = stripAndCollapse( cur );
+                                       if ( curValue !== finalValue ) {
+                                               elem.setAttribute( "class", finalValue );
+                                       }
+                               }
+                       }
+               }
+
+               return this;
+       },
+
+       removeClass: function( value ) {
+               var classes, elem, cur, curValue, clazz, j, finalValue,
+                       i = 0;
+
+               if ( isFunction( value ) ) {
+                       return this.each( function( j ) {
+                               jQuery( this ).removeClass( value.call( this, j, getClass( this ) ) );
+                       } );
+               }
+
+               if ( !arguments.length ) {
+                       return this.attr( "class", "" );
+               }
+
+               classes = classesToArray( value );
+
+               if ( classes.length ) {
+                       while ( ( elem = this[ i++ ] ) ) {
+                               curValue = getClass( elem );
+
+                               // This expression is here for better compressibility (see addClass)
+                               cur = elem.nodeType === 1 && ( " " + stripAndCollapse( curValue ) + " " );
+
+                               if ( cur ) {
+                                       j = 0;
+                                       while ( ( clazz = classes[ j++ ] ) ) {
+
+                                               // Remove *all* instances
+                                               while ( cur.indexOf( " " + clazz + " " ) > -1 ) {
+                                                       cur = cur.replace( " " + clazz + " ", " " );
+                                               }
+                                       }
+
+                                       // Only assign if different to avoid unneeded rendering.
+                                       finalValue = stripAndCollapse( cur );
+                                       if ( curValue !== finalValue ) {
+                                               elem.setAttribute( "class", finalValue );
+                                       }
+                               }
+                       }
+               }
+
+               return this;
+       },
+
+       toggleClass: function( value, stateVal ) {
+               var type = typeof value,
+                       isValidValue = type === "string" || Array.isArray( value );
+
+               if ( typeof stateVal === "boolean" && isValidValue ) {
+                       return stateVal ? this.addClass( value ) : this.removeClass( value );
+               }
+
+               if ( isFunction( value ) ) {
+                       return this.each( function( i ) {
+                               jQuery( this ).toggleClass(
+                                       value.call( this, i, getClass( this ), stateVal ),
+                                       stateVal
+                               );
+                       } );
+               }
+
+               return this.each( function() {
+                       var className, i, self, classNames;
+
+                       if ( isValidValue ) {
+
+                               // Toggle individual class names
+                               i = 0;
+                               self = jQuery( this );
+                               classNames = classesToArray( value );
+
+                               while ( ( className = classNames[ i++ ] ) ) {
+
+                                       // Check each className given, space separated list
+                                       if ( self.hasClass( className ) ) {
+                                               self.removeClass( className );
+                                       } else {
+                                               self.addClass( className );
+                                       }
+                               }
+
+                       // Toggle whole class name
+                       } else if ( value === undefined || type === "boolean" ) {
+                               className = getClass( this );
+                               if ( className ) {
+
+                                       // Store className if set
+                                       dataPriv.set( this, "__className__", className );
+                               }
+
+                               // If the element has a class name or if we're passed `false`,
+                               // then remove the whole classname (if there was one, the above saved it).
+                               // Otherwise bring back whatever was previously saved (if anything),
+                               // falling back to the empty string if nothing was stored.
+                               if ( this.setAttribute ) {
+                                       this.setAttribute( "class",
+                                               className || value === false ?
+                                               "" :
+                                               dataPriv.get( this, "__className__" ) || ""
+                                       );
+                               }
+                       }
+               } );
+       },
+
+       hasClass: function( selector ) {
+               var className, elem,
+                       i = 0;
+
+               className = " " + selector + " ";
+               while ( ( elem = this[ i++ ] ) ) {
+                       if ( elem.nodeType === 1 &&
+                               ( " " + stripAndCollapse( getClass( elem ) ) + " " ).indexOf( className ) > -1 ) {
+                                       return true;
+                       }
+               }
+
+               return false;
+       }
+} );
+
+
+
+
+var rreturn = /\r/g;
+
+jQuery.fn.extend( {
+       val: function( value ) {
+               var hooks, ret, valueIsFunction,
+                       elem = this[ 0 ];
+
+               if ( !arguments.length ) {
+                       if ( elem ) {
+                               hooks = jQuery.valHooks[ elem.type ] ||
+                                       jQuery.valHooks[ elem.nodeName.toLowerCase() ];
+
+                               if ( hooks &&
+                                       "get" in hooks &&
+                                       ( ret = hooks.get( elem, "value" ) ) !== undefined
+                               ) {
+                                       return ret;
+                               }
+
+                               ret = elem.value;
+
+                               // Handle most common string cases
+                               if ( typeof ret === "string" ) {
+                                       return ret.replace( rreturn, "" );
+                               }
+
+                               // Handle cases where value is null/undef or number
+                               return ret == null ? "" : ret;
+                       }
+
+                       return;
+               }
+
+               valueIsFunction = isFunction( value );
+
+               return this.each( function( i ) {
+                       var val;
+
+                       if ( this.nodeType !== 1 ) {
+                               return;
+                       }
+
+                       if ( valueIsFunction ) {
+                               val = value.call( this, i, jQuery( this ).val() );
+                       } else {
+                               val = value;
+                       }
+
+                       // Treat null/undefined as ""; convert numbers to string
+                       if ( val == null ) {
+                               val = "";
+
+                       } else if ( typeof val === "number" ) {
+                               val += "";
+
+                       } else if ( Array.isArray( val ) ) {
+                               val = jQuery.map( val, function( value ) {
+                                       return value == null ? "" : value + "";
+                               } );
+                       }
+
+                       hooks = jQuery.valHooks[ this.type ] || jQuery.valHooks[ this.nodeName.toLowerCase() ];
+
+                       // If set returns undefined, fall back to normal setting
+                       if ( !hooks || !( "set" in hooks ) || hooks.set( this, val, "value" ) === undefined ) {
+                               this.value = val;
+                       }
+               } );
+       }
+} );
+
+jQuery.extend( {
+       valHooks: {
+               option: {
+                       get: function( elem ) {
+
+                               var val = jQuery.find.attr( elem, "value" );
+                               return val != null ?
+                                       val :
+
+                                       // Support: IE <=10 - 11 only
+                                       // option.text throws exceptions (#14686, #14858)
+                                       // Strip and collapse whitespace
+                                       // https://html.spec.whatwg.org/#strip-and-collapse-whitespace
+                                       stripAndCollapse( jQuery.text( elem ) );
+                       }
+               },
+               select: {
+                       get: function( elem ) {
+                               var value, option, i,
+                                       options = elem.options,
+                                       index = elem.selectedIndex,
+                                       one = elem.type === "select-one",
+                                       values = one ? null : [],
+                                       max = one ? index + 1 : options.length;
+
+                               if ( index < 0 ) {
+                                       i = max;
+
+                               } else {
+                                       i = one ? index : 0;
+                               }
+
+                               // Loop through all the selected options
+                               for ( ; i < max; i++ ) {
+                                       option = options[ i ];
+
+                                       // Support: IE <=9 only
+                                       // IE8-9 doesn't update selected after form reset (#2551)
+                                       if ( ( option.selected || i === index ) &&
+
+                                                       // Don't return options that are disabled or in a disabled optgroup
+                                                       !option.disabled &&
+                                                       ( !option.parentNode.disabled ||
+                                                               !nodeName( option.parentNode, "optgroup" ) ) ) {
+
+                                               // Get the specific value for the option
+                                               value = jQuery( option ).val();
+
+                                               // We don't need an array for one selects
+                                               if ( one ) {
+                                                       return value;
+                                               }
+
+                                               // Multi-Selects return an array
+                                               values.push( value );
+                                       }
+                               }
+
+                               return values;
+                       },
+
+                       set: function( elem, value ) {
+                               var optionSet, option,
+                                       options = elem.options,
+                                       values = jQuery.makeArray( value ),
+                                       i = options.length;
+
+                               while ( i-- ) {
+                                       option = options[ i ];
+
+                                       /* eslint-disable no-cond-assign */
+
+                                       if ( option.selected =
+                                               jQuery.inArray( jQuery.valHooks.option.get( option ), values ) > -1
+                                       ) {
+                                               optionSet = true;
+                                       }
+
+                                       /* eslint-enable no-cond-assign */
+                               }
+
+                               // Force browsers to behave consistently when non-matching value is set
+                               if ( !optionSet ) {
+                                       elem.selectedIndex = -1;
+                               }
+                               return values;
+                       }
+               }
+       }
+} );
+
+// Radios and checkboxes getter/setter
+jQuery.each( [ "radio", "checkbox" ], function() {
+       jQuery.valHooks[ this ] = {
+               set: function( elem, value ) {
+                       if ( Array.isArray( value ) ) {
+                               return ( elem.checked = jQuery.inArray( jQuery( elem ).val(), value ) > -1 );
+                       }
+               }
+       };
+       if ( !support.checkOn ) {
+               jQuery.valHooks[ this ].get = function( elem ) {
+                       return elem.getAttribute( "value" ) === null ? "on" : elem.value;
+               };
+       }
+} );
+
+
+
+
+// Return jQuery for attributes-only inclusion
+
+
+support.focusin = "onfocusin" in window;
+
+
+var rfocusMorph = /^(?:focusinfocus|focusoutblur)$/,
+       stopPropagationCallback = function( e ) {
+               e.stopPropagation();
+       };
+
+jQuery.extend( jQuery.event, {
+
+       trigger: function( event, data, elem, onlyHandlers ) {
+
+               var i, cur, tmp, bubbleType, ontype, handle, special, lastElement,
+                       eventPath = [ elem || document ],
+                       type = hasOwn.call( event, "type" ) ? event.type : event,
+                       namespaces = hasOwn.call( event, "namespace" ) ? event.namespace.split( "." ) : [];
+
+               cur = lastElement = tmp = elem = elem || document;
+
+               // Don't do events on text and comment nodes
+               if ( elem.nodeType === 3 || elem.nodeType === 8 ) {
+                       return;
+               }
+
+               // focus/blur morphs to focusin/out; ensure we're not firing them right now
+               if ( rfocusMorph.test( type + jQuery.event.triggered ) ) {
+                       return;
+               }
+
+               if ( type.indexOf( "." ) > -1 ) {
+
+                       // Namespaced trigger; create a regexp to match event type in handle()
+                       namespaces = type.split( "." );
+                       type = namespaces.shift();
+                       namespaces.sort();
+               }
+               ontype = type.indexOf( ":" ) < 0 && "on" + type;
+
+               // Caller can pass in a jQuery.Event object, Object, or just an event type string
+               event = event[ jQuery.expando ] ?
+                       event :
+                       new jQuery.Event( type, typeof event === "object" && event );
+
+               // Trigger bitmask: & 1 for native handlers; & 2 for jQuery (always true)
+               event.isTrigger = onlyHandlers ? 2 : 3;
+               event.namespace = namespaces.join( "." );
+               event.rnamespace = event.namespace ?
+                       new RegExp( "(^|\\.)" + namespaces.join( "\\.(?:.*\\.|)" ) + "(\\.|$)" ) :
+                       null;
+
+               // Clean up the event in case it is being reused
+               event.result = undefined;
+               if ( !event.target ) {
+                       event.target = elem;
+               }
+
+               // Clone any incoming data and prepend the event, creating the handler arg list
+               data = data == null ?
+                       [ event ] :
+                       jQuery.makeArray( data, [ event ] );
+
+               // Allow special events to draw outside the lines
+               special = jQuery.event.special[ type ] || {};
+               if ( !onlyHandlers && special.trigger && special.trigger.apply( elem, data ) === false ) {
+                       return;
+               }
+
+               // Determine event propagation path in advance, per W3C events spec (#9951)
+               // Bubble up to document, then to window; watch for a global ownerDocument var (#9724)
+               if ( !onlyHandlers && !special.noBubble && !isWindow( elem ) ) {
+
+                       bubbleType = special.delegateType || type;
+                       if ( !rfocusMorph.test( bubbleType + type ) ) {
+                               cur = cur.parentNode;
+                       }
+                       for ( ; cur; cur = cur.parentNode ) {
+                               eventPath.push( cur );
+                               tmp = cur;
+                       }
+
+                       // Only add window if we got to document (e.g., not plain obj or detached DOM)
+                       if ( tmp === ( elem.ownerDocument || document ) ) {
+                               eventPath.push( tmp.defaultView || tmp.parentWindow || window );
+                       }
+               }
+
+               // Fire handlers on the event path
+               i = 0;
+               while ( ( cur = eventPath[ i++ ] ) && !event.isPropagationStopped() ) {
+                       lastElement = cur;
+                       event.type = i > 1 ?
+                               bubbleType :
+                               special.bindType || type;
+
+                       // jQuery handler
+                       handle = ( dataPriv.get( cur, "events" ) || {} )[ event.type ] &&
+                               dataPriv.get( cur, "handle" );
+                       if ( handle ) {
+                               handle.apply( cur, data );
+                       }
+
+                       // Native handler
+                       handle = ontype && cur[ ontype ];
+                       if ( handle && handle.apply && acceptData( cur ) ) {
+                               event.result = handle.apply( cur, data );
+                               if ( event.result === false ) {
+                                       event.preventDefault();
+                               }
+                       }
+               }
+               event.type = type;
+
+               // If nobody prevented the default action, do it now
+               if ( !onlyHandlers && !event.isDefaultPrevented() ) {
+
+                       if ( ( !special._default ||
+                               special._default.apply( eventPath.pop(), data ) === false ) &&
+                               acceptData( elem ) ) {
+
+                               // Call a native DOM method on the target with the same name as the event.
+                               // Don't do default actions on window, that's where global variables be (#6170)
+                               if ( ontype && isFunction( elem[ type ] ) && !isWindow( elem ) ) {
+
+                                       // Don't re-trigger an onFOO event when we call its FOO() method
+                                       tmp = elem[ ontype ];
+
+                                       if ( tmp ) {
+                                               elem[ ontype ] = null;
+                                       }
+
+                                       // Prevent re-triggering of the same event, since we already bubbled it above
+                                       jQuery.event.triggered = type;
+
+                                       if ( event.isPropagationStopped() ) {
+                                               lastElement.addEventListener( type, stopPropagationCallback );
+                                       }
+
+                                       elem[ type ]();
+
+                                       if ( event.isPropagationStopped() ) {
+                                               lastElement.removeEventListener( type, stopPropagationCallback );
+                                       }
+
+                                       jQuery.event.triggered = undefined;
+
+                                       if ( tmp ) {
+                                               elem[ ontype ] = tmp;
+                                       }
+                               }
+                       }
+               }
+
+               return event.result;
+       },
+
+       // Piggyback on a donor event to simulate a different one
+       // Used only for `focus(in | out)` events
+       simulate: function( type, elem, event ) {
+               var e = jQuery.extend(
+                       new jQuery.Event(),
+                       event,
+                       {
+                               type: type,
+                               isSimulated: true
+                       }
+               );
+
+               jQuery.event.trigger( e, null, elem );
+       }
+
+} );
+
+jQuery.fn.extend( {
+
+       trigger: function( type, data ) {
+               return this.each( function() {
+                       jQuery.event.trigger( type, data, this );
+               } );
+       },
+       triggerHandler: function( type, data ) {
+               var elem = this[ 0 ];
+               if ( elem ) {
+                       return jQuery.event.trigger( type, data, elem, true );
+               }
+       }
+} );
+
+
+// Support: Firefox <=44
+// Firefox doesn't have focus(in | out) events
+// Related ticket - https://bugzilla.mozilla.org/show_bug.cgi?id=687787
+//
+// Support: Chrome <=48 - 49, Safari <=9.0 - 9.1
+// focus(in | out) events fire after focus & blur events,
+// which is spec violation - http://www.w3.org/TR/DOM-Level-3-Events/#events-focusevent-event-order
+// Related ticket - https://bugs.chromium.org/p/chromium/issues/detail?id=449857
+if ( !support.focusin ) {
+       jQuery.each( { focus: "focusin", blur: "focusout" }, function( orig, fix ) {
+
+               // Attach a single capturing handler on the document while someone wants focusin/focusout
+               var handler = function( event ) {
+                       jQuery.event.simulate( fix, event.target, jQuery.event.fix( event ) );
+               };
+
+               jQuery.event.special[ fix ] = {
+                       setup: function() {
+                               var doc = this.ownerDocument || this,
+                                       attaches = dataPriv.access( doc, fix );
+
+                               if ( !attaches ) {
+                                       doc.addEventListener( orig, handler, true );
+                               }
+                               dataPriv.access( doc, fix, ( attaches || 0 ) + 1 );
+                       },
+                       teardown: function() {
+                               var doc = this.ownerDocument || this,
+                                       attaches = dataPriv.access( doc, fix ) - 1;
+
+                               if ( !attaches ) {
+                                       doc.removeEventListener( orig, handler, true );
+                                       dataPriv.remove( doc, fix );
+
+                               } else {
+                                       dataPriv.access( doc, fix, attaches );
+                               }
+                       }
+               };
+       } );
+}
+var location = window.location;
+
+var nonce = Date.now();
+
+var rquery = ( /\?/ );
+
+
+
+// Cross-browser xml parsing
+jQuery.parseXML = function( data ) {
+       var xml;
+       if ( !data || typeof data !== "string" ) {
+               return null;
+       }
+
+       // Support: IE 9 - 11 only
+       // IE throws on parseFromString with invalid input.
+       try {
+               xml = ( new window.DOMParser() ).parseFromString( data, "text/xml" );
+       } catch ( e ) {
+               xml = undefined;
+       }
+
+       if ( !xml || xml.getElementsByTagName( "parsererror" ).length ) {
+               jQuery.error( "Invalid XML: " + data );
+       }
+       return xml;
+};
+
+
+var
+       rbracket = /\[\]$/,
+       rCRLF = /\r?\n/g,
+       rsubmitterTypes = /^(?:submit|button|image|reset|file)$/i,
+       rsubmittable = /^(?:input|select|textarea|keygen)/i;
+
+function buildParams( prefix, obj, traditional, add ) {
+       var name;
+
+       if ( Array.isArray( obj ) ) {
+
+               // Serialize array item.
+               jQuery.each( obj, function( i, v ) {
+                       if ( traditional || rbracket.test( prefix ) ) {
+
+                               // Treat each array item as a scalar.
+                               add( prefix, v );
+
+                       } else {
+
+                               // Item is non-scalar (array or object), encode its numeric index.
+                               buildParams(
+                                       prefix + "[" + ( typeof v === "object" && v != null ? i : "" ) + "]",
+                                       v,
+                                       traditional,
+                                       add
+                               );
+                       }
+               } );
+
+       } else if ( !traditional && toType( obj ) === "object" ) {
+
+               // Serialize object item.
+               for ( name in obj ) {
+                       buildParams( prefix + "[" + name + "]", obj[ name ], traditional, add );
+               }
+
+       } else {
+
+               // Serialize scalar item.
+               add( prefix, obj );
+       }
+}
+
+// Serialize an array of form elements or a set of
+// key/values into a query string
+jQuery.param = function( a, traditional ) {
+       var prefix,
+               s = [],
+               add = function( key, valueOrFunction ) {
+
+                       // If value is a function, invoke it and use its return value
+                       var value = isFunction( valueOrFunction ) ?
+                               valueOrFunction() :
+                               valueOrFunction;
+
+                       s[ s.length ] = encodeURIComponent( key ) + "=" +
+                               encodeURIComponent( value == null ? "" : value );
+               };
+
+       if ( a == null ) {
+               return "";
+       }
+
+       // If an array was passed in, assume that it is an array of form elements.
+       if ( Array.isArray( a ) || ( a.jquery && !jQuery.isPlainObject( a ) ) ) {
+
+               // Serialize the form elements
+               jQuery.each( a, function() {
+                       add( this.name, this.value );
+               } );
+
+       } else {
+
+               // If traditional, encode the "old" way (the way 1.3.2 or older
+               // did it), otherwise encode params recursively.
+               for ( prefix in a ) {
+                       buildParams( prefix, a[ prefix ], traditional, add );
+               }
+       }
+
+       // Return the resulting serialization
+       return s.join( "&" );
+};
+
+jQuery.fn.extend( {
+       serialize: function() {
+               return jQuery.param( this.serializeArray() );
+       },
+       serializeArray: function() {
+               return this.map( function() {
+
+                       // Can add propHook for "elements" to filter or add form elements
+                       var elements = jQuery.prop( this, "elements" );
+                       return elements ? jQuery.makeArray( elements ) : this;
+               } )
+               .filter( function() {
+                       var type = this.type;
+
+                       // Use .is( ":disabled" ) so that fieldset[disabled] works
+                       return this.name && !jQuery( this ).is( ":disabled" ) &&
+                               rsubmittable.test( this.nodeName ) && !rsubmitterTypes.test( type ) &&
+                               ( this.checked || !rcheckableType.test( type ) );
+               } )
+               .map( function( i, elem ) {
+                       var val = jQuery( this ).val();
+
+                       if ( val == null ) {
+                               return null;
+                       }
+
+                       if ( Array.isArray( val ) ) {
+                               return jQuery.map( val, function( val ) {
+                                       return { name: elem.name, value: val.replace( rCRLF, "\r\n" ) };
+                               } );
+                       }
+
+                       return { name: elem.name, value: val.replace( rCRLF, "\r\n" ) };
+               } ).get();
+       }
+} );
+
+
+var
+       r20 = /%20/g,
+       rhash = /#.*$/,
+       rantiCache = /([?&])_=[^&]*/,
+       rheaders = /^(.*?):[ \t]*([^\r\n]*)$/mg,
+
+       // #7653, #8125, #8152: local protocol detection
+       rlocalProtocol = /^(?:about|app|app-storage|.+-extension|file|res|widget):$/,
+       rnoContent = /^(?:GET|HEAD)$/,
+       rprotocol = /^\/\//,
+
+       /* Prefilters
+        * 1) They are useful to introduce custom dataTypes (see ajax/jsonp.js for an example)
+        * 2) These are called:
+        *    - BEFORE asking for a transport
+        *    - AFTER param serialization (s.data is a string if s.processData is true)
+        * 3) key is the dataType
+        * 4) the catchall symbol "*" can be used
+        * 5) execution will start with transport dataType and THEN continue down to "*" if needed
+        */
+       prefilters = {},
+
+       /* Transports bindings
+        * 1) key is the dataType
+        * 2) the catchall symbol "*" can be used
+        * 3) selection will start with transport dataType and THEN go to "*" if needed
+        */
+       transports = {},
+
+       // Avoid comment-prolog char sequence (#10098); must appease lint and evade compression
+       allTypes = "*/".concat( "*" ),
+
+       // Anchor tag for parsing the document origin
+       originAnchor = document.createElement( "a" );
+       originAnchor.href = location.href;
+
+// Base "constructor" for jQuery.ajaxPrefilter and jQuery.ajaxTransport
+function addToPrefiltersOrTransports( structure ) {
+
+       // dataTypeExpression is optional and defaults to "*"
+       return function( dataTypeExpression, func ) {
+
+               if ( typeof dataTypeExpression !== "string" ) {
+                       func = dataTypeExpression;
+                       dataTypeExpression = "*";
+               }
+
+               var dataType,
+                       i = 0,
+                       dataTypes = dataTypeExpression.toLowerCase().match( rnothtmlwhite ) || [];
+
+               if ( isFunction( func ) ) {
+
+                       // For each dataType in the dataTypeExpression
+                       while ( ( dataType = dataTypes[ i++ ] ) ) {
+
+                               // Prepend if requested
+                               if ( dataType[ 0 ] === "+" ) {
+                                       dataType = dataType.slice( 1 ) || "*";
+                                       ( structure[ dataType ] = structure[ dataType ] || [] ).unshift( func );
+
+                               // Otherwise append
+                               } else {
+                                       ( structure[ dataType ] = structure[ dataType ] || [] ).push( func );
+                               }
+                       }
+               }
+       };
+}
+
+// Base inspection function for prefilters and transports
+function inspectPrefiltersOrTransports( structure, options, originalOptions, jqXHR ) {
+
+       var inspected = {},
+               seekingTransport = ( structure === transports );
+
+       function inspect( dataType ) {
+               var selected;
+               inspected[ dataType ] = true;
+               jQuery.each( structure[ dataType ] || [], function( _, prefilterOrFactory ) {
+                       var dataTypeOrTransport = prefilterOrFactory( options, originalOptions, jqXHR );
+                       if ( typeof dataTypeOrTransport === "string" &&
+                               !seekingTransport && !inspected[ dataTypeOrTransport ] ) {
+
+                               options.dataTypes.unshift( dataTypeOrTransport );
+                               inspect( dataTypeOrTransport );
+                               return false;
+                       } else if ( seekingTransport ) {
+                               return !( selected = dataTypeOrTransport );
+                       }
+               } );
+               return selected;
+       }
+
+       return inspect( options.dataTypes[ 0 ] ) || !inspected[ "*" ] && inspect( "*" );
+}
+
+// A special extend for ajax options
+// that takes "flat" options (not to be deep extended)
+// Fixes #9887
+function ajaxExtend( target, src ) {
+       var key, deep,
+               flatOptions = jQuery.ajaxSettings.flatOptions || {};
+
+       for ( key in src ) {
+               if ( src[ key ] !== undefined ) {
+                       ( flatOptions[ key ] ? target : ( deep || ( deep = {} ) ) )[ key ] = src[ key ];
+               }
+       }
+       if ( deep ) {
+               jQuery.extend( true, target, deep );
+       }
+
+       return target;
+}
+
+/* Handles responses to an ajax request:
+ * - finds the right dataType (mediates between content-type and expected dataType)
+ * - returns the corresponding response
+ */
+function ajaxHandleResponses( s, jqXHR, responses ) {
+
+       var ct, type, finalDataType, firstDataType,
+               contents = s.contents,
+               dataTypes = s.dataTypes;
+
+       // Remove auto dataType and get content-type in the process
+       while ( dataTypes[ 0 ] === "*" ) {
+               dataTypes.shift();
+               if ( ct === undefined ) {
+                       ct = s.mimeType || jqXHR.getResponseHeader( "Content-Type" );
+               }
+       }
+
+       // Check if we're dealing with a known content-type
+       if ( ct ) {
+               for ( type in contents ) {
+                       if ( contents[ type ] && contents[ type ].test( ct ) ) {
+                               dataTypes.unshift( type );
+                               break;
+                       }
+               }
+       }
+
+       // Check to see if we have a response for the expected dataType
+       if ( dataTypes[ 0 ] in responses ) {
+               finalDataType = dataTypes[ 0 ];
+       } else {
+
+               // Try convertible dataTypes
+               for ( type in responses ) {
+                       if ( !dataTypes[ 0 ] || s.converters[ type + " " + dataTypes[ 0 ] ] ) {
+                               finalDataType = type;
+                               break;
+                       }
+                       if ( !firstDataType ) {
+                               firstDataType = type;
+                       }
+               }
+
+               // Or just use first one
+               finalDataType = finalDataType || firstDataType;
+       }
+
+       // If we found a dataType
+       // We add the dataType to the list if needed
+       // and return the corresponding response
+       if ( finalDataType ) {
+               if ( finalDataType !== dataTypes[ 0 ] ) {
+                       dataTypes.unshift( finalDataType );
+               }
+               return responses[ finalDataType ];
+       }
+}
+
+/* Chain conversions given the request and the original response
+ * Also sets the responseXXX fields on the jqXHR instance
+ */
+function ajaxConvert( s, response, jqXHR, isSuccess ) {
+       var conv2, current, conv, tmp, prev,
+               converters = {},
+
+               // Work with a copy of dataTypes in case we need to modify it for conversion
+               dataTypes = s.dataTypes.slice();
+
+       // Create converters map with lowercased keys
+       if ( dataTypes[ 1 ] ) {
+               for ( conv in s.converters ) {
+                       converters[ conv.toLowerCase() ] = s.converters[ conv ];
+               }
+       }
+
+       current = dataTypes.shift();
+
+       // Convert to each sequential dataType
+       while ( current ) {
+
+               if ( s.responseFields[ current ] ) {
+                       jqXHR[ s.responseFields[ current ] ] = response;
+               }
+
+               // Apply the dataFilter if provided
+               if ( !prev && isSuccess && s.dataFilter ) {
+                       response = s.dataFilter( response, s.dataType );
+               }
+
+               prev = current;
+               current = dataTypes.shift();
+
+               if ( current ) {
+
+                       // There's only work to do if current dataType is non-auto
+                       if ( current === "*" ) {
+
+                               current = prev;
+
+                       // Convert response if prev dataType is non-auto and differs from current
+                       } else if ( prev !== "*" && prev !== current ) {
+
+                               // Seek a direct converter
+                               conv = converters[ prev + " " + current ] || converters[ "* " + current ];
+
+                               // If none found, seek a pair
+                               if ( !conv ) {
+                                       for ( conv2 in converters ) {
+
+                                               // If conv2 outputs current
+                                               tmp = conv2.split( " " );
+                                               if ( tmp[ 1 ] === current ) {
+
+                                                       // If prev can be converted to accepted input
+                                                       conv = converters[ prev + " " + tmp[ 0 ] ] ||
+                                                               converters[ "* " + tmp[ 0 ] ];
+                                                       if ( conv ) {
+
+                                                               // Condense equivalence converters
+                                                               if ( conv === true ) {
+                                                                       conv = converters[ conv2 ];
+
+                                                               // Otherwise, insert the intermediate dataType
+                                                               } else if ( converters[ conv2 ] !== true ) {
+                                                                       current = tmp[ 0 ];
+                                                                       dataTypes.unshift( tmp[ 1 ] );
+                                                               }
+                                                               break;
+                                                       }
+                                               }
+                                       }
+                               }
+
+                               // Apply converter (if not an equivalence)
+                               if ( conv !== true ) {
+
+                                       // Unless errors are allowed to bubble, catch and return them
+                                       if ( conv && s.throws ) {
+                                               response = conv( response );
+                                       } else {
+                                               try {
+                                                       response = conv( response );
+                                               } catch ( e ) {
+                                                       return {
+                                                               state: "parsererror",
+                                                               error: conv ? e : "No conversion from " + prev + " to " + current
+                                                       };
+                                               }
+                                       }
+                               }
+                       }
+               }
+       }
+
+       return { state: "success", data: response };
+}
+
+jQuery.extend( {
+
+       // Counter for holding the number of active queries
+       active: 0,
+
+       // Last-Modified header cache for next request
+       lastModified: {},
+       etag: {},
+
+       ajaxSettings: {
+               url: location.href,
+               type: "GET",
+               isLocal: rlocalProtocol.test( location.protocol ),
+               global: true,
+               processData: true,
+               async: true,
+               contentType: "application/x-www-form-urlencoded; charset=UTF-8",
+
+               /*
+               timeout: 0,
+               data: null,
+               dataType: null,
+               username: null,
+               password: null,
+               cache: null,
+               throws: false,
+               traditional: false,
+               headers: {},
+               */
+
+               accepts: {
+                       "*": allTypes,
+                       text: "text/plain",
+                       html: "text/html",
+                       xml: "application/xml, text/xml",
+                       json: "application/json, text/javascript"
+               },
+
+               contents: {
+                       xml: /\bxml\b/,
+                       html: /\bhtml/,
+                       json: /\bjson\b/
+               },
+
+               responseFields: {
+                       xml: "responseXML",
+                       text: "responseText",
+                       json: "responseJSON"
+               },
+
+               // Data converters
+               // Keys separate source (or catchall "*") and destination types with a single space
+               converters: {
+
+                       // Convert anything to text
+                       "* text": String,
+
+                       // Text to html (true = no transformation)
+                       "text html": true,
+
+                       // Evaluate text as a json expression
+                       "text json": JSON.parse,
+
+                       // Parse text as xml
+                       "text xml": jQuery.parseXML
+               },
+
+               // For options that shouldn't be deep extended:
+               // you can add your own custom options here if
+               // and when you create one that shouldn't be
+               // deep extended (see ajaxExtend)
+               flatOptions: {
+                       url: true,
+                       context: true
+               }
+       },
+
+       // Creates a full fledged settings object into target
+       // with both ajaxSettings and settings fields.
+       // If target is omitted, writes into ajaxSettings.
+       ajaxSetup: function( target, settings ) {
+               return settings ?
+
+                       // Building a settings object
+                       ajaxExtend( ajaxExtend( target, jQuery.ajaxSettings ), settings ) :
+
+                       // Extending ajaxSettings
+                       ajaxExtend( jQuery.ajaxSettings, target );
+       },
+
+       ajaxPrefilter: addToPrefiltersOrTransports( prefilters ),
+       ajaxTransport: addToPrefiltersOrTransports( transports ),
+
+       // Main method
+       ajax: function( url, options ) {
+
+               // If url is an object, simulate pre-1.5 signature
+               if ( typeof url === "object" ) {
+                       options = url;
+                       url = undefined;
+               }
+
+               // Force options to be an object
+               options = options || {};
+
+               var transport,
+
+                       // URL without anti-cache param
+                       cacheURL,
+
+                       // Response headers
+                       responseHeadersString,
+                       responseHeaders,
+
+                       // timeout handle
+                       timeoutTimer,
+
+                       // Url cleanup var
+                       urlAnchor,
+
+                       // Request state (becomes false upon send and true upon completion)
+                       completed,
+
+                       // To know if global events are to be dispatched
+                       fireGlobals,
+
+                       // Loop variable
+                       i,
+
+                       // uncached part of the url
+                       uncached,
+
+                       // Create the final options object
+                       s = jQuery.ajaxSetup( {}, options ),
+
+                       // Callbacks context
+                       callbackContext = s.context || s,
+
+                       // Context for global events is callbackContext if it is a DOM node or jQuery collection
+                       globalEventContext = s.context &&
+                               ( callbackContext.nodeType || callbackContext.jquery ) ?
+                                       jQuery( callbackContext ) :
+                                       jQuery.event,
+
+                       // Deferreds
+                       deferred = jQuery.Deferred(),
+                       completeDeferred = jQuery.Callbacks( "once memory" ),
+
+                       // Status-dependent callbacks
+                       statusCode = s.statusCode || {},
+
+                       // Headers (they are sent all at once)
+                       requestHeaders = {},
+                       requestHeadersNames = {},
+
+                       // Default abort message
+                       strAbort = "canceled",
+
+                       // Fake xhr
+                       jqXHR = {
+                               readyState: 0,
+
+                               // Builds headers hashtable if needed
+                               getResponseHeader: function( key ) {
+                                       var match;
+                                       if ( completed ) {
+                                               if ( !responseHeaders ) {
+                                                       responseHeaders = {};
+                                                       while ( ( match = rheaders.exec( responseHeadersString ) ) ) {
+                                                               responseHeaders[ match[ 1 ].toLowerCase() + " " ] =
+                                                                       ( responseHeaders[ match[ 1 ].toLowerCase() + " " ] || [] )
+                                                                               .concat( match[ 2 ] );
+                                                       }
+                                               }
+                                               match = responseHeaders[ key.toLowerCase() + " " ];
+                                       }
+                                       return match == null ? null : match.join( ", " );
+                               },
+
+                               // Raw string
+                               getAllResponseHeaders: function() {
+                                       return completed ? responseHeadersString : null;
+                               },
+
+                               // Caches the header
+                               setRequestHeader: function( name, value ) {
+                                       if ( completed == null ) {
+                                               name = requestHeadersNames[ name.toLowerCase() ] =
+                                                       requestHeadersNames[ name.toLowerCase() ] || name;
+                                               requestHeaders[ name ] = value;
+                                       }
+                                       return this;
+                               },
+
+                               // Overrides response content-type header
+                               overrideMimeType: function( type ) {
+                                       if ( completed == null ) {
+                                               s.mimeType = type;
+                                       }
+                                       return this;
+                               },
+
+                               // Status-dependent callbacks
+                               statusCode: function( map ) {
+                                       var code;
+                                       if ( map ) {
+                                               if ( completed ) {
+
+                                                       // Execute the appropriate callbacks
+                                                       jqXHR.always( map[ jqXHR.status ] );
+                                               } else {
+
+                                                       // Lazy-add the new callbacks in a way that preserves old ones
+                                                       for ( code in map ) {
+                                                               statusCode[ code ] = [ statusCode[ code ], map[ code ] ];
+                                                       }
+                                               }
+                                       }
+                                       return this;
+                               },
+
+                               // Cancel the request
+                               abort: function( statusText ) {
+                                       var finalText = statusText || strAbort;
+                                       if ( transport ) {
+                                               transport.abort( finalText );
+                                       }
+                                       done( 0, finalText );
+                                       return this;
+                               }
+                       };
+
+               // Attach deferreds
+               deferred.promise( jqXHR );
+
+               // Add protocol if not provided (prefilters might expect it)
+               // Handle falsy url in the settings object (#10093: consistency with old signature)
+               // We also use the url parameter if available
+               s.url = ( ( url || s.url || location.href ) + "" )
+                       .replace( rprotocol, location.protocol + "//" );
+
+               // Alias method option to type as per ticket #12004
+               s.type = options.method || options.type || s.method || s.type;
+
+               // Extract dataTypes list
+               s.dataTypes = ( s.dataType || "*" ).toLowerCase().match( rnothtmlwhite ) || [ "" ];
+
+               // A cross-domain request is in order when the origin doesn't match the current origin.
+               if ( s.crossDomain == null ) {
+                       urlAnchor = document.createElement( "a" );
+
+                       // Support: IE <=8 - 11, Edge 12 - 15
+                       // IE throws exception on accessing the href property if url is malformed,
+                       // e.g. http://example.com:80x/
+                       try {
+                               urlAnchor.href = s.url;
+
+                               // Support: IE <=8 - 11 only
+                               // Anchor's host property isn't correctly set when s.url is relative
+                               urlAnchor.href = urlAnchor.href;
+                               s.crossDomain = originAnchor.protocol + "//" + originAnchor.host !==
+                                       urlAnchor.protocol + "//" + urlAnchor.host;
+                       } catch ( e ) {
+
+                               // If there is an error parsing the URL, assume it is crossDomain,
+                               // it can be rejected by the transport if it is invalid
+                               s.crossDomain = true;
+                       }
+               }
+
+               // Convert data if not already a string
+               if ( s.data && s.processData && typeof s.data !== "string" ) {
+                       s.data = jQuery.param( s.data, s.traditional );
+               }
+
+               // Apply prefilters
+               inspectPrefiltersOrTransports( prefilters, s, options, jqXHR );
+
+               // If request was aborted inside a prefilter, stop there
+               if ( completed ) {
+                       return jqXHR;
+               }
+
+               // We can fire global events as of now if asked to
+               // Don't fire events if jQuery.event is undefined in an AMD-usage scenario (#15118)
+               fireGlobals = jQuery.event && s.global;
+
+               // Watch for a new set of requests
+               if ( fireGlobals && jQuery.active++ === 0 ) {
+                       jQuery.event.trigger( "ajaxStart" );
+               }
+
+               // Uppercase the type
+               s.type = s.type.toUpperCase();
+
+               // Determine if request has content
+               s.hasContent = !rnoContent.test( s.type );
+
+               // Save the URL in case we're toying with the If-Modified-Since
+               // and/or If-None-Match header later on
+               // Remove hash to simplify url manipulation
+               cacheURL = s.url.replace( rhash, "" );
+
+               // More options handling for requests with no content
+               if ( !s.hasContent ) {
+
+                       // Remember the hash so we can put it back
+                       uncached = s.url.slice( cacheURL.length );
+
+                       // If data is available and should be processed, append data to url
+                       if ( s.data && ( s.processData || typeof s.data === "string" ) ) {
+                               cacheURL += ( rquery.test( cacheURL ) ? "&" : "?" ) + s.data;
+
+                               // #9682: remove data so that it's not used in an eventual retry
+                               delete s.data;
+                       }
+
+                       // Add or update anti-cache param if needed
+                       if ( s.cache === false ) {
+                               cacheURL = cacheURL.replace( rantiCache, "$1" );
+                               uncached = ( rquery.test( cacheURL ) ? "&" : "?" ) + "_=" + ( nonce++ ) + uncached;
+                       }
+
+                       // Put hash and anti-cache on the URL that will be requested (gh-1732)
+                       s.url = cacheURL + uncached;
+
+               // Change '%20' to '+' if this is encoded form body content (gh-2658)
+               } else if ( s.data && s.processData &&
+                       ( s.contentType || "" ).indexOf( "application/x-www-form-urlencoded" ) === 0 ) {
+                       s.data = s.data.replace( r20, "+" );
+               }
+
+               // Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode.
+               if ( s.ifModified ) {
+                       if ( jQuery.lastModified[ cacheURL ] ) {
+                               jqXHR.setRequestHeader( "If-Modified-Since", jQuery.lastModified[ cacheURL ] );
+                       }
+                       if ( jQuery.etag[ cacheURL ] ) {
+                               jqXHR.setRequestHeader( "If-None-Match", jQuery.etag[ cacheURL ] );
+                       }
+               }
+
+               // Set the correct header, if data is being sent
+               if ( s.data && s.hasContent && s.contentType !== false || options.contentType ) {
+                       jqXHR.setRequestHeader( "Content-Type", s.contentType );
+               }
+
+               // Set the Accepts header for the server, depending on the dataType
+               jqXHR.setRequestHeader(
+                       "Accept",
+                       s.dataTypes[ 0 ] && s.accepts[ s.dataTypes[ 0 ] ] ?
+                               s.accepts[ s.dataTypes[ 0 ] ] +
+                                       ( s.dataTypes[ 0 ] !== "*" ? ", " + allTypes + "; q=0.01" : "" ) :
+                               s.accepts[ "*" ]
+               );
+
+               // Check for headers option
+               for ( i in s.headers ) {
+                       jqXHR.setRequestHeader( i, s.headers[ i ] );
+               }
+
+               // Allow custom headers/mimetypes and early abort
+               if ( s.beforeSend &&
+                       ( s.beforeSend.call( callbackContext, jqXHR, s ) === false || completed ) ) {
+
+                       // Abort if not done already and return
+                       return jqXHR.abort();
+               }
+
+               // Aborting is no longer a cancellation
+               strAbort = "abort";
+
+               // Install callbacks on deferreds
+               completeDeferred.add( s.complete );
+               jqXHR.done( s.success );
+               jqXHR.fail( s.error );
+
+               // Get transport
+               transport = inspectPrefiltersOrTransports( transports, s, options, jqXHR );
+
+               // If no transport, we auto-abort
+               if ( !transport ) {
+                       done( -1, "No Transport" );
+               } else {
+                       jqXHR.readyState = 1;
+
+                       // Send global event
+                       if ( fireGlobals ) {
+                               globalEventContext.trigger( "ajaxSend", [ jqXHR, s ] );
+                       }
+
+                       // If request was aborted inside ajaxSend, stop there
+                       if ( completed ) {
+                               return jqXHR;
+                       }
+
+                       // Timeout
+                       if ( s.async && s.timeout > 0 ) {
+                               timeoutTimer = window.setTimeout( function() {
+                                       jqXHR.abort( "timeout" );
+                               }, s.timeout );
+                       }
+
+                       try {
+                               completed = false;
+                               transport.send( requestHeaders, done );
+                       } catch ( e ) {
+
+                               // Rethrow post-completion exceptions
+                               if ( completed ) {
+                                       throw e;
+                               }
+
+                               // Propagate others as results
+                               done( -1, e );
+                       }
+               }
+
+               // Callback for when everything is done
+               function done( status, nativeStatusText, responses, headers ) {
+                       var isSuccess, success, error, response, modified,
+                               statusText = nativeStatusText;
+
+                       // Ignore repeat invocations
+                       if ( completed ) {
+                               return;
+                       }
+
+                       completed = true;
+
+                       // Clear timeout if it exists
+                       if ( timeoutTimer ) {
+                               window.clearTimeout( timeoutTimer );
+                       }
+
+                       // Dereference transport for early garbage collection
+                       // (no matter how long the jqXHR object will be used)
+                       transport = undefined;
+
+                       // Cache response headers
+                       responseHeadersString = headers || "";
+
+                       // Set readyState
+                       jqXHR.readyState = status > 0 ? 4 : 0;
+
+                       // Determine if successful
+                       isSuccess = status >= 200 && status < 300 || status === 304;
+
+                       // Get response data
+                       if ( responses ) {
+                               response = ajaxHandleResponses( s, jqXHR, responses );
+                       }
+
+                       // Convert no matter what (that way responseXXX fields are always set)
+                       response = ajaxConvert( s, response, jqXHR, isSuccess );
+
+                       // If successful, handle type chaining
+                       if ( isSuccess ) {
+
+                               // Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode.
+                               if ( s.ifModified ) {
+                                       modified = jqXHR.getResponseHeader( "Last-Modified" );
+                                       if ( modified ) {
+                                               jQuery.lastModified[ cacheURL ] = modified;
+                                       }
+                                       modified = jqXHR.getResponseHeader( "etag" );
+                                       if ( modified ) {
+                                               jQuery.etag[ cacheURL ] = modified;
+                                       }
+                               }
+
+                               // if no content
+                               if ( status === 204 || s.type === "HEAD" ) {
+                                       statusText = "nocontent";
+
+                               // if not modified
+                               } else if ( status === 304 ) {
+                                       statusText = "notmodified";
+
+                               // If we have data, let's convert it
+                               } else {
+                                       statusText = response.state;
+                                       success = response.data;
+                                       error = response.error;
+                                       isSuccess = !error;
+                               }
+                       } else {
+
+                               // Extract error from statusText and normalize for non-aborts
+                               error = statusText;
+                               if ( status || !statusText ) {
+                                       statusText = "error";
+                                       if ( status < 0 ) {
+                                               status = 0;
+                                       }
+                               }
+                       }
+
+                       // Set data for the fake xhr object
+                       jqXHR.status = status;
+                       jqXHR.statusText = ( nativeStatusText || statusText ) + "";
+
+                       // Success/Error
+                       if ( isSuccess ) {
+                               deferred.resolveWith( callbackContext, [ success, statusText, jqXHR ] );
+                       } else {
+                               deferred.rejectWith( callbackContext, [ jqXHR, statusText, error ] );
+                       }
+
+                       // Status-dependent callbacks
+                       jqXHR.statusCode( statusCode );
+                       statusCode = undefined;
+
+                       if ( fireGlobals ) {
+                               globalEventContext.trigger( isSuccess ? "ajaxSuccess" : "ajaxError",
+                                       [ jqXHR, s, isSuccess ? success : error ] );
+                       }
+
+                       // Complete
+                       completeDeferred.fireWith( callbackContext, [ jqXHR, statusText ] );
+
+                       if ( fireGlobals ) {
+                               globalEventContext.trigger( "ajaxComplete", [ jqXHR, s ] );
+
+                               // Handle the global AJAX counter
+                               if ( !( --jQuery.active ) ) {
+                                       jQuery.event.trigger( "ajaxStop" );
+                               }
+                       }
+               }
+
+               return jqXHR;
+       },
+
+       getJSON: function( url, data, callback ) {
+               return jQuery.get( url, data, callback, "json" );
+       },
+
+       getScript: function( url, callback ) {
+               return jQuery.get( url, undefined, callback, "script" );
+       }
+} );
+
+jQuery.each( [ "get", "post" ], function( i, method ) {
+       jQuery[ method ] = function( url, data, callback, type ) {
+
+               // Shift arguments if data argument was omitted
+               if ( isFunction( data ) ) {
+                       type = type || callback;
+                       callback = data;
+                       data = undefined;
+               }
+
+               // The url can be an options object (which then must have .url)
+               return jQuery.ajax( jQuery.extend( {
+                       url: url,
+                       type: method,
+                       dataType: type,
+                       data: data,
+                       success: callback
+               }, jQuery.isPlainObject( url ) && url ) );
+       };
+} );
+
+
+jQuery._evalUrl = function( url, options ) {
+       return jQuery.ajax( {
+               url: url,
+
+               // Make this explicit, since user can override this through ajaxSetup (#11264)
+               type: "GET",
+               dataType: "script",
+               cache: true,
+               async: false,
+               global: false,
+
+               // Only evaluate the response if it is successful (gh-4126)
+               // dataFilter is not invoked for failure responses, so using it instead
+               // of the default converter is kludgy but it works.
+               converters: {
+                       "text script": function() {}
+               },
+               dataFilter: function( response ) {
+                       jQuery.globalEval( response, options );
+               }
+       } );
+};
+
+
+jQuery.fn.extend( {
+       wrapAll: function( html ) {
+               var wrap;
+
+               if ( this[ 0 ] ) {
+                       if ( isFunction( html ) ) {
+                               html = html.call( this[ 0 ] );
+                       }
+
+                       // The elements to wrap the target around
+                       wrap = jQuery( html, this[ 0 ].ownerDocument ).eq( 0 ).clone( true );
+
+                       if ( this[ 0 ].parentNode ) {
+                               wrap.insertBefore( this[ 0 ] );
+                       }
+
+                       wrap.map( function() {
+                               var elem = this;
+
+                               while ( elem.firstElementChild ) {
+                                       elem = elem.firstElementChild;
+                               }
+
+                               return elem;
+                       } ).append( this );
+               }
+
+               return this;
+       },
+
+       wrapInner: function( html ) {
+               if ( isFunction( html ) ) {
+                       return this.each( function( i ) {
+                               jQuery( this ).wrapInner( html.call( this, i ) );
+                       } );
+               }
+
+               return this.each( function() {
+                       var self = jQuery( this ),
+                               contents = self.contents();
+
+                       if ( contents.length ) {
+                               contents.wrapAll( html );
+
+                       } else {
+                               self.append( html );
+                       }
+               } );
+       },
+
+       wrap: function( html ) {
+               var htmlIsFunction = isFunction( html );
+
+               return this.each( function( i ) {
+                       jQuery( this ).wrapAll( htmlIsFunction ? html.call( this, i ) : html );
+               } );
+       },
+
+       unwrap: function( selector ) {
+               this.parent( selector ).not( "body" ).each( function() {
+                       jQuery( this ).replaceWith( this.childNodes );
+               } );
+               return this;
+       }
+} );
+
+
+jQuery.expr.pseudos.hidden = function( elem ) {
+       return !jQuery.expr.pseudos.visible( elem );
+};
+jQuery.expr.pseudos.visible = function( elem ) {
+       return !!( elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length );
+};
+
+
+
+
+jQuery.ajaxSettings.xhr = function() {
+       try {
+               return new window.XMLHttpRequest();
+       } catch ( e ) {}
+};
+
+var xhrSuccessStatus = {
+
+               // File protocol always yields status code 0, assume 200
+               0: 200,
+
+               // Support: IE <=9 only
+               // #1450: sometimes IE returns 1223 when it should be 204
+               1223: 204
+       },
+       xhrSupported = jQuery.ajaxSettings.xhr();
+
+support.cors = !!xhrSupported && ( "withCredentials" in xhrSupported );
+support.ajax = xhrSupported = !!xhrSupported;
+
+jQuery.ajaxTransport( function( options ) {
+       var callback, errorCallback;
+
+       // Cross domain only allowed if supported through XMLHttpRequest
+       if ( support.cors || xhrSupported && !options.crossDomain ) {
+               return {
+                       send: function( headers, complete ) {
+                               var i,
+                                       xhr = options.xhr();
+
+                               xhr.open(
+                                       options.type,
+                                       options.url,
+                                       options.async,
+                                       options.username,
+                                       options.password
+                               );
+
+                               // Apply custom fields if provided
+                               if ( options.xhrFields ) {
+                                       for ( i in options.xhrFields ) {
+                                               xhr[ i ] = options.xhrFields[ i ];
+                                       }
+                               }
+
+                               // Override mime type if needed
+                               if ( options.mimeType && xhr.overrideMimeType ) {
+                                       xhr.overrideMimeType( options.mimeType );
+                               }
+
+                               // X-Requested-With header
+                               // For cross-domain requests, seeing as conditions for a preflight are
+                               // akin to a jigsaw puzzle, we simply never set it to be sure.
+                               // (it can always be set on a per-request basis or even using ajaxSetup)
+                               // For same-domain requests, won't change header if already provided.
+                               if ( !options.crossDomain && !headers[ "X-Requested-With" ] ) {
+                                       headers[ "X-Requested-With" ] = "XMLHttpRequest";
+                               }
+
+                               // Set headers
+                               for ( i in headers ) {
+                                       xhr.setRequestHeader( i, headers[ i ] );
+                               }
+
+                               // Callback
+                               callback = function( type ) {
+                                       return function() {
+                                               if ( callback ) {
+                                                       callback = errorCallback = xhr.onload =
+                                                               xhr.onerror = xhr.onabort = xhr.ontimeout =
+                                                                       xhr.onreadystatechange = null;
+
+                                                       if ( type === "abort" ) {
+                                                               xhr.abort();
+                                                       } else if ( type === "error" ) {
+
+                                                               // Support: IE <=9 only
+                                                               // On a manual native abort, IE9 throws
+                                                               // errors on any property access that is not readyState
+                                                               if ( typeof xhr.status !== "number" ) {
+                                                                       complete( 0, "error" );
+                                                               } else {
+                                                                       complete(
+
+                                                                               // File: protocol always yields status 0; see #8605, #14207
+                                                                               xhr.status,
+                                                                               xhr.statusText
+                                                                       );
+                                                               }
+                                                       } else {
+                                                               complete(
+                                                                       xhrSuccessStatus[ xhr.status ] || xhr.status,
+                                                                       xhr.statusText,
+
+                                                                       // Support: IE <=9 only
+                                                                       // IE9 has no XHR2 but throws on binary (trac-11426)
+                                                                       // For XHR2 non-text, let the caller handle it (gh-2498)
+                                                                       ( xhr.responseType || "text" ) !== "text"  ||
+                                                                       typeof xhr.responseText !== "string" ?
+                                                                               { binary: xhr.response } :
+                                                                               { text: xhr.responseText },
+                                                                       xhr.getAllResponseHeaders()
+                                                               );
+                                                       }
+                                               }
+                                       };
+                               };
+
+                               // Listen to events
+                               xhr.onload = callback();
+                               errorCallback = xhr.onerror = xhr.ontimeout = callback( "error" );
+
+                               // Support: IE 9 only
+                               // Use onreadystatechange to replace onabort
+                               // to handle uncaught aborts
+                               if ( xhr.onabort !== undefined ) {
+                                       xhr.onabort = errorCallback;
+                               } else {
+                                       xhr.onreadystatechange = function() {
+
+                                               // Check readyState before timeout as it changes
+                                               if ( xhr.readyState === 4 ) {
+
+                                                       // Allow onerror to be called first,
+                                                       // but that will not handle a native abort
+                                                       // Also, save errorCallback to a variable
+                                                       // as xhr.onerror cannot be accessed
+                                                       window.setTimeout( function() {
+                                                               if ( callback ) {
+                                                                       errorCallback();
+                                                               }
+                                                       } );
+                                               }
+                                       };
+                               }
+
+                               // Create the abort callback
+                               callback = callback( "abort" );
+
+                               try {
+
+                                       // Do send the request (this may raise an exception)
+                                       xhr.send( options.hasContent && options.data || null );
+                               } catch ( e ) {
+
+                                       // #14683: Only rethrow if this hasn't been notified as an error yet
+                                       if ( callback ) {
+                                               throw e;
+                                       }
+                               }
+                       },
+
+                       abort: function() {
+                               if ( callback ) {
+                                       callback();
+                               }
+                       }
+               };
+       }
+} );
+
+
+
+
+// Prevent auto-execution of scripts when no explicit dataType was provided (See gh-2432)
+jQuery.ajaxPrefilter( function( s ) {
+       if ( s.crossDomain ) {
+               s.contents.script = false;
+       }
+} );
+
+// Install script dataType
+jQuery.ajaxSetup( {
+       accepts: {
+               script: "text/javascript, application/javascript, " +
+                       "application/ecmascript, application/x-ecmascript"
+       },
+       contents: {
+               script: /\b(?:java|ecma)script\b/
+       },
+       converters: {
+               "text script": function( text ) {
+                       jQuery.globalEval( text );
+                       return text;
+               }
+       }
+} );
+
+// Handle cache's special case and crossDomain
+jQuery.ajaxPrefilter( "script", function( s ) {
+       if ( s.cache === undefined ) {
+               s.cache = false;
+       }
+       if ( s.crossDomain ) {
+               s.type = "GET";
+       }
+} );
+
+// Bind script tag hack transport
+jQuery.ajaxTransport( "script", function( s ) {
+
+       // This transport only deals with cross domain or forced-by-attrs requests
+       if ( s.crossDomain || s.scriptAttrs ) {
+               var script, callback;
+               return {
+                       send: function( _, complete ) {
+                               script = jQuery( "<script>" )
+                                       .attr( s.scriptAttrs || {} )
+                                       .prop( { charset: s.scriptCharset, src: s.url } )
+                                       .on( "load error", callback = function( evt ) {
+                                               script.remove();
+                                               callback = null;
+                                               if ( evt ) {
+                                                       complete( evt.type === "error" ? 404 : 200, evt.type );
+                                               }
+                                       } );
+
+                               // Use native DOM manipulation to avoid our domManip AJAX trickery
+                               document.head.appendChild( script[ 0 ] );
+                       },
+                       abort: function() {
+                               if ( callback ) {
+                                       callback();
+                               }
+                       }
+               };
+       }
+} );
+
+
+
+
+var oldCallbacks = [],
+       rjsonp = /(=)\?(?=&|$)|\?\?/;
+
+// Default jsonp settings
+jQuery.ajaxSetup( {
+       jsonp: "callback",
+       jsonpCallback: function() {
+               var callback = oldCallbacks.pop() || ( jQuery.expando + "_" + ( nonce++ ) );
+               this[ callback ] = true;
+               return callback;
+       }
+} );
+
+// Detect, normalize options and install callbacks for jsonp requests
+jQuery.ajaxPrefilter( "json jsonp", function( s, originalSettings, jqXHR ) {
+
+       var callbackName, overwritten, responseContainer,
+               jsonProp = s.jsonp !== false && ( rjsonp.test( s.url ) ?
+                       "url" :
+                       typeof s.data === "string" &&
+                               ( s.contentType || "" )
+                                       .indexOf( "application/x-www-form-urlencoded" ) === 0 &&
+                               rjsonp.test( s.data ) && "data"
+               );
+
+       // Handle iff the expected data type is "jsonp" or we have a parameter to set
+       if ( jsonProp || s.dataTypes[ 0 ] === "jsonp" ) {
+
+               // Get callback name, remembering preexisting value associated with it
+               callbackName = s.jsonpCallback = isFunction( s.jsonpCallback ) ?
+                       s.jsonpCallback() :
+                       s.jsonpCallback;
+
+               // Insert callback into url or form data
+               if ( jsonProp ) {
+                       s[ jsonProp ] = s[ jsonProp ].replace( rjsonp, "$1" + callbackName );
+               } else if ( s.jsonp !== false ) {
+                       s.url += ( rquery.test( s.url ) ? "&" : "?" ) + s.jsonp + "=" + callbackName;
+               }
+
+               // Use data converter to retrieve json after script execution
+               s.converters[ "script json" ] = function() {
+                       if ( !responseContainer ) {
+                               jQuery.error( callbackName + " was not called" );
+                       }
+                       return responseContainer[ 0 ];
+               };
+
+               // Force json dataType
+               s.dataTypes[ 0 ] = "json";
+
+               // Install callback
+               overwritten = window[ callbackName ];
+               window[ callbackName ] = function() {
+                       responseContainer = arguments;
+               };
+
+               // Clean-up function (fires after converters)
+               jqXHR.always( function() {
+
+                       // If previous value didn't exist - remove it
+                       if ( overwritten === undefined ) {
+                               jQuery( window ).removeProp( callbackName );
+
+                       // Otherwise restore preexisting value
+                       } else {
+                               window[ callbackName ] = overwritten;
+                       }
+
+                       // Save back as free
+                       if ( s[ callbackName ] ) {
+
+                               // Make sure that re-using the options doesn't screw things around
+                               s.jsonpCallback = originalSettings.jsonpCallback;
+
+                               // Save the callback name for future use
+                               oldCallbacks.push( callbackName );
+                       }
+
+                       // Call if it was a function and we have a response
+                       if ( responseContainer && isFunction( overwritten ) ) {
+                               overwritten( responseContainer[ 0 ] );
+                       }
+
+                       responseContainer = overwritten = undefined;
+               } );
+
+               // Delegate to script
+               return "script";
+       }
+} );
+
+
+
+
+// Support: Safari 8 only
+// In Safari 8 documents created via document.implementation.createHTMLDocument
+// collapse sibling forms: the second one becomes a child of the first one.
+// Because of that, this security measure has to be disabled in Safari 8.
+// https://bugs.webkit.org/show_bug.cgi?id=137337
+support.createHTMLDocument = ( function() {
+       var body = document.implementation.createHTMLDocument( "" ).body;
+       body.innerHTML = "<form></form><form></form>";
+       return body.childNodes.length === 2;
+} )();
+
+
+// Argument "data" should be string of html
+// context (optional): If specified, the fragment will be created in this context,
+// defaults to document
+// keepScripts (optional): If true, will include scripts passed in the html string
+jQuery.parseHTML = function( data, context, keepScripts ) {
+       if ( typeof data !== "string" ) {
+               return [];
+       }
+       if ( typeof context === "boolean" ) {
+               keepScripts = context;
+               context = false;
+       }
+
+       var base, parsed, scripts;
+
+       if ( !context ) {
+
+               // Stop scripts or inline event handlers from being executed immediately
+               // by using document.implementation
+               if ( support.createHTMLDocument ) {
+                       context = document.implementation.createHTMLDocument( "" );
+
+                       // Set the base href for the created document
+                       // so any parsed elements with URLs
+                       // are based on the document's URL (gh-2965)
+                       base = context.createElement( "base" );
+                       base.href = document.location.href;
+                       context.head.appendChild( base );
+               } else {
+                       context = document;
+               }
+       }
+
+       parsed = rsingleTag.exec( data );
+       scripts = !keepScripts && [];
+
+       // Single tag
+       if ( parsed ) {
+               return [ context.createElement( parsed[ 1 ] ) ];
+       }
+
+       parsed = buildFragment( [ data ], context, scripts );
+
+       if ( scripts && scripts.length ) {
+               jQuery( scripts ).remove();
+       }
+
+       return jQuery.merge( [], parsed.childNodes );
+};
+
+
+/**
+ * Load a url into a page
+ */
+jQuery.fn.load = function( url, params, callback ) {
+       var selector, type, response,
+               self = this,
+               off = url.indexOf( " " );
+
+       if ( off > -1 ) {
+               selector = stripAndCollapse( url.slice( off ) );
+               url = url.slice( 0, off );
+       }
+
+       // If it's a function
+       if ( isFunction( params ) ) {
+
+               // We assume that it's the callback
+               callback = params;
+               params = undefined;
+
+       // Otherwise, build a param string
+       } else if ( params && typeof params === "object" ) {
+               type = "POST";
+       }
+
+       // If we have elements to modify, make the request
+       if ( self.length > 0 ) {
+               jQuery.ajax( {
+                       url: url,
+
+                       // If "type" variable is undefined, then "GET" method will be used.
+                       // Make value of this field explicit since
+                       // user can override it through ajaxSetup method
+                       type: type || "GET",
+                       dataType: "html",
+                       data: params
+               } ).done( function( responseText ) {
+
+                       // Save response for use in complete callback
+                       response = arguments;
+
+                       self.html( selector ?
+
+                               // If a selector was specified, locate the right elements in a dummy div
+                               // Exclude scripts to avoid IE 'Permission Denied' errors
+                               jQuery( "<div>" ).append( jQuery.parseHTML( responseText ) ).find( selector ) :
+
+                               // Otherwise use the full result
+                               responseText );
+
+               // If the request succeeds, this function gets "data", "status", "jqXHR"
+               // but they are ignored because response was set above.
+               // If it fails, this function gets "jqXHR", "status", "error"
+               } ).always( callback && function( jqXHR, status ) {
+                       self.each( function() {
+                               callback.apply( this, response || [ jqXHR.responseText, status, jqXHR ] );
+                       } );
+               } );
+       }
+
+       return this;
+};
+
+
+
+
+// Attach a bunch of functions for handling common AJAX events
+jQuery.each( [
+       "ajaxStart",
+       "ajaxStop",
+       "ajaxComplete",
+       "ajaxError",
+       "ajaxSuccess",
+       "ajaxSend"
+], function( i, type ) {
+       jQuery.fn[ type ] = function( fn ) {
+               return this.on( type, fn );
+       };
+} );
+
+
+
+
+jQuery.expr.pseudos.animated = function( elem ) {
+       return jQuery.grep( jQuery.timers, function( fn ) {
+               return elem === fn.elem;
+       } ).length;
+};
+
+
+
+
+jQuery.offset = {
+       setOffset: function( elem, options, i ) {
+               var curPosition, curLeft, curCSSTop, curTop, curOffset, curCSSLeft, calculatePosition,
+                       position = jQuery.css( elem, "position" ),
+                       curElem = jQuery( elem ),
+                       props = {};
+
+               // Set position first, in-case top/left are set even on static elem
+               if ( position === "static" ) {
+                       elem.style.position = "relative";
+               }
+
+               curOffset = curElem.offset();
+               curCSSTop = jQuery.css( elem, "top" );
+               curCSSLeft = jQuery.css( elem, "left" );
+               calculatePosition = ( position === "absolute" || position === "fixed" ) &&
+                       ( curCSSTop + curCSSLeft ).indexOf( "auto" ) > -1;
+
+               // Need to be able to calculate position if either
+               // top or left is auto and position is either absolute or fixed
+               if ( calculatePosition ) {
+                       curPosition = curElem.position();
+                       curTop = curPosition.top;
+                       curLeft = curPosition.left;
+
+               } else {
+                       curTop = parseFloat( curCSSTop ) || 0;
+                       curLeft = parseFloat( curCSSLeft ) || 0;
+               }
+
+               if ( isFunction( options ) ) {
+
+                       // Use jQuery.extend here to allow modification of coordinates argument (gh-1848)
+                       options = options.call( elem, i, jQuery.extend( {}, curOffset ) );
+               }
+
+               if ( options.top != null ) {
+                       props.top = ( options.top - curOffset.top ) + curTop;
+               }
+               if ( options.left != null ) {
+                       props.left = ( options.left - curOffset.left ) + curLeft;
+               }
+
+               if ( "using" in options ) {
+                       options.using.call( elem, props );
+
+               } else {
+                       curElem.css( props );
+               }
+       }
+};
+
+jQuery.fn.extend( {
+
+       // offset() relates an element's border box to the document origin
+       offset: function( options ) {
+
+               // Preserve chaining for setter
+               if ( arguments.length ) {
+                       return options === undefined ?
+                               this :
+                               this.each( function( i ) {
+                                       jQuery.offset.setOffset( this, options, i );
+                               } );
+               }
+
+               var rect, win,
+                       elem = this[ 0 ];
+
+               if ( !elem ) {
+                       return;
+               }
+
+               // Return zeros for disconnected and hidden (display: none) elements (gh-2310)
+               // Support: IE <=11 only
+               // Running getBoundingClientRect on a
+               // disconnected node in IE throws an error
+               if ( !elem.getClientRects().length ) {
+                       return { top: 0, left: 0 };
+               }
+
+               // Get document-relative position by adding viewport scroll to viewport-relative gBCR
+               rect = elem.getBoundingClientRect();
+               win = elem.ownerDocument.defaultView;
+               return {
+                       top: rect.top + win.pageYOffset,
+                       left: rect.left + win.pageXOffset
+               };
+       },
+
+       // position() relates an element's margin box to its offset parent's padding box
+       // This corresponds to the behavior of CSS absolute positioning
+       position: function() {
+               if ( !this[ 0 ] ) {
+                       return;
+               }
+
+               var offsetParent, offset, doc,
+                       elem = this[ 0 ],
+                       parentOffset = { top: 0, left: 0 };
+
+               // position:fixed elements are offset from the viewport, which itself always has zero offset
+               if ( jQuery.css( elem, "position" ) === "fixed" ) {
+
+                       // Assume position:fixed implies availability of getBoundingClientRect
+                       offset = elem.getBoundingClientRect();
+
+               } else {
+                       offset = this.offset();
+
+                       // Account for the *real* offset parent, which can be the document or its root element
+                       // when a statically positioned element is identified
+                       doc = elem.ownerDocument;
+                       offsetParent = elem.offsetParent || doc.documentElement;
+                       while ( offsetParent &&
+                               ( offsetParent === doc.body || offsetParent === doc.documentElement ) &&
+                               jQuery.css( offsetParent, "position" ) === "static" ) {
+
+                               offsetParent = offsetParent.parentNode;
+                       }
+                       if ( offsetParent && offsetParent !== elem && offsetParent.nodeType === 1 ) {
+
+                               // Incorporate borders into its offset, since they are outside its content origin
+                               parentOffset = jQuery( offsetParent ).offset();
+                               parentOffset.top += jQuery.css( offsetParent, "borderTopWidth", true );
+                               parentOffset.left += jQuery.css( offsetParent, "borderLeftWidth", true );
+                       }
+               }
+
+               // Subtract parent offsets and element margins
+               return {
+                       top: offset.top - parentOffset.top - jQuery.css( elem, "marginTop", true ),
+                       left: offset.left - parentOffset.left - jQuery.css( elem, "marginLeft", true )
+               };
+       },
+
+       // This method will return documentElement in the following cases:
+       // 1) For the element inside the iframe without offsetParent, this method will return
+       //    documentElement of the parent window
+       // 2) For the hidden or detached element
+       // 3) For body or html element, i.e. in case of the html node - it will return itself
+       //
+       // but those exceptions were never presented as a real life use-cases
+       // and might be considered as more preferable results.
+       //
+       // This logic, however, is not guaranteed and can change at any point in the future
+       offsetParent: function() {
+               return this.map( function() {
+                       var offsetParent = this.offsetParent;
+
+                       while ( offsetParent && jQuery.css( offsetParent, "position" ) === "static" ) {
+                               offsetParent = offsetParent.offsetParent;
+                       }
+
+                       return offsetParent || documentElement;
+               } );
+       }
+} );
+
+// Create scrollLeft and scrollTop methods
+jQuery.each( { scrollLeft: "pageXOffset", scrollTop: "pageYOffset" }, function( method, prop ) {
+       var top = "pageYOffset" === prop;
+
+       jQuery.fn[ method ] = function( val ) {
+               return access( this, function( elem, method, val ) {
+
+                       // Coalesce documents and windows
+                       var win;
+                       if ( isWindow( elem ) ) {
+                               win = elem;
+                       } else if ( elem.nodeType === 9 ) {
+                               win = elem.defaultView;
+                       }
+
+                       if ( val === undefined ) {
+                               return win ? win[ prop ] : elem[ method ];
+                       }
+
+                       if ( win ) {
+                               win.scrollTo(
+                                       !top ? val : win.pageXOffset,
+                                       top ? val : win.pageYOffset
+                               );
+
+                       } else {
+                               elem[ method ] = val;
+                       }
+               }, method, val, arguments.length );
+       };
+} );
+
+// Support: Safari <=7 - 9.1, Chrome <=37 - 49
+// Add the top/left cssHooks using jQuery.fn.position
+// Webkit bug: https://bugs.webkit.org/show_bug.cgi?id=29084
+// Blink bug: https://bugs.chromium.org/p/chromium/issues/detail?id=589347
+// getComputedStyle returns percent when specified for top/left/bottom/right;
+// rather than make the css module depend on the offset module, just check for it here
+jQuery.each( [ "top", "left" ], function( i, prop ) {
+       jQuery.cssHooks[ prop ] = addGetHookIf( support.pixelPosition,
+               function( elem, computed ) {
+                       if ( computed ) {
+                               computed = curCSS( elem, prop );
+
+                               // If curCSS returns percentage, fallback to offset
+                               return rnumnonpx.test( computed ) ?
+                                       jQuery( elem ).position()[ prop ] + "px" :
+                                       computed;
+                       }
+               }
+       );
+} );
+
+
+// Create innerHeight, innerWidth, height, width, outerHeight and outerWidth methods
+jQuery.each( { Height: "height", Width: "width" }, function( name, type ) {
+       jQuery.each( { padding: "inner" + name, content: type, "": "outer" + name },
+               function( defaultExtra, funcName ) {
+
+               // Margin is only for outerHeight, outerWidth
+               jQuery.fn[ funcName ] = function( margin, value ) {
+                       var chainable = arguments.length && ( defaultExtra || typeof margin !== "boolean" ),
+                               extra = defaultExtra || ( margin === true || value === true ? "margin" : "border" );
+
+                       return access( this, function( elem, type, value ) {
+                               var doc;
+
+                               if ( isWindow( elem ) ) {
+
+                                       // $( window ).outerWidth/Height return w/h including scrollbars (gh-1729)
+                                       return funcName.indexOf( "outer" ) === 0 ?
+                                               elem[ "inner" + name ] :
+                                               elem.document.documentElement[ "client" + name ];
+                               }
+
+                               // Get document width or height
+                               if ( elem.nodeType === 9 ) {
+                                       doc = elem.documentElement;
+
+                                       // Either scroll[Width/Height] or offset[Width/Height] or client[Width/Height],
+                                       // whichever is greatest
+                                       return Math.max(
+                                               elem.body[ "scroll" + name ], doc[ "scroll" + name ],
+                                               elem.body[ "offset" + name ], doc[ "offset" + name ],
+                                               doc[ "client" + name ]
+                                       );
+                               }
+
+                               return value === undefined ?
+
+                                       // Get width or height on the element, requesting but not forcing parseFloat
+                                       jQuery.css( elem, type, extra ) :
+
+                                       // Set width or height on the element
+                                       jQuery.style( elem, type, value, extra );
+                       }, type, chainable ? margin : undefined, chainable );
+               };
+       } );
+} );
+
+
+jQuery.each( ( "blur focus focusin focusout resize scroll click dblclick " +
+       "mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave " +
+       "change select submit keydown keypress keyup contextmenu" ).split( " " ),
+       function( i, name ) {
+
+       // Handle event binding
+       jQuery.fn[ name ] = function( data, fn ) {
+               return arguments.length > 0 ?
+                       this.on( name, null, data, fn ) :
+                       this.trigger( name );
+       };
+} );
+
+jQuery.fn.extend( {
+       hover: function( fnOver, fnOut ) {
+               return this.mouseenter( fnOver ).mouseleave( fnOut || fnOver );
+       }
+} );
+
+
+
+
+jQuery.fn.extend( {
+
+       bind: function( types, data, fn ) {
+               return this.on( types, null, data, fn );
+       },
+       unbind: function( types, fn ) {
+               return this.off( types, null, fn );
+       },
+
+       delegate: function( selector, types, data, fn ) {
+               return this.on( types, selector, data, fn );
+       },
+       undelegate: function( selector, types, fn ) {
+
+               // ( namespace ) or ( selector, types [, fn] )
+               return arguments.length === 1 ?
+                       this.off( selector, "**" ) :
+                       this.off( types, selector || "**", fn );
+       }
+} );
+
+// Bind a function to a context, optionally partially applying any
+// arguments.
+// jQuery.proxy is deprecated to promote standards (specifically Function#bind)
+// However, it is not slated for removal any time soon
+jQuery.proxy = function( fn, context ) {
+       var tmp, args, proxy;
+
+       if ( typeof context === "string" ) {
+               tmp = fn[ context ];
+               context = fn;
+               fn = tmp;
+       }
+
+       // Quick check to determine if target is callable, in the spec
+       // this throws a TypeError, but we will just return undefined.
+       if ( !isFunction( fn ) ) {
+               return undefined;
+       }
+
+       // Simulated bind
+       args = slice.call( arguments, 2 );
+       proxy = function() {
+               return fn.apply( context || this, args.concat( slice.call( arguments ) ) );
+       };
+
+       // Set the guid of unique handler to the same of original handler, so it can be removed
+       proxy.guid = fn.guid = fn.guid || jQuery.guid++;
+
+       return proxy;
+};
+
+jQuery.holdReady = function( hold ) {
+       if ( hold ) {
+               jQuery.readyWait++;
+       } else {
+               jQuery.ready( true );
+       }
+};
+jQuery.isArray = Array.isArray;
+jQuery.parseJSON = JSON.parse;
+jQuery.nodeName = nodeName;
+jQuery.isFunction = isFunction;
+jQuery.isWindow = isWindow;
+jQuery.camelCase = camelCase;
+jQuery.type = toType;
+
+jQuery.now = Date.now;
+
+jQuery.isNumeric = function( obj ) {
+
+       // As of jQuery 3.0, isNumeric is limited to
+       // strings and numbers (primitives or objects)
+       // that can be coerced to finite numbers (gh-2662)
+       var type = jQuery.type( obj );
+       return ( type === "number" || type === "string" ) &&
+
+               // parseFloat NaNs numeric-cast false positives ("")
+               // ...but misinterprets leading-number strings, particularly hex literals ("0x...")
+               // subtraction forces infinities to NaN
+               !isNaN( obj - parseFloat( obj ) );
+};
+
+
+
+
+// Register as a named AMD module, since jQuery can be concatenated with other
+// files that may use define, but not via a proper concatenation script that
+// understands anonymous AMD modules. A named AMD is safest and most robust
+// way to register. Lowercase jquery is used because AMD module names are
+// derived from file names, and jQuery is normally delivered in a lowercase
+// file name. Do this after creating the global so that if an AMD module wants
+// to call noConflict to hide this version of jQuery, it will work.
+
+// Note that for maximum portability, libraries that are not jQuery should
+// declare themselves as anonymous modules, and avoid setting a global if an
+// AMD loader is present. jQuery is a special case. For more information, see
+// https://github.com/jrburke/requirejs/wiki/Updating-existing-libraries#wiki-anon
+
+if ( typeof define === "function" && define.amd ) {
+       define( "jquery", [], function() {
+               return jQuery;
+       } );
+}
+
+
+
+
+var
+
+       // Map over jQuery in case of overwrite
+       _jQuery = window.jQuery,
+
+       // Map over the $ in case of overwrite
+       _$ = window.$;
+
+jQuery.noConflict = function( deep ) {
+       if ( window.$ === jQuery ) {
+               window.$ = _$;
+       }
+
+       if ( deep && window.jQuery === jQuery ) {
+               window.jQuery = _jQuery;
+       }
+
+       return jQuery;
+};
+
+// Expose jQuery and $ identifiers, even in AMD
+// (#7102#comment:10, https://github.com/jquery/jquery/pull/557)
+// and CommonJS for browser emulators (#13566)
+if ( !noGlobal ) {
+       window.jQuery = window.$ = jQuery;
+}
+
+
+
+
+return jQuery;
+} );
index 84be0d4..333d9d1 100644 (file)
@@ -2,13 +2,13 @@
 
 function ajaxGet(url, data, successCallback, errorCallback) {
        (function(url, data, successCallback, errorCallback) {
-               $.ajax({"cache": false, "type": "GET", "url": url, "data": data, "dataType": "json", "success": function(data, textStatus, xmlHttpRequest) {
+               $.ajax({"cache": false, "type": "GET", "url": url, "data": data, "dataType": "json", "success": function(data, textStatus) {
                        ajaxSuccess();
                        if (typeof successCallback != "undefined") {
                                successCallback(data, textStatus);
                        }
-               }, "error": function(xmlHttpRequest, textStatus, errorThrown) {
-                       if (xmlHttpRequest.status == 403) {
+               }, "error": function(xmlHttpRequest) {
+                       if (xmlHttpRequest.status === 403) {
                                notLoggedIn = true;
                        }
                        if (typeof errorCallback != "undefined") {
@@ -22,10 +22,10 @@ function ajaxGet(url, data, successCallback, errorCallback) {
 
 function registerInputTextareaSwap(inputElement, defaultText, inputFieldName, optional, dontUseTextarea) {
        $(inputElement).each(function() {
-               var textarea = $(dontUseTextarea ? "<input type=\"text\" name=\"" + inputFieldName + "\">" : "<textarea name=\"" + inputFieldName + "\"></textarea>").blur(function() {
-                       if ($(this).val() == "") {
+               const textarea = $(dontUseTextarea ? "<input type=\"text\" name=\"" + inputFieldName + "\">" : "<textarea name=\"" + inputFieldName + "\"></textarea>").blur(function() {
+                       if ($(this).val() === "") {
                                $(this).hide();
-                               var inputField = $(this).data("inputField");
+                               const inputField = $(this).data("inputField");
                                inputField.show().removeAttr("disabled").addClass("default");
                                inputField.val(defaultText);
                        }
@@ -33,20 +33,20 @@ function registerInputTextareaSwap(inputElement, defaultText, inputFieldName, op
                $(this).data("textarea", textarea).after(textarea);
                (function(inputField, textarea) {
                        inputField.focus(function() {
-                               $(this).hide().attr("disabled", "disabled");
+                               $(this).hide().prop("disabled", "disabled");
                                /* no, show(), “display: block” is not what I need. */
-                               textarea.attr("style", "display: inline").focus();
+                               textarea.prop("style", "display: inline").focus();
                        });
-                       if (inputField.val() == "") {
+                       if (inputField.val() === "") {
                                inputField.addClass("default");
                                inputField.val(defaultText);
                        } else {
-                               inputField.hide().attr("disabled", "disabled");
+                               inputField.hide().prop("disabled", "disabled");
                                textarea.show();
                        }
                        $(inputField.get(0).form).submit(function() {
-                               inputField.attr("disabled", "disabled");
-                               if (!optional && (textarea.val() == "")) {
+                               inputField.prop("disabled", "disabled");
+                               if (!optional && (textarea.val() === "")) {
                                        inputField.removeAttr("disabled").focus();
                                        return false;
                                }
@@ -64,14 +64,14 @@ function registerInputTextareaSwap(inputElement, defaultText, inputFieldName, op
  *            The element to add a “comment” link to
  */
 function addCommentLink(postId, author, element, insertAfterThisElement) {
-       if (($(element).find(".show-reply-form").length > 0) || (getPostElement(element).find(".create-reply").length == 0)) {
+       if (($(element).find(".show-reply-form").length > 0) || (getPostElement(element).find(".create-reply").length === 0)) {
                return;
        }
        (function(postId, author, insertAfterThisElement) {
-               var separator = $("<span> · </span>").addClass("separator");
+               const separator = $("<span> · </span>").addClass("separator");
                getTranslation("WebInterface.Button.Comment", function(text) {
-                       var commentElement = $("<div><span>" + text + "</span></div>").addClass("show-reply-form").click(function() {
-                               var replyElement = sone.find(".post#post-" + postId + " .create-reply");
+                       const commentElement = $("<div><span>" + text + "</span></div>").addClass("show-reply-form").click(function() {
+                               const replyElement = sone.find(".post#post-" + postId + " .create-reply");
                                replyElement.removeClass("hidden");
                                replyElement.removeClass("light");
                                (function(replyElement) {
@@ -83,8 +83,8 @@ function addCommentLink(postId, author, element, insertAfterThisElement) {
                                                replyElement.removeClass("light");
                                        });
                                })(replyElement);
-                               var textArea = replyElement.find(":input.reply-input").focus().data("textarea");
-                               if (author != getCurrentSoneId()) {
+                               const textArea = replyElement.find(":input.reply-input").focus().data("textarea");
+                               if (author !== getCurrentSoneId()) {
                                        textArea.val(textArea.val() + "@sone://" + author + " ");
                                }
                        });
@@ -94,7 +94,7 @@ function addCommentLink(postId, author, element, insertAfterThisElement) {
        })(postId, author, insertAfterThisElement);
 }
 
-var translations = {};
+const translations = {};
 
 /**
  * Retrieves the translation for the given key and calls the callback function.
@@ -110,7 +110,7 @@ function getTranslation(key, callback) {
                callback(translations[key]);
                return;
        }
-       ajaxGet("getTranslation.ajax", {"key": key}, function(data, textStatus) {
+       ajaxGet("getTranslation.ajax", {"key": key}, function(data) {
                if ((data != null) && data.success) {
                        translations[key] = data.value;
                        callback(data.value);
@@ -145,16 +145,16 @@ function filterSoneId(soneId) {
  *            The date and time of the last update (formatted for display)
  */
 function updateSoneStatus(soneId, name, status, modified, locked, lastUpdated, lastUpdatedText) {
-    var updateSone = sone.find(".sone." + filterSoneId(soneId));
-       updateSone.toggleClass("unknown", status == "unknown").
-               toggleClass("idle", status == "idle").
-               toggleClass("inserting", status == "inserting").
-               toggleClass("downloading", status == "downloading").
+       const updateSone = sone.find(".sone." + filterSoneId(soneId));
+       updateSone.toggleClass("unknown", status === "unknown").
+               toggleClass("idle", status === "idle").
+               toggleClass("inserting", status === "inserting").
+               toggleClass("downloading", status === "downloading").
                toggleClass("modified", modified);
        updateSone.find(".lock").toggleClass("hidden", locked);
        updateSone.find(".unlock").toggleClass("hidden", !locked);
        if (lastUpdated != null) {
-               updateSone.find(".last-update span.time").attr("title", lastUpdated).text(lastUpdatedText);
+               updateSone.find(".last-update span.time").prop("title", lastUpdated).text(lastUpdatedText);
        } else {
                getTranslation("View.Sone.Text.UnknownDate", function(unknown) {
                        updateSone.find(".last-update span.time").text(unknown);
@@ -175,7 +175,7 @@ function updateSoneStatus(soneId, name, status, modified, locked, lastUpdated, l
  */
 function enhanceDeleteButton(button, text, deleteCallback) {
        (function(button) {
-               var newButton = $("<button></button>").addClass("confirm").hide().text(text).click(function() {
+               const newButton = $("<button></button>").addClass("confirm").hide().text(text).click(function() {
                        $(this).fadeOut("slow");
                        deleteCallback();
                        return false;
@@ -185,7 +185,7 @@ function enhanceDeleteButton(button, text, deleteCallback) {
                                button.fadeOut("slow", function() {
                                        newButton.fadeIn("slow");
                                        $(document).one("click", function() {
-                                               if (this != newButton.get(0)) {
+                                               if (this !== newButton.get(0)) {
                                                        newButton.fadeOut(function() {
                                                                button.fadeIn();
                                                        });
@@ -210,21 +210,21 @@ function enhanceDeleteButton(button, text, deleteCallback) {
  */
 function enhanceDeletePostButton(button, postId, text) {
        enhanceDeleteButton(button, text, function() {
-               ajaxGet("deletePost.ajax", { "post": postId, "formPassword": getFormPassword() }, function(data, textStatus) {
+               ajaxGet("deletePost.ajax", { "post": postId, "formPassword": getFormPassword() }, function(data) {
                        if (data == null) {
                                return;
                        }
                        if (data.success) {
                                sone.find(".post#post-" + postId).slideUp();
-                       } else if (data.error == "invalid-post-id") {
+                       } else if (data.error === "invalid-post-id") {
                                /* pretend the post is already gone. */
                                getPost(postId).slideUp();
-                       } else if (data.error == "auth-required") {
+                       } else if (data.error === "auth-required") {
                                alert("You need to be logged in.");
-                       } else if (data.error == "not-authorized") {
+                       } else if (data.error === "not-authorized") {
                                alert("You are not allowed to delete this post.");
                        }
-               }, function(xmlHttpRequest, textStatus, error) {
+               }, function() {
                        /* ignore error. */
                });
        });
@@ -242,21 +242,21 @@ function enhanceDeletePostButton(button, postId, text) {
  */
 function enhanceDeleteReplyButton(button, replyId, text) {
        enhanceDeleteButton(button, text, function() {
-               ajaxGet("deleteReply.ajax", { "reply": replyId, "formPassword": sone.find("#formPassword").text() }, function(data, textStatus) {
+               ajaxGet("deleteReply.ajax", { "reply": replyId, "formPassword": sone.find("#formPassword").text() }, function(data) {
                        if (data == null) {
                                return;
                        }
                        if (data.success) {
                                sone.find(".reply#reply-" + replyId).slideUp();
-                       } else if (data.error == "invalid-reply-id") {
+                       } else if (data.error === "invalid-reply-id") {
                                /* pretend the reply is already gone. */
                                getReply(replyId).slideUp();
-                       } else if (data.error == "auth-required") {
+                       } else if (data.error === "auth-required") {
                                alert("You need to be logged in.");
-                       } else if (data.error == "not-authorized") {
+                       } else if (data.error === "not-authorized") {
                                alert("You are not allowed to delete this reply.");
                        }
-               }, function(xmlHttpRequest, textStatus, error) {
+               }, function() {
                        /* ignore error. */
                });
        });
@@ -274,8 +274,8 @@ function getFormPassword() {
  * @returns All Sone elements with the given ID
  */
 function getSone(soneId) {
-       return sone.find(".sone").filter(function(index) {
-               return $(".id", this).text() == soneId;
+       return sone.find(".sone").filter(function() {
+               return $(".id", this).text() === soneId;
        });
 }
 
@@ -297,21 +297,14 @@ function getMenuSone(element) {
 
 /**
  * Generates a list of Sones by concatening the names of the given sones with a
- * new line character (“\n”).
+ * comma.
  *
  * @param sones
  *            The sones to format
  * @returns {String} The created string
  */
 function generateSoneList(sones) {
-       var soneList = "";
-       $.each(sones, function() {
-               if (soneList != "") {
-                       soneList += ", ";
-               }
-               soneList += this.name;
-       });
-       return soneList;
+       return sones.map(sone => sone.name).join(", ")
 }
 
 /**
@@ -341,7 +334,7 @@ function getPostElement(element) {
 }
 
 function getPostId(element) {
-       return getPostElement(element).attr("id").substr(5);
+       return getPostElement(element).prop("id").substr(5);
 }
 
 function getPostTime(element) {
@@ -375,7 +368,7 @@ function getReplyElement(element) {
 }
 
 function getReplyId(element) {
-       return getReplyElement(element).attr("id").substr(6);
+       return getReplyElement(element).prop("id").substr(6);
 }
 
 function getReplyTime(element) {
@@ -423,7 +416,7 @@ function getNotificationElement(element) {
  * @returns The ID of the notification
  */
 function getNotificationId(notificationElement) {
-       return $(notificationElement).attr("id");
+       return $(notificationElement).prop("id");
 }
 
 /**
@@ -434,142 +427,74 @@ function getNotificationId(notificationElement) {
  * @returns The last update time of the notification
  */
 function getNotificationLastUpdatedTime(notificationElement) {
-       return $(notificationElement).attr("lastUpdatedTime");
+       return $(notificationElement).prop("lastUpdatedTime");
 }
 
 function likePost(postId) {
-       ajaxGet("like.ajax", { "type": "post", "post" : postId, "formPassword": getFormPassword() }, function(data, textStatus) {
+       ajaxGet("like.ajax", { "type": "post", "post" : postId, "formPassword": getFormPassword() }, function(data) {
                if ((data == null) || !data.success) {
                        return;
                }
                sone.find(".post#post-" + postId + " > .inner-part > .status-line .like").addClass("hidden");
                sone.find(".post#post-" + postId + " > .inner-part > .status-line .unlike").removeClass("hidden");
                updatePostLikes(postId);
-       }, function(xmlHttpRequest, textStatus, error) {
+       }, function() {
                /* ignore error. */
        });
 }
 
 function unlikePost(postId) {
-       ajaxGet("unlike.ajax", { "type": "post", "post" : postId, "formPassword": getFormPassword() }, function(data, textStatus) {
+       ajaxGet("unlike.ajax", { "type": "post", "post" : postId, "formPassword": getFormPassword() }, function(data) {
                if ((data == null) || !data.success) {
                        return;
                }
                sone.find(".post#post-" + postId + " > .inner-part > .status-line .unlike").addClass("hidden");
                sone.find(".post#post-" + postId + " > .inner-part > .status-line .like").removeClass("hidden");
                updatePostLikes(postId);
-       }, function(xmlHttpRequest, textStatus, error) {
+       }, function() {
                /* ignore error. */
        });
 }
 
 function updatePostLikes(postId) {
-       ajaxGet("getLikes.ajax", { "type": "post", "post": postId }, function(data, textStatus) {
+       ajaxGet("getLikes.ajax", { "type": "post", "post": postId }, function(data) {
                if ((data != null) && data.success) {
-                       sone.find(".post#post-" + postId + " > .inner-part > .status-line .likes").toggleClass("hidden", data.likes == 0);
+                       sone.find(".post#post-" + postId + " > .inner-part > .status-line .likes").toggleClass("hidden", data.likes === 0);
                        sone.find(".post#post-" + postId + " > .inner-part > .status-line .likes span.like-count").text(data.likes);
-                       sone.find(".post#post-" + postId + " > .inner-part > .status-line .likes > span").attr("title", generateSoneList(data.sones));
+                       sone.find(".post#post-" + postId + " > .inner-part > .status-line .likes > span").prop("title", generateSoneList(data.sones));
                }
-       }, function(xmlHttpRequest, textStatus, error) {
+       }, function() {
                /* ignore error. */
        });
 }
 
 function likeReply(replyId) {
-       ajaxGet("like.ajax", { "type": "reply", "reply" : replyId, "formPassword": getFormPassword() }, function(data, textStatus) {
+       ajaxGet("like.ajax", { "type": "reply", "reply" : replyId, "formPassword": getFormPassword() }, function(data) {
                if ((data == null) || !data.success) {
                        return;
                }
                sone.find(".reply#reply-" + replyId + " .status-line .like").addClass("hidden");
                sone.find(".reply#reply-" + replyId + " .status-line .unlike").removeClass("hidden");
                updateReplyLikes(replyId);
-       }, function(xmlHttpRequest, textStatus, error) {
+       }, function() {
                /* ignore error. */
        });
 }
 
 function unlikeReply(replyId) {
-       ajaxGet("unlike.ajax", { "type": "reply", "reply" : replyId, "formPassword": getFormPassword() }, function(data, textStatus) {
+       ajaxGet("unlike.ajax", { "type": "reply", "reply" : replyId, "formPassword": getFormPassword() }, function(data) {
                if ((data == null) || !data.success) {
                        return;
                }
                sone.find(".reply#reply-" + replyId + " .status-line .unlike").addClass("hidden");
                sone.find(".reply#reply-" + replyId + " .status-line .like").removeClass("hidden");
                updateReplyLikes(replyId);
-       }, function(xmlHttpRequest, textStatus, error) {
+       }, function() {
                /* ignore error. */
        });
 }
 
 /**
- * Trusts the Sone with the given ID.
- *
- * @param soneId
- *            The ID of the Sone to trust
- */
-function trustSone(soneId) {
-       ajaxGet("trustSone.ajax", { "formPassword" : getFormPassword(), "sone" : soneId }, function(data, textStatus) {
-               if ((data != null) && data.success) {
-                       updateTrustControls(soneId, data.trustValue);
-               }
-       });
-}
-
-/**
- * Distrusts the Sone with the given ID, i.e. assigns a negative trust value.
- *
- * @param soneId
- *            The ID of the Sone to distrust
- */
-function distrustSone(soneId) {
-       ajaxGet("distrustSone.ajax", { "formPassword" : getFormPassword(), "sone" : soneId }, function(data, textStatus) {
-               if ((data != null) && data.success) {
-                       updateTrustControls(soneId, data.trustValue);
-               }
-       });
-}
-
-/**
- * Untrusts the Sone with the given ID, i.e. removes any trust assignment.
- *
- * @param soneId
- *            The ID of the Sone to untrust
- */
-function untrustSone(soneId) {
-       ajaxGet("untrustSone.ajax", { "formPassword" : getFormPassword(), "sone" : soneId }, function(data, textStatus) {
-               if ((data != null) && data.success) {
-                       updateTrustControls(soneId, data.trustValue);
-               }
-       });
-}
-
-/**
- * Updates the trust controls for all posts and replies of the given Sone,
- * according to the given trust value.
- *
- * @param soneId
- *            The ID of the Sone to update all trust controls for
- * @param trustValue
- *            The trust value for the Sone
- */
-function updateTrustControls(soneId, trustValue) {
-       sone.find(".post").each(function() {
-               if (getPostAuthor(this) == soneId) {
-                       getPostElement(this).find(".post-trust").toggleClass("hidden", trustValue != null);
-                       getPostElement(this).find(".post-distrust").toggleClass("hidden", trustValue != null);
-                       getPostElement(this).find(".post-untrust").toggleClass("hidden", trustValue == null);
-               }
-       });
-       sone.find(".reply").each(function() {
-               if (getReplyAuthor(this) == soneId) {
-                       getReplyElement(this).find(".reply-trust").toggleClass("hidden", trustValue != null);
-                       getReplyElement(this).find(".reply-distrust").toggleClass("hidden", trustValue != null);
-                       getReplyElement(this).find(".reply-untrust").toggleClass("hidden", trustValue == null);
-               }
-       });
-}
-
-/**
  * Bookmarks the post with the given ID.
  *
  * @param postId
@@ -577,7 +502,7 @@ function updateTrustControls(soneId, trustValue) {
  */
 function bookmarkPost(postId) {
        (function(postId) {
-               ajaxGet("bookmark.ajax", {"formPassword": getFormPassword(), "type": "post", "post": postId}, function(data, textStatus) {
+               ajaxGet("bookmark.ajax", {"formPassword": getFormPassword(), "type": "post", "post": postId}, function(data) {
                        if ((data != null) && data.success) {
                                getPost(postId).find(".bookmark").toggleClass("hidden", true);
                                getPost(postId).find(".unbookmark").toggleClass("hidden", false);
@@ -593,7 +518,7 @@ function bookmarkPost(postId) {
  *            The ID of the post to unbookmark
  */
 function unbookmarkPost(postId) {
-       ajaxGet("unbookmark.ajax", {"formPassword": getFormPassword(), "type": "post", "post": postId}, function(data, textStatus) {
+       ajaxGet("unbookmark.ajax", {"formPassword": getFormPassword(), "type": "post", "post": postId}, function(data) {
                if ((data != null) && data.success) {
                        getPost(postId).find(".bookmark").toggleClass("hidden", false);
                        getPost(postId).find(".unbookmark").toggleClass("hidden", true);
@@ -602,13 +527,13 @@ function unbookmarkPost(postId) {
 }
 
 function updateReplyLikes(replyId) {
-       ajaxGet("getLikes.ajax", { "type": "reply", "reply": replyId }, function(data, textStatus) {
+       ajaxGet("getLikes.ajax", { "type": "reply", "reply": replyId }, function(data) {
                if ((data != null) && data.success) {
-                       sone.find(".reply#reply-" + replyId + " .status-line .likes").toggleClass("hidden", data.likes == 0);
+                       sone.find(".reply#reply-" + replyId + " .status-line .likes").toggleClass("hidden", data.likes === 0);
                        sone.find(".reply#reply-" + replyId + " .status-line .likes span.like-count").text(data.likes);
-                       sone.find(".reply#reply-" + replyId + " .status-line .likes > span").attr("title", generateSoneList(data.sones));
+                       sone.find(".reply#reply-" + replyId + " .status-line .likes > span").prop("title", generateSoneList(data.sones));
                }
-       }, function(xmlHttpRequest, textStatus, error) {
+       }, function() {
                /* ignore error. */
        });
 }
@@ -627,7 +552,7 @@ function updateReplyLikes(replyId) {
  *            parameters: success, error, replyId)
  */
 function postReply(sender, postId, text, callbackFunction) {
-       ajaxGet("createReply.ajax", { "formPassword" : getFormPassword(), "sender": sender, "post" : postId, "text": text }, function(data, textStatus) {
+       ajaxGet("createReply.ajax", { "formPassword" : getFormPassword(), "sender": sender, "post" : postId, "text": text }, function(data) {
                if (data == null) {
                        /* TODO - show error */
                        return;
@@ -637,7 +562,7 @@ function postReply(sender, postId, text, callbackFunction) {
                } else {
                        callbackFunction(false, data.error);
                }
-       }, function(xmlHttpRequest, textStatus, error) {
+       }, function() {
                /* ignore error. */
        });
 }
@@ -654,7 +579,7 @@ function ajaxifySone(soneElement) {
         * nicer.
         */
        $(".follow", soneElement).submit(function() {
-               var followElement = this;
+               const followElement = this;
                ajaxGet("followSone.ajax", { "sone": getSoneId(this), "formPassword": getFormPassword() }, function() {
                        $(followElement).addClass("hidden");
                        $(followElement).parent().find(".unfollow").removeClass("hidden");
@@ -662,7 +587,7 @@ function ajaxifySone(soneElement) {
                return false;
        });
        $(".unfollow", soneElement).submit(function() {
-               var unfollowElement = this;
+               const unfollowElement = this;
                ajaxGet("unfollowSone.ajax", { "sone": getSoneId(this), "formPassword": getFormPassword() }, function() {
                        $(unfollowElement).addClass("hidden");
                        $(unfollowElement).parent().find(".follow").removeClass("hidden");
@@ -670,7 +595,7 @@ function ajaxifySone(soneElement) {
                return false;
        });
        $(".lock", soneElement).submit(function() {
-               var lockElement = this;
+               const lockElement = this;
                ajaxGet("lockSone.ajax", { "sone" : getSoneId(this), "formPassword" : getFormPassword() }, function() {
                        $(lockElement).addClass("hidden");
                        $(lockElement).parent().find(".unlock").removeClass("hidden");
@@ -678,7 +603,7 @@ function ajaxifySone(soneElement) {
                return false;
        });
        $(".unlock", soneElement).submit(function() {
-               var unlockElement = this;
+               const unlockElement = this;
                ajaxGet("unlockSone.ajax", { "sone" : getSoneId(this), "formPassword" : getFormPassword() }, function() {
                        $(unlockElement).addClass("hidden");
                        $(unlockElement).parent().find(".lock").removeClass("hidden");
@@ -692,6 +617,40 @@ function ajaxifySone(soneElement) {
        });
 }
 
+function followSone(soneId) {
+       return function() {
+               const followElement = this;
+               ajaxGet("followSone.ajax", {"sone": soneId, "formPassword": getFormPassword()}, function () {
+                       $(followElement).addClass("hidden");
+                       $(followElement).parent().find(".unfollow").removeClass("hidden");
+                       sone.find(".sone-menu").each(function () {
+                               if (getMenuSone(this) === soneId) {
+                                       $(".follow", this).toggleClass("hidden", true);
+                                       $(".unfollow", this).toggleClass("hidden", false);
+                               }
+                       });
+               });
+               return false;
+       }
+}
+
+function unfollowSone(soneId) {
+       return function() {
+               const unfollowElement = this;
+               ajaxGet("unfollowSone.ajax", {"sone": soneId, "formPassword": getFormPassword()}, function () {
+                       $(unfollowElement).addClass("hidden");
+                       $(unfollowElement).parent().find(".follow").removeClass("hidden");
+                       sone.find(".sone-menu").each(function () {
+                               if (getMenuSone(this) === soneId) {
+                                       $(".follow", this).toggleClass("hidden", false);
+                                       $(".unfollow", this).toggleClass("hidden", true);
+                               }
+                       });
+               });
+               return false;
+       }
+};
+
 /**
  * Ajaxifies the given post by enhancing all eligible elements with AJAX.
  *
@@ -703,12 +662,12 @@ function ajaxifyPost(postElement) {
                return false;
        });
        $(postElement).find(".create-reply button:submit").click(function() {
-               var button = $(this);
-               button.attr("disabled", "disabled");
-               var sender = $(this.form).find(":input[name=sender]").val();
-               var inputField = $(this.form).find(":input[name=text]:enabled").get(0);
-               var postId = getPostId(this);
-               var text = $(inputField).val();
+               const button = $(this);
+               button.prop("disabled", "disabled");
+               const sender = $(this.form).find(":input[name=sender]").val();
+               const inputField = $(this.form).find(":input[name=text]:enabled").get(0);
+               const postId = getPostId(this);
+               const text = $(inputField).val();
                (function(sender, postId, text, inputField) {
                        postReply(sender, postId, text, function(success, error, replyId, soneId) {
                                if (success) {
@@ -731,7 +690,7 @@ function ajaxifyPost(postElement) {
        /* replace all “delete” buttons with javascript. */
        (function(postElement) {
                getTranslation("WebInterface.Confirmation.DeletePostButton", function(deletePostText) {
-                       var postId = getPostId(postElement);
+                       const postId = getPostId(postElement);
                        enhanceDeletePostButton($(postElement).find(".delete-post button"), postId, deletePostText);
                });
        })(postElement);
@@ -746,20 +705,6 @@ function ajaxifyPost(postElement) {
                return false;
        });
 
-       /* convert trust control buttons to javascript functions. */
-       $(postElement).find(".post-trust").submit(function() {
-               trustSone(getPostAuthor(this));
-               return false;
-       });
-       $(postElement).find(".post-distrust").submit(function() {
-               distrustSone(getPostAuthor(this));
-               return false;
-       });
-       $(postElement).find(".post-untrust").submit(function() {
-               untrustSone(getPostAuthor(this));
-               return false;
-       });
-
        /* convert bookmark/unbookmark buttons to javascript functions. */
        $(postElement).find(".bookmark").submit(function() {
                bookmarkPost(getPostId(this));
@@ -773,8 +718,8 @@ function ajaxifyPost(postElement) {
        /* convert “show source” link into javascript function. */
        $(postElement).find(".show-source").each(function() {
                $("a", this).click(function() {
-                       var post = getPostElement(this);
-                       var rawPostText = $(".post-text.raw-text", post);
+                       const post = getPostElement(this);
+                       const rawPostText = $(".post-text.raw-text", post);
                        rawPostText.toggleClass("hidden");
                        if (rawPostText.hasClass("hidden")) {
                                $(".post-text.short-text", post).removeClass("hidden");
@@ -792,16 +737,7 @@ function ajaxifyPost(postElement) {
        });
 
        /* convert “show more” link into javascript function. */
-       $(postElement).find(".expand-post-text").each(function() {
-               $(this).click(function() {
-                       $(".post-text.text", getPostElement(this)).toggleClass("hidden");
-                       $(".post-text.short-text", getPostElement(this)).toggleClass("hidden");
-                       $(".expand-post-text", getPostElement(this)).toggleClass("hidden");
-                       $(".shrink-post-text", getPostElement(this)).toggleClass("hidden");
-                       return false;
-               });
-       });
-       $(postElement).find(".shrink-post-text").each(function() {
+       const toggleShowMore = function() {
                $(this).click(function() {
                        $(".post-text.text", getPostElement(this)).toggleClass("hidden");
                        $(".post-text.short-text", getPostElement(this)).toggleClass("hidden");
@@ -809,13 +745,15 @@ function ajaxifyPost(postElement) {
                        $(".shrink-post-text", getPostElement(this)).toggleClass("hidden");
                        return false;
                });
-       });
+       };
+       $(postElement).find(".expand-post-text").each(toggleShowMore);
+       $(postElement).find(".shrink-post-text").each(toggleShowMore);
 
        /* ajaxify author/post links */
        $(".post-status-line .permalink a", postElement).click(function() {
                if (!$(".create-reply", postElement).hasClass("hidden")) {
-                       var textArea = $(":input.reply-input", postElement).focus().data("textarea");
-                       $(textArea).replaceSelection($(this).attr("href"));
+                       const textArea = $(":input.reply-input", postElement).focus().data("textarea");
+                       $(textArea).replaceSelection($(this).prop("href"));
                }
                return false;
        });
@@ -824,7 +762,7 @@ function ajaxifyPost(postElement) {
        addCommentLink(getPostId(postElement), getPostAuthor(postElement), postElement, $(postElement).find(".post-status-line .permalink-author"));
 
        /* process all replies. */
-       var replyIds = [];
+       const replyIds = [];
        $(postElement).find(".reply").each(function() {
                replyIds.push(getReplyId(this));
                ajaxifyReply(this);
@@ -862,7 +800,7 @@ function ajaxifyPost(postElement) {
 
        /* show Sone menu when hovering over the avatar. */
        $(postElement).find(".post-avatar").mouseover(function() {
-               if (typeof currentSoneMenuTimeoutHandler != undefined) {
+               if (typeof currentSoneMenuTimeoutHandler !== undefined) {
                        clearTimeout(currentSoneMenuTimeoutHandler);
                }
                currentSoneMenuId = getPostId(this);
@@ -873,40 +811,14 @@ function ajaxifyPost(postElement) {
                        }).fadeIn();
                }, 1000);
        }).mouseleave(function() {
-               if (currentSoneMenuId == getPostId(this)) {
+               if (currentSoneMenuId === getPostId(this)) {
                        clearTimeout(currentSoneMenuTimeoutHandler);
                }
        });
        (function(postElement) {
-               var soneId = $(".sone-menu-id:first", postElement).text();
-               $(".sone-post-menu .follow", postElement).click(function() {
-                       var followElement = this;
-                       ajaxGet("followSone.ajax", { "sone": soneId, "formPassword": getFormPassword() }, function() {
-                               $(followElement).addClass("hidden");
-                               $(followElement).parent().find(".unfollow").removeClass("hidden");
-                               sone.find(".sone-menu").each(function() {
-                                       if (getMenuSone(this) == soneId) {
-                                               $(".follow", this).toggleClass("hidden", true);
-                                               $(".unfollow", this).toggleClass("hidden", false);
-                                       }
-                               });
-                       });
-                       return false;
-               });
-               $(".sone-post-menu .unfollow", postElement).click(function() {
-                       var unfollowElement = this;
-                       ajaxGet("unfollowSone.ajax", { "sone": soneId, "formPassword": getFormPassword() }, function() {
-                               $(unfollowElement).addClass("hidden");
-                               $(unfollowElement).parent().find(".follow").removeClass("hidden");
-                               sone.find(".sone-menu").each(function() {
-                                       if (getMenuSone(this) == soneId) {
-                                               $(".follow", this).toggleClass("hidden", false);
-                                               $(".unfollow", this).toggleClass("hidden", true);
-                                       }
-                               });
-                       });
-                       return false;
-               });
+               const soneId = $(".sone-menu-id:first", postElement).text();
+               $(".sone-post-menu .follow", postElement).click(followSone(soneId));
+               $(".sone-post-menu .unfollow", postElement).click(unfollowSone(soneId));
        })(postElement);
 }
 
@@ -936,8 +848,8 @@ function ajaxifyReply(replyElement) {
        /* ajaxify author links */
        $(".reply-status-line .permalink a", replyElement).click(function() {
                if (!$(".create-reply", getPostElement(replyElement)).hasClass("hidden")) {
-                       var textArea = $(":input.reply-input", getPostElement(replyElement)).focus().data("textarea");
-                       $(textArea).replaceSelection($(this).attr("href"));
+                       const textArea = $(":input.reply-input", getPostElement(replyElement)).focus().data("textarea");
+                       $(textArea).replaceSelection($(this).prop("href"));
                }
                return false;
        });
@@ -947,8 +859,8 @@ function ajaxifyReply(replyElement) {
        /* convert “show source” link into javascript function. */
        $(replyElement).find(".show-reply-source").each(function() {
                $("a", this).click(function() {
-                       var reply = getReplyElement(this);
-                       var rawReplyText = $(".reply-text.raw-text", reply);
+                       const reply = getReplyElement(this);
+                       const rawReplyText = $(".reply-text.raw-text", reply);
                        rawReplyText.toggleClass("hidden");
                        if (rawReplyText.hasClass("hidden")) {
                                $(".reply-text.short-text", reply).removeClass("hidden");
@@ -966,7 +878,7 @@ function ajaxifyReply(replyElement) {
        });
 
        /* convert “show more” link into javascript function. */
-       $(replyElement).find(".expand-reply-text").each(function() {
+       const toggleShowMore = function() {
                $(this).click(function() {
                        $(".reply-text.text", getReplyElement(this)).toggleClass("hidden");
                        $(".reply-text.short-text", getReplyElement(this)).toggleClass("hidden");
@@ -974,34 +886,13 @@ function ajaxifyReply(replyElement) {
                        $(".shrink-reply-text", getReplyElement(this)).toggleClass("hidden");
                        return false;
                });
-       });
-       $(replyElement).find(".shrink-reply-text").each(function() {
-               $(this).click(function() {
-                       $(".reply-text.text", getReplyElement(this)).toggleClass("hidden");
-                       $(".reply-text.short-text", getReplyElement(this)).toggleClass("hidden");
-                       $(".expand-reply-text", getReplyElement(this)).toggleClass("hidden");
-                       $(".shrink-reply-text", getReplyElement(this)).toggleClass("hidden");
-                       return false;
-               });
-       });
-
-       /* convert trust control buttons to javascript functions. */
-       $(replyElement).find(".reply-trust").submit(function() {
-               trustSone(getReplyAuthor(this));
-               return false;
-       });
-       $(replyElement).find(".reply-distrust").submit(function() {
-               distrustSone(getReplyAuthor(this));
-               return false;
-       });
-       $(replyElement).find(".reply-untrust").submit(function() {
-               untrustSone(getReplyAuthor(this));
-               return false;
-       });
+       };
+       $(replyElement).find(".expand-reply-text").each(toggleShowMore);
+       $(replyElement).find(".shrink-reply-text").each(toggleShowMore);
 
        /* show Sone menu when hovering over the avatar. */
        $(replyElement).find(".reply-avatar").mouseover(function() {
-               if (typeof currentSoneMenuTimeoutHandler != undefined) {
+               if (typeof currentSoneMenuTimeoutHandler !== undefined) {
                        clearTimeout(currentSoneMenuTimeoutHandler);
                }
                currentSoneMenuId = getPostId(this) + "-" + getReplyId(this);
@@ -1012,40 +903,14 @@ function ajaxifyReply(replyElement) {
                        }).fadeIn();
                }, 1000);
        }).mouseleave(function() {
-               if (currentSoneMenuId == getPostId(this) + "-" + getReplyId(this)) {
+               if (currentSoneMenuId === getPostId(this) + "-" + getReplyId(this)) {
                        clearTimeout(currentSoneMenuTimeoutHandler);
                }
        });
        (function(replyElement) {
-               var soneId = $(".sone-menu-id", replyElement).text();
-               $(".sone-menu .follow", replyElement).click(function() {
-                       var followElement = this;
-                       ajaxGet("followSone.ajax", { "sone": soneId, "formPassword": getFormPassword() }, function() {
-                               $(followElement).addClass("hidden");
-                               $(followElement).parent().find(".unfollow").removeClass("hidden");
-                               sone.find(".sone-menu").each(function() {
-                                       if (getMenuSone(this) == soneId) {
-                                               $(".follow", this).toggleClass("hidden", true);
-                                               $(".unfollow", this).toggleClass("hidden", false);
-                                       }
-                               });
-                       });
-                       return false;
-               });
-               $(".sone-menu .unfollow", replyElement).click(function() {
-                       var unfollowElement = this;
-                       ajaxGet("unfollowSone.ajax", { "sone": soneId, "formPassword": getFormPassword() }, function() {
-                               $(unfollowElement).addClass("hidden");
-                               $(unfollowElement).parent().find(".follow").removeClass("hidden");
-                               sone.find(".sone-menu").each(function() {
-                                       if (getMenuSone(this) == soneId) {
-                                               $(".follow", this).toggleClass("hidden", false);
-                                               $(".unfollow", this).toggleClass("hidden", true);
-                                       }
-                               });
-                       });
-                       return false;
-               });
+               const soneId = $(".sone-menu-id", replyElement).text();
+               $(".sone-menu .follow", replyElement).click(followSone(soneId));
+               $(".sone-menu .unfollow", replyElement).click(unfollowSone(soneId));
        })(replyElement);
 }
 
@@ -1065,26 +930,26 @@ function ajaxifyNotification(notification) {
                notification.find(".text").addClass("hidden");
        }
        notification.find("form.mark-as-read button").click(function() {
-               var allIds = $(":input[name=id]", this.form).val().split(" ");
-               for (var index = 0; index < allIds.length; index += 16) {
-                       var ids = allIds.slice(index, index + 16).join(" ");
+               const allIds = $(":input[name=id]", this.form).val().split(" ");
+               for (let index = 0; index < allIds.length; index += 16) {
+                       const ids = allIds.slice(index, index + 16).join(" ");
                        ajaxGet("markAsKnown.ajax", {"formPassword": getFormPassword(), "type": $(":input[name=type]", this.form).val(), "id": ids});
                }
        });
        notification.find("a[class^='link-']").each(function() {
-               var linkElement = $(this);
+               const linkElement = $(this);
                if (linkElement.is("[href^='viewPost']")) {
-                       var id = linkElement.attr("class").substr(5);
+                       const id = linkElement.prop("class").substr(5);
                        if (hasPost(id)) {
-                               linkElement.attr("href", "#post-" + id).addClass("in-page-link");
+                               linkElement.prop("href", "#post-" + id).addClass("in-page-link");
                        }
                }
        });
        notification.find("form.dismiss button").click(function() {
-               ajaxGet("dismissNotification.ajax", { "formPassword" : getFormPassword(), "notification" : notification.attr("id") }, function(data, textStatus) {
+               ajaxGet("dismissNotification.ajax", { "formPassword" : getFormPassword(), "notification" : notification.prop("id") }, function() {
                        /* dismiss in case of error, too. */
                        notification.slideUp();
-               }, function(xmlHttpRequest, textStatus, error) {
+               }, function() {
                        /* ignore error. */
                });
        });
@@ -1119,7 +984,7 @@ function setNotificationHash(notificationHash) {
  * @returns All extracted IDs
  */
 function getElementIds(notification, selector) {
-       var elementIds = [];
+       const elementIds = [];
        $(selector, notification).each(function() {
                elementIds.push($(this).text());
        });
@@ -1136,13 +1001,13 @@ function getElementIds(notification, selector) {
  *            The new notification element
  */
 function checkForRemovedSones(oldNotification, newNotification) {
-       if (getNotificationId(oldNotification) != "new-sone-notification") {
+       if (getNotificationId(oldNotification) !== "new-sone-notification") {
                return;
        }
-       var oldIds = getElementIds(oldNotification, ".new-sone-id");
-       var newIds = getElementIds(newNotification, ".new-sone-id");
+       const oldIds = getElementIds(oldNotification, ".new-sone-id");
+       const newIds = getElementIds(newNotification, ".new-sone-id");
        $.each(oldIds, function(index, value) {
-               if ($.inArray(value, newIds) == -1) {
+               if ($.inArray(value, newIds) === -1) {
                        markSoneAsKnown(getSone(value), true);
                }
        });
@@ -1158,13 +1023,13 @@ function checkForRemovedSones(oldNotification, newNotification) {
  *            The new notification element
  */
 function checkForRemovedPosts(oldNotification, newNotification) {
-       if (getNotificationId(oldNotification) != "new-post-notification") {
+       if (getNotificationId(oldNotification) !== "new-post-notification") {
                return;
        }
-       var oldIds = getElementIds(oldNotification, ".post-id");
-       var newIds = getElementIds(newNotification, ".post-id");
+       const oldIds = getElementIds(oldNotification, ".post-id");
+       const newIds = getElementIds(newNotification, ".post-id");
        $.each(oldIds, function(index, value) {
-               if ($.inArray(value, newIds) == -1) {
+               if ($.inArray(value, newIds) === -1) {
                        markPostAsKnown(getPost(value), true);
                }
        });
@@ -1181,26 +1046,26 @@ function checkForRemovedPosts(oldNotification, newNotification) {
  *            The new notification element
  */
 function checkForRemovedReplies(oldNotification, newNotification) {
-       if (getNotificationId(oldNotification) != "new-reply-notification") {
+       if (getNotificationId(oldNotification) !== "new-reply-notification") {
                return;
        }
-       var oldIds = getElementIds(oldNotification, ".reply-id");
-       var newIds = getElementIds(newNotification, ".reply-id");
+       const oldIds = getElementIds(oldNotification, ".reply-id");
+       const newIds = getElementIds(newNotification, ".reply-id");
        $.each(oldIds, function(index, value) {
-               if ($.inArray(value, newIds) == -1) {
+               if ($.inArray(value, newIds) === -1) {
                        markReplyAsKnown(getReply(value), true);
                }
        });
 }
 
 function getStatus() {
-       var parameters = isViewSonePage() ? {"soneIds": getShownSoneId() } : isKnownSonesPage() ? {"soneIds": getShownSoneIds() } : {};
+       const parameters = isViewSonePage() ? {"soneIds": getShownSoneId()} : isKnownSonesPage() ? {"soneIds": getShownSoneIds()} : {};
        $.extend(parameters, {
                "elements": JSON.stringify($(".linked-element.not-loaded").map(function () {
-                       return $(this).attr("title");
+                       return $(this).prop("title");
                }).toArray())
        });
-       ajaxGet("getStatus.ajax", parameters, function(data, textStatus) {
+       ajaxGet("getStatus.ajax", parameters, function(data) {
                if ((data != null) && data.success) {
                        /* process Sone information. */
                        $.each(data.sones, function(index, value) {
@@ -1210,7 +1075,7 @@ function getStatus() {
                        if (!notLoggedIn) {
                                showOfflineMarker(!online);
                        }
-                       if (data.notificationHash != getNotificationHash()) {
+                       if (data.notificationHash !== getNotificationHash()) {
                                console.log("Old hash: ", getNotificationHash(), ", new hash: ", data.notificationHash);
                                requestNotifications();
                                /* process new posts. */
@@ -1219,7 +1084,7 @@ function getStatus() {
                                });
                                /* process new replies. */
                                $.each(data.newReplies, function(index, value) {
-                                       loadNewReply(value.id, value.sone, value.post, value.postSone);
+                                       loadNewReply(value.id, value.sone, value.post);
                                });
                        }
                        if (data.linkedElements) {
@@ -1238,39 +1103,39 @@ function getStatus() {
 }
 
 function requestNotifications() {
-       ajaxGet("getNotifications.ajax", {}, function(data, textStatus) {
+       ajaxGet("getNotifications.ajax", {}, function(data) {
                if (data && data.success) {
                        /* search for removed notifications. */
                        sone.find("#notification-area .notification").each(function() {
-                               var notificationId = $(this).attr("id");
-                               var foundNotification = false;
+                               const notificationId = $(this).prop("id");
+                               let foundNotification = false;
                                $.each(data.notifications, function(index, value) {
-                                       if (value.id == notificationId) {
+                                       if (value.id === notificationId) {
                                                foundNotification = true;
                                                return false;
                                        }
                                });
                                if (!foundNotification) {
-                                       if (notificationId == "new-sone-notification" && (data.options["ShowNotification/NewSones"] == true)) {
-                                               $(".new-sone-id", this).each(function(index, element) {
-                                                       var soneId = $(this).text();
+                                       if (notificationId === "new-sone-notification" && (data.options["ShowNotification/NewSones"] === true)) {
+                                               $(".new-sone-id", this).each(function() {
+                                                       const soneId = $(this).text();
                                                        markSoneAsKnown(getSone(soneId), true);
                                                });
-                                       } else if (notificationId == "new-post-notification" && (data.options["ShowNotification/NewPosts"] == true)) {
-                                               $(".post-id", this).each(function(index, element) {
-                                                       var postId = $(this).text();
+                                       } else if (notificationId === "new-post-notification" && (data.options["ShowNotification/NewPosts"] === true)) {
+                                               $(".post-id", this).each(function() {
+                                                       const postId = $(this).text();
                                                        markPostAsKnown(getPost(postId), true);
                                                });
-                                       } else if (notificationId == "new-reply-notification" && (data.options["ShowNotification/NewReplies"] == true)) {
-                                               $(".reply-id", this).each(function(index, element) {
-                                                       var replyId = $(this).text();
+                                       } else if (notificationId === "new-reply-notification" && (data.options["ShowNotification/NewReplies"] === true)) {
+                                               $(".reply-id", this).each(function() {
+                                                       const replyId = $(this).text();
                                                        markReplyAsKnown(getReply(replyId), true);
                                                });
                                        }
                                        $(this).slideUp("normal", function() {
                                                $(this).remove();
                                                /* remove activity when no notifications are visible. */
-                                               if (sone.find("#notification-area .notification").length == 0) {
+                                               if (sone.find("#notification-area .notification").length === 0) {
                                                        resetActivity();
                                                }
                                        });
@@ -1278,11 +1143,11 @@ function requestNotifications() {
                        });
                        /* process notifications. */
                        $.each(data.notifications, function(index, value) {
-                               var oldNotification = getNotification(value.id);
-                               var notification = ajaxifyNotification(createNotification(value.id, value.lastUpdatedTime, value.text, value.dismissable)).hide();
-                               if (oldNotification.length != 0) {
+                               const oldNotification = getNotification(value.id);
+                               const notification = ajaxifyNotification(createNotification(value.id, value.lastUpdatedTime, value.text, value.dismissable)).hide();
+                               if (oldNotification.length !== 0) {
                                        if ((oldNotification.find(".short-text").length > 0) && (notification.find(".short-text").length > 0)) {
-                                               var opened = oldNotification.is(":visible") && oldNotification.find(".short-text").hasClass("hidden");
+                                               const opened = oldNotification.is(":visible") && oldNotification.find(".short-text").hasClass("hidden");
                                                notification.find(".short-text").toggleClass("hidden", opened);
                                                notification.find(".text").toggleClass("hidden", !opened);
                                        }
@@ -1292,7 +1157,7 @@ function requestNotifications() {
                                        oldNotification.replaceWith(notification.show());
                                } else {
                                        sone.find("#notification-area").append(notification);
-                                       if (value.id.substring(0, 5) != "local") {
+                                       if (value.id.substring(0, 5) !== "local") {
                                                notification.slideDown();
                                                setActivity();
                                        }
@@ -1316,7 +1181,7 @@ function getCurrentSoneId() {
 /**
  * Returns the content of the page-id attribute.
  *
- * @returns The page ID
+ * @returns String The page ID
  */
 function getPageId() {
        return sone.find(".page-id").text();
@@ -1329,7 +1194,7 @@ function getPageId() {
  *          <code>false</code> otherwise
  */
 function isIndexPage() {
-       return getPageId() == "index";
+       return getPageId() === "index";
 }
 
 /**
@@ -1341,7 +1206,7 @@ function isIndexPage() {
  * @returns The current page of the pagination
  */
 function getPage(paginationSelector) {
-       var pagination = $(paginationSelector);
+       const pagination = $(paginationSelector);
        if (pagination.length > 0) {
                return $(".current-page", paginationSelector).text();
        }
@@ -1355,7 +1220,7 @@ function getPage(paginationSelector) {
  *          page, <code>false</code> otherwise
  */
 function isViewSonePage() {
-       return getPageId() == "view-sone";
+       return getPageId() === "view-sone";
 }
 
 /**
@@ -1375,7 +1240,7 @@ function getShownSoneId() {
  * @returns The ID of the currently shown Sones
  */
 function getShownSoneIds() {
-       var soneIds = [];
+       const soneIds = [];
        sone.find("#known-sones .sone .id").each(function() {
                soneIds.push($(this).text());
        });
@@ -1389,7 +1254,7 @@ function getShownSoneIds() {
  *          page, <code>false</code> otherwise
  */
 function isViewPostPage() {
-       return getPageId() == "view-post";
+       return getPageId() === "view-post";
 }
 
 /**
@@ -1409,7 +1274,7 @@ function getShownPostId() {
  *          Sones” page, <code>false</code> otherwise
  */
 function isKnownSonesPage() {
-       return getPageId() == "known-sones";
+       return getPageId() === "known-sones";
 }
 
 /**
@@ -1441,8 +1306,8 @@ function loadNewPost(postId, soneId, recipientId, time) {
                return;
        }
        if (!isIndexPage() || (getPage(".pagination-index") > 1)) {
-               if (!isViewPostPage() || (getShownPostId() != postId)) {
-                       if (!isViewSonePage() || ((getShownSoneId() != soneId) && (getShownSoneId() != recipientId)) || (getPage(".post-navigation") > 1)) {
+               if (!isViewPostPage() || (getShownPostId() !== postId)) {
+                       if (!isViewSonePage() || ((getShownSoneId() !== soneId) && (getShownSoneId() !== recipientId)) || (getPage(".post-navigation") > 1)) {
                                return;
                        }
                }
@@ -1450,23 +1315,23 @@ function loadNewPost(postId, soneId, recipientId, time) {
        if (getPostTime(sone.find(".post").last()) > time) {
                return;
        }
-       ajaxGet("getPost.ajax", { "post" : postId }, function(data, textStatus) {
+       ajaxGet("getPost.ajax", { "post" : postId }, function(data) {
                if ((data != null) && data.success) {
                        if (hasPost(data.post.id)) {
                                return;
                        }
-                       if ((!isIndexPage() || (getPage(".pagination-index") > 1)) && !(isViewSonePage() && ((getShownSoneId() == data.post.sone) || (getShownSoneId() == data.post.recipient) || (getPage(".post-navigation") > 1)))) {
+                       if ((!isIndexPage() || (getPage(".pagination-index") > 1)) && !(isViewSonePage() && ((getShownSoneId() === data.post.sone) || (getShownSoneId() === data.post.recipient) || (getPage(".post-navigation") > 1)))) {
                                return;
                        }
-                       var firstOlderPost = null;
+                       let firstOlderPost = null;
                        sone.find(".post").each(function() {
                                if (getPostTime(this) < data.post.time) {
                                        firstOlderPost = $(this);
                                        return false;
                                }
                        });
-                       var newPost = $(data.post.html).addClass("hidden");
-                       if ($(".post-author-local", newPost).text() == "true") {
+                       const newPost = $(data.post.html).addClass("hidden");
+                       if ($(".post-author-local", newPost).text() === "true") {
                                newPost.removeClass("new");
                        }
                        if (firstOlderPost != null) {
@@ -1480,29 +1345,29 @@ function loadNewPost(postId, soneId, recipientId, time) {
        });
 }
 
-function loadNewReply(replyId, soneId, postId, postSoneId) {
+function loadNewReply(replyId, soneId, postId) {
        if (hasReply(replyId)) {
                return;
        }
        if (!hasPost(postId)) {
                return;
        }
-       ajaxGet("getReply.ajax", { "reply": replyId }, function(data, textStatus) {
+       ajaxGet("getReply.ajax", { "reply": replyId }, function(data) {
                /* find post. */
                if ((data != null) && data.success) {
                        if (hasReply(data.reply.id)) {
                                return;
                        }
                        sone.find(".post#post-" + data.reply.postId).each(function() {
-                               var firstNewerReply = null;
+                               let firstNewerReply = null;
                                $(this).find(".replies .reply").each(function() {
                                        if (getReplyTime(this) > data.reply.time) {
                                                firstNewerReply = $(this);
                                                return false;
                                        }
                                });
-                               var newReply = $(data.reply.html).addClass("hidden");
-                               if ($(".reply-author-local", newReply).text() == "true") {
+                               const newReply = $(data.reply.html).addClass("hidden");
+                               if ($(".reply-author-local", newReply).text() === "true") {
                                        newReply.removeClass("new");
                                        (function(newReply) {
                                                setTimeout(function() {
@@ -1530,7 +1395,7 @@ function loadNewReply(replyId, soneId, postId, postSoneId) {
 }
 
 function loadLinkedElements(links) {
-       var failedElements = links.filter(function(element) {
+       const failedElements = links.filter(function(element) {
                return element.failed;
        });
        if (failedElements.length > 0) {
@@ -1540,7 +1405,7 @@ function loadLinkedElements(links) {
                        });
                });
        }
-       var loadedElements = links.filter(function(element) {
+       const loadedElements = links.filter(function(element) {
                return !element.loading && !element.failed;
        });
        if (loadedElements.length > 0) {
@@ -1548,7 +1413,7 @@ function loadLinkedElements(links) {
                        "elements": JSON.stringify(loadedElements.map(function(element) {
                                return element.link;
                        }))
-               }, function (data, textStatus) {
+               }, function (data) {
                        if ((data != null) && (data.success)) {
                                data.linkedElements.forEach(function (linkedElement) {
                                        getLinkedElements(linkedElement.link).each(function() {
@@ -1585,7 +1450,7 @@ function markSoneAsKnown(soneElement, skipRequest) {
 
 function markPostAsKnown(postElements, skipRequest) {
        $(postElements).each(function() {
-               var postElement = this;
+               const postElement = this;
                if ($(postElement).hasClass("new") || ((typeof skipRequest != "undefined"))) {
                        (function(postElement) {
                                $(postElement).removeClass("new");
@@ -1602,7 +1467,7 @@ function markPostAsKnown(postElements, skipRequest) {
 
 function markReplyAsKnown(replyElements, skipRequest) {
        $(replyElements).each(function() {
-               var replyElement = this;
+               const replyElement = this;
                if ($(replyElement).hasClass("new") || ((typeof skipRequest != "undefined"))) {
                        (function(replyElement) {
                                $(replyElement).removeClass("new");
@@ -1631,7 +1496,7 @@ function updatePostTime(postId, timeText, refreshTime, tooltip) {
        if (!getPost(postId).is(":visible")) {
                return;
        }
-       getPost(postId).find(".post-status-line > .time a").html(timeText).attr("title", tooltip);
+       getPost(postId).find(".post-status-line > .time a").html(timeText).prop("title", tooltip);
        (function(postId, refreshTime) {
                setTimeout(function() {
                        updatePostTimes(postId);
@@ -1646,8 +1511,8 @@ function updatePostTime(postId, timeText, refreshTime, tooltip) {
  *            Comma-separated post IDs
  */
 function updatePostTimes(postIds) {
-       if (postIds != "") {
-        ajaxGet("getTimes.ajax", {"posts": postIds}, function (data, textStatus) {
+       if (postIds !== "") {
+        ajaxGet("getTimes.ajax", {"posts": postIds}, function (data) {
             if ((data != null) && data.success) {
                 $.each(data.postTimes, function (index, value) {
                     updatePostTime(index, value.timeText, value.refreshTime, value.tooltip);
@@ -1660,7 +1525,7 @@ function updatePostTimes(postIds) {
 /**
  * Updates the time of the reply with the given ID.
  *
- * @param postId
+ * @param replyId
  *            The ID of the reply to update
  * @param timeText
  *            The text of the time to show
@@ -1670,7 +1535,7 @@ function updatePostTimes(postIds) {
  *            The tooltip to show
  */
 function updateReplyTime(replyId, timeText, refreshTime, tooltip) {
-       getReply(replyId).find(".reply-status-line > .time").html(timeText).attr("title", tooltip);
+       getReply(replyId).find(".reply-status-line > .time").html(timeText).prop("title", tooltip);
        (function(replyId, refreshTime) {
                setTimeout(function() {
                        updateReplyTimes(replyId);
@@ -1681,12 +1546,12 @@ function updateReplyTime(replyId, timeText, refreshTime, tooltip) {
 /**
  * Requests new rendered times for the posts with the given IDs.
  *
- * @param postIds
+ * @param replyIds
  *            Comma-separated post IDs
  */
 function updateReplyTimes(replyIds) {
-       if (replyIds != "") {
-        ajaxGet("getTimes.ajax", {"replies": replyIds}, function (data, textStatus) {
+       if (replyIds !== "") {
+        ajaxGet("getTimes.ajax", {"replies": replyIds}, function (data) {
             if ((data != null) && data.success) {
                 $.each(data.replyTimes, function (index, value) {
                     updateReplyTime(index, value.timeText, value.refreshTime, value.tooltip);
@@ -1697,8 +1562,8 @@ function updateReplyTimes(replyIds) {
 }
 
 function resetActivity() {
-       var title = document.title;
-       if (title.indexOf('(') == 0) {
+       const title = document.title;
+       if (title.indexOf('(') === 0) {
                setTitle(title.substr(title.indexOf(' ') + 1));
        }
        iconBlinking = false;
@@ -1706,8 +1571,8 @@ function resetActivity() {
 
 function setActivity() {
        if (!focus) {
-               var title = document.title;
-               if (title.indexOf('(') != 0) {
+               const title = document.title;
+               if (title.indexOf('(') !== 0) {
                        setTitle("(!) " + title);
                }
                if (!iconBlinking) {
@@ -1730,10 +1595,10 @@ function setTitle(title) {
 }
 
 /** Whether the icon is currently showing activity. */
-var iconActive = false;
+let iconActive = false;
 
 /** Whether the icon is currently supposed to blink. */
-var iconBlinking = false;
+let iconBlinking = false;
 
 /**
  * Toggles the icon. If the window has gained focus and the icon is still
@@ -1761,7 +1626,7 @@ function toggleIcon() {
  */
 function changeIcon(iconUrl) {
        $("link[rel=icon]").remove();
-       $("head").append($("<link>").attr("rel", "icon").attr("type", "image/png").attr("href", iconUrl));
+       $("head").append($("<link>").prop("rel", "icon").prop("type", "image/png").prop("href", iconUrl));
        $("iframe[id=icon-update]")[0].src += "";
 }
 
@@ -1777,9 +1642,9 @@ function changeIcon(iconUrl) {
  *            user
  */
 function createNotification(id, lastUpdatedTime, text, dismissable) {
-       var notification = $("<div></div>").addClass("notification").attr("id", id).attr("lastUpdatedTime", lastUpdatedTime);
+       const notification = $("<div></div>").addClass("notification").prop("id", id).prop("lastUpdatedTime", lastUpdatedTime);
        if (dismissable) {
-               var dismissForm = sone.find("#notification-area #notification-dismiss-template").clone().removeClass("hidden").removeAttr("id");
+               const dismissForm = sone.find("#notification-area #notification-dismiss-template").clone().removeClass("hidden").removeAttr("id");
                dismissForm.find("input[name=notification]").val(id);
                notification.append(dismissForm);
        }
@@ -1805,7 +1670,7 @@ function showNotificationDetails(notificationId) {
  *            The ID of the field to delete
  */
 function deleteProfileField(fieldId) {
-       ajaxGet("deleteProfileField.ajax", {"formPassword": getFormPassword(), "field": fieldId}, function(data, textStatus) {
+       ajaxGet("deleteProfileField.ajax", {"formPassword": getFormPassword(), "field": fieldId}, function(data) {
                if (data && data.success) {
                        sone.find(".profile-field#" + data.field.id).slideUp();
                }
@@ -1823,7 +1688,7 @@ function deleteProfileField(fieldId) {
  *            Called when the renaming was successful
  */
 function editProfileField(fieldId, newName, successFunction) {
-       ajaxGet("editProfileField.ajax", {"formPassword": getFormPassword(), "field": fieldId, "name": newName}, function(data, textStatus) {
+       ajaxGet("editProfileField.ajax", {"formPassword": getFormPassword(), "field": fieldId, "name": newName}, function(data) {
                if (data && data.success) {
                        successFunction();
                }
@@ -1841,7 +1706,7 @@ function editProfileField(fieldId, newName, successFunction) {
  *            Function to call on success
  */
 function moveProfileField(fieldId, direction, successFunction) {
-       ajaxGet("moveProfileField.ajax", {"formPassword": getFormPassword(), "field": fieldId, "direction": direction}, function(data, textStatus) {
+       ajaxGet("moveProfileField.ajax", {"formPassword": getFormPassword(), "field": fieldId, "direction": direction}, function(data) {
                if (data && data.success) {
                        successFunction();
                }
@@ -1872,7 +1737,7 @@ function moveProfileFieldDown(fieldId, successFunction) {
        moveProfileField(fieldId, "down", successFunction);
 }
 
-var statusRequestQueued = true;
+let statusRequestQueued = true;
 
 /**
  * Sets the status of the web interface as offline.
@@ -1915,25 +1780,25 @@ function showOfflineMarker(visible) {
 // EVERYTHING BELOW HERE IS EXECUTED AFTER LOADING THE PAGE
 //
 
-var sone = $("#sone");
-var focus = true;
-var online = true;
-var initiallyLoggedIn = sone.find("#loggedIn").text() == "true";
-var notLoggedIn = !initiallyLoggedIn;
+const sone = $("#sone");
+let focus = true;
+let online = true;
+const initiallyLoggedIn = sone.find("#loggedIn").text() === "true";
+let notLoggedIn = !initiallyLoggedIn;
 
 /** ID of the next-to-show Sone context menu. */
-var currentSoneMenuId;
+let currentSoneMenuId;
 
 /** Timeout handler for the next-to-show Sone context menu. */
-var currentSoneMenuTimeoutHandler;
+let currentSoneMenuTimeoutHandler;
 
 $(document).ready(function() {
 
        /* rip out the status update textarea. */
        sone.find(".rip-out").each(function() {
-               var oldElement = $(this);
-               var newElement = $("<input type='text'/>");
-               newElement.attr("class", oldElement.attr("class")).attr("name", oldElement.attr("name"));
+               const oldElement = $(this);
+               const newElement = $("<input type='text'/>");
+               newElement.prop("class", oldElement.prop("class")).prop("name", oldElement.prop("name"));
                oldElement.before(newElement).remove();
        });
 
@@ -1948,14 +1813,14 @@ $(document).ready(function() {
                        return false;
                });
                sone.find("#update-status").submit(function() {
-                       var button = $("button:submit", this);
-                       button.attr("disabled", "disabled");
+                       const button = $("button:submit", this);
+                       button.prop("disabled", "disabled");
                        if ($(this).find(":input.default:enabled").length > 0) {
                                return false;
                        }
-                       var sender = $(this).find(":input[name=sender]").val();
-                       var text = $(this).find(":input[name=text]:enabled").val();
-                       ajaxGet("createPost.ajax", { "formPassword": getFormPassword(), "sender": sender, "text": text }, function(data, textStatus) {
+                       const sender = $(this).find(":input[name=sender]").val();
+                       const text = $(this).find(":input[name=text]:enabled").val();
+                       ajaxGet("createPost.ajax", { "formPassword": getFormPassword(), "sender": sender, "text": text }, function() {
                                button.removeAttr("disabled");
                        });
                        $(this).find(":input[name=sender]").val(getCurrentSoneId());
@@ -1982,8 +1847,8 @@ $(document).ready(function() {
                        return false;
                });
                sone.find("#post-message").submit(function() {
-                       var sender = $(this).find(":input[name=sender]").val();
-                       var text = $(this).find(":input[name=text]:enabled").val();
+                       const sender = $(this).find(":input[name=sender]").val();
+                       const text = $(this).find(":input[name=text]:enabled").val();
                        ajaxGet("createPost.ajax", { "formPassword": getFormPassword(), "recipient": getShownSoneId(), "sender": sender, "text": text });
                        $(this).find(":input[name=sender]").val(getCurrentSoneId());
                        $(this).find(":input[name=text]:enabled").val("").blur();
@@ -2008,7 +1873,7 @@ $(document).ready(function() {
        });
 
        /* update post times. */
-       var postIds = [];
+       const postIds = [];
        sone.find(".post").each(function() {
                postIds.push(getPostId(this));
        });
@@ -2018,14 +1883,14 @@ $(document).ready(function() {
        if (!isViewPostPage()) {
                getTranslation("WebInterface.ClickToShow.Replies", function(text) {
                        sone.find(".post .replies").each(function() {
-                               var allReplies = $(this).find(".reply");
+                               const allReplies = $(this).find(".reply");
                                if (allReplies.length > 2) {
-                                       var newHidden = false;
-                                       for (var replyIndex = 0; replyIndex < (allReplies.length - 2); ++replyIndex) {
+                                       let newHidden = false;
+                                       for (let replyIndex = 0; replyIndex < (allReplies.length - 2); ++replyIndex) {
                                                $(allReplies[replyIndex]).addClass("hidden");
                                                newHidden |= $(allReplies[replyIndex]).hasClass("new");
                                        }
-                                       var clickToShowElement = $("<div></div>").addClass("click-to-show");
+                                       const clickToShowElement = $("<div></div>").addClass("click-to-show");
                                        if (newHidden) {
                                                clickToShowElement.addClass("new");
                                        }
index ddc08a9..d61df4e 100644 (file)
@@ -1,10 +1,10 @@
 <%include include/head.html>
 
-       <script language="javascript">
+       <script type="application/javascript">
                function recheckMoveButtons() {
                        $("#sone .profile-field").each(function() {
-                               $(".move-up-field", this).toggleClass("hidden", $(this).prev(".profile-field").length == 0);
-                               $(".move-down-field", this).toggleClass("hidden", $(this).next(".profile-field").length == 0);
+                               $(".move-up-field", this).toggleClass("hidden", $(this).prev(".profile-field").length === 0);
+                               $(".move-down-field", this).toggleClass("hidden", $(this).next(".profile-field").length === 0);
                        });
                }
 
                        /* ajaxify the delete buttons. */
                        getTranslation("Page.EditProfile.Fields.Button.ReallyDelete", function(reallyDeleteText) {
                                $("#sone #edit-profile .delete-field-name button").each(function() {
-                                       confirmButton = $(this).clone().addClass("hidden").addClass("confirm").text(reallyDeleteText).insertAfter(this);
+                                       const confirmButton = $(this).clone().addClass("hidden").addClass("confirm").text(reallyDeleteText).insertAfter(this);
                                        (function(deleteButton, confirmButton) {
                                                deleteButton.click(function() {
                                                        deleteButton.fadeOut("slow", function() {
                                                                confirmButton.fadeIn("slow");
                                                                $(document).one("click", function() {
-                                                                       if (this != confirmButton.get(0)) {
+                                                                       if (this !== confirmButton.get(0)) {
                                                                                confirmButton.fadeOut("slow", function() {
                                                                                        deleteButton.fadeIn("slow");
                                                                                });
@@ -59,8 +59,8 @@
                                                });
                                                confirmButton.click(function() {
                                                        confirmButton.fadeOut("slow");
-                                                       buttonName = confirmButton.attr("name");
-                                                       fieldId = buttonName.substring("delete-field-".length);
+                                                       const buttonName = confirmButton.prop("name");
+                                                       const fieldId = buttonName.substring("delete-field-".length);
                                                        deleteProfileField(fieldId);
                                                        recheckMoveButtons();
                                                        return false;
 
                        /* ajaxify the edit button. */
                        $("#sone #edit-profile .edit-field-name button").each(function() {
-                               profileField = $(this).parents(".profile-field");
-                               fieldNameElement = profileField.find(".name");
-                               inputField = $("input[type=text].short", profileField);
-                               confirmButton = $("button.confirm", profileField);
-                               cancelButton = $("button.cancel", profileField);
+                               const profileField = $(this).parents(".profile-field");
+                               const fieldNameElement = profileField.find(".name");
+                               const inputField = $("input[type=text].short", profileField);
+                               const confirmButton = $("button.confirm", profileField);
+                               const cancelButton = $("button.cancel", profileField);
                                (function(editButton, inputField, confirmButton, cancelButton, fieldNameElement) {
-                                       cleanUp = function(editButton, inputField, confirmButton, cancelButton, fieldNameElement) {
+                                       const cleanUp = function(editButton, inputField, confirmButton, cancelButton, fieldNameElement) {
                                                editButton.removeAttr("disabled");
                                                inputField.addClass("hidden");
                                                confirmButton.addClass("hidden");
                                                fieldNameElement.removeClass("hidden");
                                        };
                                        confirmButton.click(function() {
-                                               inputField.attr("disabled", "disabled");
-                                               confirmButton.attr("disabled", "disabled");
-                                               cancelButton.attr("disabled", "disabled");
-                                               editProfileField(confirmButton.parents(".profile-field").attr("id"), inputField.val(), function() {
+                                               inputField.prop("disabled", "disabled");
+                                               confirmButton.prop("disabled", "disabled");
+                                               cancelButton.prop("disabled", "disabled");
+                                               editProfileField(confirmButton.parents(".profile-field").prop("id"), inputField.val(), function() {
                                                        fieldNameElement.text(inputField.val());
                                                        cleanUp(editButton, inputField, confirmButton, cancelButton, fieldNameElement);
                                                });
                                                return false;
                                        });
                                        inputField.keypress(function(event) {
-                                               if (event.which == 13) {
+                                               if (event.which === 13) {
                                                        confirmButton.click();
                                                        return false;
-                                               } else if (event.which == 27) {
+                                               } else if (event.which === 27) {
                                                        cancelButton.click();
                                                        return false;
                                                }
                                        });
                                        editButton.click(function() {
-                                               editButton.attr("disabled", "disabled");
+                                               editButton.prop("disabled", "disabled");
                                                fieldNameElement.addClass("hidden");
                                                inputField.removeAttr("disabled").val(fieldNameElement.text()).removeClass("hidden").focus().select();
                                                confirmButton.removeAttr("disabled").removeClass("hidden");
 
                        /* ajaxify “move up” and “move down” buttons. */
                        $("#sone .profile-field .move-down-field button").click(function() {
-                               profileField = $(this).parents(".profile-field");
-                               moveProfileFieldDown(profileField.attr("id"), function() {
-                                       next = profileField.next();
-                                       current = profileField.insertAfter(next);
+                               const profileField = $(this).parents(".profile-field");
+                               moveProfileFieldDown(profileField.prop("id"), function() {
+                                       profileField.insertAfter(profileField.next());
                                        recheckMoveButtons();
                                });
                                return false;
                        });
                        $("#sone .profile-field .move-up-field button").click(function() {
-                               profileField = $(this).parents(".profile-field");
-                               moveProfileFieldUp(profileField.attr("id"), function() {
-                                       previous = profileField.prev();
-                                       current = profileField.insertBefore(previous);
+                               const profileField = $(this).parents(".profile-field");
+                               moveProfileFieldUp(profileField.prop("id"), function() {
+                                       profileField.insertBefore(profileField.prev());
                                        recheckMoveButtons();
                                });
                                return false;
index b8b9cb2..a8a33c9 100644 (file)
@@ -2,7 +2,7 @@
 
        <div class="page-id hidden">image-browser</div>
 
-       <script language="javascript">
+       <script type="application/javascript">
 
                /* hide all those forms. */
                function hideAndShowBlock(blockElement, clickToShowElement, clickToHideElement) {
@@ -21,7 +21,7 @@
                }
 
                /* ID of the image currently being edited. */
-               var editingImageId = null;
+               let editingImageId = null;
 
                /**
                 * Shows the form for editing an image.
@@ -29,7 +29,7 @@
                 * @param imageId The ID of the image to edit.
                 */
                function editImage(imageId) {
-                       if (editingImageId != imageId) {
+                       if (editingImageId !== imageId) {
                                cancelImageEditing();
                        } else {
                                return;
@@ -38,7 +38,7 @@
                        $(".show-data", getImage(imageId)).hide();
                        $(".edit-data", getImage(imageId)).show();
                        $(document).bind("click.sone", function(event) {
-                               if ($(event.target).closest("#image-" + imageId).size() == 0) {
+                               if ($(event.target).closest("#image-" + imageId).size() === 0) {
                                        cancelImageEditing();
                                }
                        });
                 * @param destinationId The ID of the destionation image
                 */
                function swapImage(sourceId, destinationId) {
-                       sourceElement = getImage(sourceId);
-                       destinationElement = getImage(destinationId);
-                       sourceParent = sourceElement.closest(".image-row");
-                       sourcePrevSibling = sourceElement.prev();
+                       const sourceElement = getImage(sourceId);
+                       const destinationElement = getImage(destinationId);
+                       const sourceParent = sourceElement.closest(".image-row");
+                       const sourcePrevSibling = sourceElement.prev();
                        sourceElement.detach();
                        destinationElement.before(sourceElement);
-                       if (sourcePrevSibling.get(0) != destinationElement.get(0)) {
+                       if (sourcePrevSibling.get(0) !== destinationElement.get(0)) {
                                destinationElement.detach();
                                (sourcePrevSibling.size() > 0) ? sourcePrevSibling.after(destinationElement) : sourceParent.prepend(destinationElement);
                        }
-                       if ($("button[name='moveLeft']", sourceElement).hasClass("hidden") != $("button[name='moveLeft']", destinationElement).hasClass("hidden")) {
+                       if ($("button[name='moveLeft']", sourceElement).hasClass("hidden") !== $("button[name='moveLeft']", destinationElement).hasClass("hidden")) {
                                $("button[name='moveLeft']", sourceElement).toggleClass("hidden");
                                $("button[name='moveLeft']", destinationElement).toggleClass("hidden");
                        }
-                       if ($("button[name='moveRight']", sourceElement).hasClass("hidden") != $("button[name='moveRight']", destinationElement).hasClass("hidden")) {
+                       if ($("button[name='moveRight']", sourceElement).hasClass("hidden") !== $("button[name='moveRight']", destinationElement).hasClass("hidden")) {
                                $("button[name='moveRight']", sourceElement).toggleClass("hidden");
                                $("button[name='moveRight']", destinationElement).toggleClass("hidden");
                        }
                 */
                function prepareImages() {
                        $(".image").each(function() {
-                               imageId = $(this).closest(".image").find(".image-id").text();
+                               const imageId = $(this).closest(".image").find(".image-id").text();
                                (function(element, imageId) {
                                        $(".show-data", element).click(function() {
                                                editImage(imageId);
                                        });
                                        $("button[name='moveLeft'], button[name='moveRight']", element).click(function() {
-                                               ajaxGet("editImage.ajax", { "formPassword": getFormPassword(), "image": imageId, "moveLeft": this.name == "moveLeft", "moveRight": this.name == "moveRight" }, function(data) {
+                                               ajaxGet("editImage.ajax", { "formPassword": getFormPassword(), "image": imageId, "moveLeft": this.name === "moveLeft", "moveRight": this.name === "moveRight" }, function(data) {
                                                        if (data && data.success) {
                                                                swapImage(data.sourceImageId, data.destinationImageId);
                                                        }
                                                return false;
                                        });
                                        $("button[name='submit']", element).click(function() {
-                                               title = $(":input[name='title']:enabled", this.form).val();
-                                               description = $(":input[name='description']:enabled", this.form).val();
+                                               const title = $(":input[name='title']:enabled", this.form).val();
+                                               const description = $(":input[name='description']:enabled", this.form).val();
                                                ajaxGet("editImage.ajax", { "formPassword": getFormPassword(), "image": imageId, "title": title, "description": description }, function(data) {
-                            var imageElement = getImage(data.imageId);
-                            var imageTitleInput = imageElement.find(":input[name='title']");
-                            var imageDescriptionInput = imageElement.find(":input[name='description']");
+                                                       const imageElement = getImage(data.imageId);
+                                                       const imageTitleInput = imageElement.find(":input[name='title']");
+                                                       const imageDescriptionInput = imageElement.find(":input[name='description']");
                             if (data && data.success) {
                                                                imageElement.find(".image-title").text(data.title);
                                                                imageElement.find(".image-description").html(data.parsedDescription);
-                                                               imageTitleInput.attr("defaultValue", data.title);
-                                                               imageDescriptionInput.attr("defaultValue", data.description);
+                                                               imageTitleInput.prop("defaultValue", data.title);
+                                                               imageDescriptionInput.prop("defaultValue", data.description);
                                                                cancelImageEditing();
                                                        } else if (data && !data.success) {
-                                                               imageTitleInput.attr("value", imageTitleInput.attr("defaultValue"));
-                                imageDescriptionInput.attr("value", imageDescriptionInput.attr("defaultValue"));
+                                                               imageTitleInput.prop("value", imageTitleInput.prop("defaultValue"));
+                                imageDescriptionInput.prop("value", imageDescriptionInput.prop("defaultValue"));
                                 cancelImageEditing();
                             }
                                                });
                }
 
                /* ID of the album currently being edited. */
-               var editingAlbumId = null;
+               let editingAlbumId = null;
 
                /**
                 * Shows the form for editing an album.
                 * @param albumId The ID of the album to edit.
                 */
                function editAlbum(albumId) {
-                       if (editingAlbumId != albumId) {
+                       if (editingAlbumId !== albumId) {
                                if (editingAlbumId != null) {
                                        cancelAlbumEditing();
                                }
                        $(".edit-data", getAlbum(albumId)).show();
                        console.log(getAlbum(albumId));
                        $(document).bind("click.sone", function(event) {
-                               if ($(event.target).closest("#album-" + albumId).size() == 0) {
+                               if ($(event.target).closest("#album-" + albumId).size() === 0) {
                                        cancelAlbumEditing();
                                }
                        });
                 * @param destinationId The ID of the destionation album
                 */
                function swapAlbum(sourceId, destinationId) {
-                       sourceElement = getAlbum(sourceId);
-                       destinationElement = getAlbum(destinationId);
-                       sourceParent = sourceElement.closest(".album-row");
-                       sourcePrevSibling = sourceElement.prev();
+                       const sourceElement = getAlbum(sourceId);
+                       const destinationElement = getAlbum(destinationId);
+                       const sourceParent = sourceElement.closest(".album-row");
+                       const sourcePrevSibling = sourceElement.prev();
                        sourceElement.detach();
                        destinationElement.before(sourceElement);
-                       if (sourcePrevSibling.get(0) != destinationElement.get(0)) {
+                       if (sourcePrevSibling.get(0) !== destinationElement.get(0)) {
                                destinationElement.detach();
                                (sourcePrevSibling.size() > 0) ? sourcePrevSibling.after(destinationElement) : sourceParent.prepend(destinationElement);
                        }
-                       if ($("button[name='moveLeft']", sourceElement).hasClass("hidden") != $("button[name='moveLeft']", destinationElement).hasClass("hidden")) {
+                       if ($("button[name='moveLeft']", sourceElement).hasClass("hidden") !== $("button[name='moveLeft']", destinationElement).hasClass("hidden")) {
                                $("button[name='moveLeft']", sourceElement).toggleClass("hidden");
                                $("button[name='moveLeft']", destinationElement).toggleClass("hidden");
                        }
-                       if ($("button[name='moveRight']", sourceElement).hasClass("hidden") != $("button[name='moveRight']", destinationElement).hasClass("hidden")) {
+                       if ($("button[name='moveRight']", sourceElement).hasClass("hidden") !== $("button[name='moveRight']", destinationElement).hasClass("hidden")) {
                                $("button[name='moveRight']", sourceElement).toggleClass("hidden");
                                $("button[name='moveRight']", destinationElement).toggleClass("hidden");
                        }
                 */
                function prepareAlbums() {
                        $(".album").each(function() {
-                               albumId = $(this).closest(".album").find(".album-id").text();
+                               const albumId = $(this).closest(".album").find(".album-id").text();
                                (function(element, albumId) {
                                        $(".show-data", element).click(function() {
                                                console.log("show-data");
                                                editAlbum(albumId);
                                        });
                                        $("button[name='moveLeft'], button[name='moveRight']", element).click(function() {
-                                               ajaxGet("editAlbum.ajax", { "formPassword": getFormPassword(), "album": albumId, "moveLeft": this.name == "moveLeft", "moveRight": this.name == "moveRight" }, function(data) {
+                                               ajaxGet("editAlbum.ajax", { "formPassword": getFormPassword(), "album": albumId, "moveLeft": this.name === "moveLeft", "moveRight": this.name === "moveRight" }, function(data) {
                                                        if (data && data.success) {
                                                                swapAlbum(data.sourceAlbumId, data.destinationAlbumId);
                                                        }
                                                return false;
                                        });
                                        $("button[name='submit']", element).click(function() {
-                                               title = $(":input[name='title']:enabled", this.form).val();
-                                               description = $(":input[name='description']:enabled", this.form).val();
+                                               const title = $(":input[name='title']:enabled", this.form).val();
+                                               const description = $(":input[name='description']:enabled", this.form).val();
                                                ajaxGet("editAlbum.ajax", { "formPassword": getFormPassword(), "album": albumId, "title": title, "description": description }, function(data) {
                                                        if (data) {
-                                var albumTitleField = getAlbum(data.albumId).find(".album-title");
-                                var albumDescriptionField = getAlbum(data.albumId).find(".album-description");
+                                                               const albumTitleField = getAlbum(data.albumId).find(".album-title");
+                                                               const albumDescriptionField = getAlbum(data.albumId).find(".album-description");
                                 if (data.success) {
                                     albumTitleField.text(data.title);
                                     albumDescriptionField.text(data.description);
-                                    getAlbum(data.albumId).find(":input[name='title']").attr("defaultValue", title);
-                                    getAlbum(data.albumId).find(":input[name='description']").attr("defaultValue", description);
+                                    getAlbum(data.albumId).find(":input[name='title']").prop("defaultValue", title);
+                                    getAlbum(data.albumId).find(":input[name='description']").prop("defaultValue", description);
                                 } else {
-                                    albumTitleField.attr("value", albumTitleField.attr("defaultValue"));
-                                    albumDescriptionField.attr("value", albumDescriptionField.attr("defaultValue"));
+                                    albumTitleField.prop("value", albumTitleField.prop("defaultValue"));
+                                    albumDescriptionField.prop("value", albumDescriptionField.prop("defaultValue"));
                                 }
                                 cancelAlbumEditing();
                             }
                <%else>
 
                        <%if album.sone.local>
-                               <script language="javascript">
+                               <script type="application/javascript">
 
                                        $(function() {
                                                getTranslation("WebInterface.DefaultText.UploadImage.Title", function(text) {
                <%else>
 
                        <%if image.sone.local>
-                               <script language="javascript">
+                               <script type="application/javascript">
                                        $(function() {
                                                getTranslation("WebInterface.DefaultText.EditImage.Title", function(text) {
                                                        $("#edit-image input[name='title']").each(function() {
        <%elseif soneRequested>
 
                <%if sone.local>
-                       <script language="javascript">
+                       <script type="application/javascript">
                                $(function() {
                                        getTranslation("WebInterface.DefaultText.CreateAlbum.Name", function(text) {
                                                $("#create-album input[name='name']").each(function() {
index 13b564d..71d5b17 100644 (file)
@@ -4,7 +4,7 @@
        <div id="currentSoneId" class="hidden"><% currentSone.id|html></div>
        <div id="loggedIn" class="hidden"><%ifnull !currentSone>true<%else>false<%/if></div>
 
-       <script src="javascript/jquery-1.4.2.js" language="javascript"></script>
+       <script src="javascript/jquery-3.4.1.js" language="javascript"></script>
        <script src="javascript/jquery.url.js" language="javascript"></script>
        <script src="javascript/jquery.fieldselection.js" language="javascript"></script>
        <script src="javascript/sone.js" language="javascript"></script>
index fc51e79..a2b2532 100644 (file)
@@ -10,9 +10,9 @@
        <div class="inner-menu">
                <div>
                        <a class="author" href="viewSone.html?sone=<%sone.id|html>"><%sone.niceName|html></a>
-                       (<%= View.Sone.Stats.Posts|l10n 0=sone.posts.size>, <%= View.Sone.Stats.Replies|l10n 0=sone.replies.size><%if ! sone.allImages.size|match value==0>, <%= View.Sone.Stats.Images|l10n 0=sone.allImages.size><%/if>)
+                       (<%= View.Sone.Stats.Posts|l10n 0=sone.posts.size>, <%= View.Sone.Stats.Replies|l10n 0=sone.replies.size><%if ! sone.allImages.size|match value==0>, <%= View.Sone.Stats.Images|l10n 0=sone.allImages.size><%/if><%if core.debug>, <% sone.client|html><%/if>)
                </div>
-               <div><a href="/WebOfTrust/ShowIdentity?id=<%sone.id|html>">» <% =View.Post.WebOfTrustLink|l10n|html></a></div>
+               <div><a href="/WebOfTrust/ShowIdentity?id=<%sone.id|html>">» <% =View.SoneMenu.WebOfTrustLink|l10n|html></a></div>
                <%foreach sone.albums album>
                        <%first>
                                <div><a href="imageBrowser.html?sone=<% sone.id|html>">» <% =View.SoneMenu.Link.AllAlbums|l10n|html></a></div>
index 81683e4..edcabce 100644 (file)
                                        <input type="hidden" name="post" value="<% post.id|html>" />
                                        <button type="submit" value="1"><%= View.Post.UnlikeLink|l10n|html></button>
                                </form>
-                               <%if !post.sone.current>
-                                       <%ifnull !post.sone.trust>
-                                               <span class='separator'>·</span>
-                                               <form class="trust post-trust<%if post.sone.trust.assigned> hidden<%/if>" action="trust.html" method="post">
-                                                       <input type="hidden" name="formPassword" value="<% formPassword|html>" />
-                                                       <input type="hidden" name="returnPage" value="<% request.uri|html>" />
-                                                       <input type="hidden" name="sone" value="<% post.sone.id|html>" />
-                                                       <button type="submit" title="<%= View.Trust.Tooltip.Trust|l10n|html>">✓</button>
-                                               </form>
-                                               <form class="distrust post-distrust<%if post.sone.trust.assigned> hidden<%/if>" action="distrust.html" method="post">
-                                                       <input type="hidden" name="formPassword" value="<% formPassword|html>" />
-                                                       <input type="hidden" name="returnPage" value="<% request.uri|html>" />
-                                                       <input type="hidden" name="sone" value="<% post.sone.id|html>" />
-                                                       <button type="submit" title="<%= View.Trust.Tooltip.Distrust|l10n|html>">✗</button>
-                                               </form>
-                                               <form class="untrust post-untrust<%if !post.sone.trust.assigned> hidden<%/if>" action="untrust.html" method="post">
-                                                       <input type="hidden" name="formPassword" value="<% formPassword|html>" />
-                                                       <input type="hidden" name="returnPage" value="<% request.uri|html>" />
-                                                       <input type="hidden" name="sone" value="<% post.sone.id|html>" />
-                                                       <button type="submit" title="<%= View.Trust.Tooltip.Untrust|l10n|html>">↶</button>
-                                               </form>
-                                       <%/if>
-                               <%/if>
                        <%/if>
+                       <span class='separator'>·</span>
+                       <a class="wot-link" href="/WebOfTrust/ShowIdentity?id=<% post.sone.id|html>"><%= View.Post.WebOfTrustLink|l10n|html></a>
                        <%if post.sone.local>
                                <span class='separator'>·</span>
                                <form class="delete delete-post" action="deletePost.html" method="post">
                                                <div class="sender">
                                                        <select name="sender" title="<%= View.UpdateStatus.Text.ChooseSenderIdentity|l10n|html>">
                                                                <%foreach localSones localSone|sort>
-                                                                       <option value="<% localSone.id|html>"<%if localSone.current> selected="selected"<%/if>><% localSone.niceName|html></option>
+                                                                       <option value="<% localSone.id|html>"<%if localSone|match value=post.replySone> selected="selected"<%/if>><% localSone.niceName|html></option>
                                                                <%/foreach>
                                                        </select>
                                                </div>
index 3a5e59a..7462b2b 100644 (file)
                                        <input type="hidden" name="reply" value="<% reply.id|html>" />
                                        <button type="submit" value="1"><%= View.Post.UnlikeLink|l10n|html></button>
                                </form>
-                               <%if !reply.sone.current>
-                                       <%ifnull !reply.sone.trust>
-                                               <span class='separator'>·</span>
-                                               <form class="trust reply-trust<%if reply.sone.trust.assigned> hidden<%/if>" action="trust.html" method="post">
-                                                       <input type="hidden" name="formPassword" value="<% formPassword|html>" />
-                                                       <input type="hidden" name="returnPage" value="<% request.uri|html>" />
-                                                       <input type="hidden" name="sone" value="<% reply.sone.id|html>" />
-                                                       <button type="submit" title="<%= View.Trust.Tooltip.Trust|l10n|html>">✓</button>
-                                               </form>
-                                               <form class="distrust reply-distrust<%if reply.sone.trust.assigned> hidden<%/if>" action="distrust.html" method="post">
-                                                       <input type="hidden" name="formPassword" value="<% formPassword|html>" />
-                                                       <input type="hidden" name="returnPage" value="<% request.uri|html>" />
-                                                       <input type="hidden" name="sone" value="<% reply.sone.id|html>" />
-                                                       <button type="submit" title="<%= View.Trust.Tooltip.Distrust|l10n|html>">✗</button>
-                                               </form>
-                                               <form class="untrust reply-untrust<%if !reply.sone.trust.assigned> hidden<%/if>" action="untrust.html" method="post">
-                                                       <input type="hidden" name="formPassword" value="<% formPassword|html>" />
-                                                       <input type="hidden" name="returnPage" value="<% request.uri|html>" />
-                                                       <input type="hidden" name="sone" value="<% reply.sone.id|html>" />
-                                                       <button type="submit" title="<%= View.Trust.Tooltip.Untrust|l10n|html>">↶</button>
-                                               </form>
-                                       <%/if>
-                               <%/if>
                        <%/if>
+                       <span class='separator'>·</span>
+                       <a class="wot-link" href="/WebOfTrust/ShowIdentity?id=<% reply.sone.id|html>"><%= View.Post.WebOfTrustLink|l10n|html></a>
                        <%if reply.sone.local>
                                <span class='separator'>·</span>
                                <form class="delete delete-reply" action="deleteReply.html" method="post">
index c113748..21c6be2 100644 (file)
@@ -2,14 +2,14 @@
 
        <div class="page-id hidden">known-sones</div>
 
-       <script language="javascript">
+       <script type="application/javascript">
 
                $(document).ready(function() {
                        $("select[name=sort]").change(function() {
-                               value = $(this).val();
-                               if ((value == "activity") || (value == "posts") || (value == "images")) {
+                               const value = $(this).val();
+                               if ((value === "activity") || (value === "posts") || (value === "images")) {
                                        $("select[name=order]").val("desc");
-                               } else if (value == "name") {
+                               } else if (value === "name") {
                                        $("select[name=order]").val("asc");
                                }
                        });
diff --git a/src/main/resources/templates/metrics.html b/src/main/resources/templates/metrics.html
new file mode 100644 (file)
index 0000000..5707476
--- /dev/null
@@ -0,0 +1,30 @@
+<%include include/head.html>
+
+       <div class="page-id hidden">metrics</div>
+
+       <h1><%= Page.Metrics.Page.Title|l10n|html></h1>
+
+       <table>
+               <thead>
+                       <tr>
+                               <td>Metric</td>
+                               <td class="numeric">Count</td>
+                               <td class="numeric">Min</td>
+                               <td class="numeric">Max</td>
+                               <td class="numeric">Mean</td>
+                               <td class="numeric">Median</td>
+                               <td class="numeric">75%</td>
+                               <td class="numeric">95%</td>
+                               <td class="numeric">98%</td>
+                               <td class="numeric">99%</td>
+                               <td class="numeric">99.9%</td>
+                       </tr>
+               </thead>
+               <tbody>
+                       <%foreach histograms histogram>
+                               <% histogram.value|render-histogram name=histogram.key>
+                       <%/foreach>
+               </tbody>
+       </table>
+
+<%include include/tail.html>
diff --git a/src/main/resources/templates/notify/soneLockedOnStartupNotification.html b/src/main/resources/templates/notify/soneLockedOnStartupNotification.html
new file mode 100644 (file)
index 0000000..8cd4e56
--- /dev/null
@@ -0,0 +1,6 @@
+<div class="text">
+       <%= Notification.SoneLockedOnStartup.Text|l10n|html>
+       <%foreach sones sone>
+               <a href="viewSone.html?sone=<% sone.id|html>" title="<% sone.requestUri|html>"><% sone.niceName|html></a><%notlast>,<%/notlast><%last>.<%/last>
+       <%/foreach>
+</div>
index 34a1196..9ef5ceb 100644 (file)
@@ -1,6 +1,6 @@
 <%include include/head.html>
 
-       <script language="javascript">
+       <script type="application/javascript">
                $(document).ready(function() {
                        getTranslation("WebInterface.DefaultText.Option.InsertionDelay", function(insertionDelayDefaultText) {
                                registerInputTextareaSwap("#sone #options input[name=insertion-delay]", insertionDelayDefaultText, "insertion-delay", true, true);
                        getTranslation("WebInterface.DefaultText.Option.PostCutOffLength", function(postCutOffLengthText) {
                                registerInputTextareaSwap("#sone #options input[name=post-cut-off-length]", postCutOffLengthText, "post-cut-off-length", true, true);
                        });
-                       getTranslation("WebInterface.DefaultText.Option.PositiveTrust", function(positiveTrustText) {
-                               registerInputTextareaSwap("#sone #options input[name=positive-trust]", positiveTrustText, "positive-trust", true, true);
-                       });
-                       getTranslation("WebInterface.DefaultText.Option.NegativeTrust", function(negativeTrustText) {
-                               registerInputTextareaSwap("#sone #options input[name=negative-trust]", negativeTrustText, "negative-trust", true, true);
-                       });
-                       getTranslation("WebInterface.DefaultText.Option.TrustComment", function(trustCommentText) {
-                               registerInputTextareaSwap("#sone #options input[name=trust-comment]", trustCommentText, "trust-comment", true, true);
-                       });
                });
        </script>
 
                        <%= Page.Options.Option.RequireFullAccess.Description|l10n|html></p>
                </p>
 
-               <h2><%= Page.Options.Section.TrustOptions.Title|l10n|html></h2>
-
-               <p><%= Page.Options.Option.PositiveTrust.Description|l10n|html></p>
-               <%if =positive-trust|in collection=fieldErrors>
-                       <p class="warning"><%= Page.Options.Warnings.ValueNotChanged|l10n|html></p>
-               <%/if>
-               <p><input type="text" name="positive-trust" value="<% positive-trust|html>" /></p>
-
-               <p><%= Page.Options.Option.NegativeTrust.Description|l10n|html></p>
-               <%if =negative-trust|in collection=fieldErrors>
-                       <p class="warning"><%= Page.Options.Warnings.ValueNotChanged|l10n|html></p>
-               <%/if>
-               <p><input type="text" name="negative-trust" value="<% negative-trust|html>" /></p>
-
-               <p><%= Page.Options.Option.TrustComment.Description|l10n|html></p>
-               <p><input type="text" name="trust-comment" value="<% trust-comment|html>" /></p>
-
                <h2><%= Page.Options.Section.FcpOptions.Title|l10n|html></h2>
 
                <p><input type="checkbox" name="fcp-interface-active"<%if fcp-interface-active> checked="checked"<%/if> /> <%= Page.Options.Option.FcpInterfaceActive.Description|l10n|html></p>
                        </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/ConfigurationSoneParserTest.java b/src/test/java/net/pterodactylus/sone/core/ConfigurationSoneParserTest.java
deleted file mode 100644 (file)
index 4edc1b7..0000000
+++ /dev/null
@@ -1,522 +0,0 @@
-package net.pterodactylus.sone.core;
-
-import static com.google.common.base.Optional.of;
-import static net.pterodactylus.sone.test.Matchers.isAlbum;
-import static net.pterodactylus.sone.test.Matchers.isImage;
-import static net.pterodactylus.sone.test.Matchers.isPost;
-import static net.pterodactylus.sone.test.Matchers.isPostReply;
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.contains;
-import static org.hamcrest.Matchers.containsInAnyOrder;
-import static org.hamcrest.Matchers.emptyIterable;
-import static org.hamcrest.Matchers.hasSize;
-import static org.hamcrest.Matchers.is;
-import static org.hamcrest.Matchers.notNullValue;
-import static org.hamcrest.Matchers.nullValue;
-import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
-
-import java.util.Collection;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-
-import net.pterodactylus.sone.core.ConfigurationSoneParser.InvalidAlbumFound;
-import net.pterodactylus.sone.core.ConfigurationSoneParser.InvalidImageFound;
-import net.pterodactylus.sone.core.ConfigurationSoneParser.InvalidParentAlbumFound;
-import net.pterodactylus.sone.core.ConfigurationSoneParser.InvalidPostFound;
-import net.pterodactylus.sone.core.ConfigurationSoneParser.InvalidPostReplyFound;
-import net.pterodactylus.sone.data.Album;
-import net.pterodactylus.sone.data.Post;
-import net.pterodactylus.sone.data.PostReply;
-import net.pterodactylus.sone.data.Profile;
-import net.pterodactylus.sone.data.Profile.Field;
-import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.sone.database.AlbumBuilder;
-import net.pterodactylus.sone.database.AlbumBuilderFactory;
-import net.pterodactylus.sone.database.ImageBuilder;
-import net.pterodactylus.sone.database.ImageBuilderFactory;
-import net.pterodactylus.sone.database.PostBuilder;
-import net.pterodactylus.sone.database.PostBuilderFactory;
-import net.pterodactylus.sone.database.PostReplyBuilder;
-import net.pterodactylus.sone.database.PostReplyBuilderFactory;
-import net.pterodactylus.sone.test.TestAlbumBuilder;
-import net.pterodactylus.sone.test.TestImageBuilder;
-import net.pterodactylus.sone.test.TestPostBuilder;
-import net.pterodactylus.sone.test.TestPostReplyBuilder;
-import net.pterodactylus.sone.test.TestValue;
-import net.pterodactylus.util.config.Configuration;
-
-import com.google.common.base.Optional;
-import org.hamcrest.Matchers;
-import org.junit.Test;
-import org.mockito.invocation.InvocationOnMock;
-import org.mockito.stubbing.Answer;
-
-/**
- * Unit test for {@link ConfigurationSoneParser}.
- */
-public class ConfigurationSoneParserTest {
-
-       private final Configuration configuration = mock(Configuration.class);
-       private final Sone sone = mock(Sone.class);
-       private final ConfigurationSoneParser configurationSoneParser;
-
-       public ConfigurationSoneParserTest() {
-               when(sone.getId()).thenReturn("1");
-               configurationSoneParser =
-                               new ConfigurationSoneParser(configuration, sone);
-       }
-
-       @Test
-       public void emptyProfileIsLoadedCorrectly() {
-               setupEmptyProfile();
-               Profile profile = configurationSoneParser.parseProfile();
-               assertThat(profile, notNullValue());
-               assertThat(profile.getFirstName(), nullValue());
-               assertThat(profile.getMiddleName(), nullValue());
-               assertThat(profile.getLastName(), nullValue());
-               assertThat(profile.getBirthDay(), nullValue());
-               assertThat(profile.getBirthMonth(), nullValue());
-               assertThat(profile.getBirthYear(), nullValue());
-               assertThat(profile.getFields(), emptyIterable());
-       }
-
-       private void setupEmptyProfile() {
-               when(configuration.getStringValue(anyString())).thenReturn(
-                               TestValue.<String>from(null));
-               when(configuration.getIntValue(anyString())).thenReturn(
-                               TestValue.<Integer>from(null));
-       }
-
-       @Test
-       public void filledProfileWithFieldsIsParsedCorrectly() {
-               setupFilledProfile();
-               Profile profile = configurationSoneParser.parseProfile();
-               assertThat(profile, notNullValue());
-               assertThat(profile.getFirstName(), is("First"));
-               assertThat(profile.getMiddleName(), is("M."));
-               assertThat(profile.getLastName(), is("Last"));
-               assertThat(profile.getBirthDay(), is(18));
-               assertThat(profile.getBirthMonth(), is(12));
-               assertThat(profile.getBirthYear(), is(1976));
-               final List<Field> fields = profile.getFields();
-               assertThat(fields, hasSize(2));
-               assertThat(fields.get(0).getName(), is("Field1"));
-               assertThat(fields.get(0).getValue(), is("Value1"));
-               assertThat(fields.get(1).getName(), is("Field2"));
-               assertThat(fields.get(1).getValue(), is("Value2"));
-       }
-
-       private void setupFilledProfile() {
-               setupString("Sone/1/Profile/FirstName", "First");
-               setupString("Sone/1/Profile/MiddleName", "M.");
-               setupString("Sone/1/Profile/LastName", "Last");
-               setupInteger("Sone/1/Profile/BirthDay", 18);
-               setupInteger("Sone/1/Profile/BirthMonth", 12);
-               setupInteger("Sone/1/Profile/BirthYear", 1976);
-               setupString("Sone/1/Profile/Fields/0/Name", "Field1");
-               setupString("Sone/1/Profile/Fields/0/Value", "Value1");
-               setupString("Sone/1/Profile/Fields/1/Name", "Field2");
-               setupString("Sone/1/Profile/Fields/1/Value", "Value2");
-               setupString("Sone/1/Profile/Fields/2/Name", null);
-       }
-
-       private void setupString(String nodeName, String value) {
-               when(configuration.getStringValue(eq(nodeName))).thenReturn(
-                               TestValue.from(value));
-       }
-
-       private void setupInteger(String nodeName, Integer value) {
-               when(configuration.getIntValue(eq(nodeName))).thenReturn(
-                               TestValue.from(value));
-       }
-
-       @Test
-       public void postsAreParsedCorrectly() {
-               setupCompletePosts();
-               PostBuilderFactory postBuilderFactory = createPostBuilderFactory();
-               Collection<Post> posts =
-                               configurationSoneParser.parsePosts(postBuilderFactory);
-               assertThat(posts,
-                               Matchers.<Post>containsInAnyOrder(
-                                               isPost("P0", 1000L, "T0", Optional.<String>absent()),
-                                               isPost("P1", 1001L, "T1",
-                                                               of("1234567890123456789012345678901234567890123"))));
-       }
-
-       private PostBuilderFactory createPostBuilderFactory() {
-               PostBuilderFactory postBuilderFactory =
-                               mock(PostBuilderFactory.class);
-               when(postBuilderFactory.newPostBuilder()).thenAnswer(
-                               new Answer<PostBuilder>() {
-                                       @Override
-                                       public PostBuilder answer(InvocationOnMock invocation)
-                                       throws Throwable {
-                                               return new TestPostBuilder();
-                                       }
-                               });
-               return postBuilderFactory;
-       }
-
-       private void setupCompletePosts() {
-               setupPost("0", "P0", 1000L, "T0", null);
-               setupPost("1", "P1", 1001L, "T1",
-                               "1234567890123456789012345678901234567890123");
-               setupPost("2", null, 0L, null, null);
-       }
-
-       private void setupPost(String postNumber, String postId, long time,
-                       String text, String recipientId) {
-               setupString("Sone/1/Posts/" + postNumber + "/ID", postId);
-               setupLong("Sone/1/Posts/" + postNumber + "/Time", time);
-               setupString("Sone/1/Posts/" + postNumber + "/Text", text);
-               setupString("Sone/1/Posts/" + postNumber + "/Recipient", recipientId);
-       }
-
-       private void setupLong(String nodeName, Long value) {
-               when(configuration.getLongValue(eq(nodeName))).thenReturn(
-                               TestValue.from(value));
-       }
-
-       @Test(expected = InvalidPostFound.class)
-       public void postWithoutTimeIsRecognized() {
-               setupPostWithoutTime();
-               configurationSoneParser.parsePosts(createPostBuilderFactory());
-       }
-
-       private void setupPostWithoutTime() {
-               setupPost("0", "P0", 0L, "T0", null);
-       }
-
-       @Test(expected = InvalidPostFound.class)
-       public void postWithoutTextIsRecognized() {
-               setupPostWithoutText();
-               configurationSoneParser.parsePosts(createPostBuilderFactory());
-       }
-
-       private void setupPostWithoutText() {
-               setupPost("0", "P0", 1000L, null, null);
-       }
-
-       @Test
-       public void postWithInvalidRecipientIdIsRecognized() {
-               setupPostWithInvalidRecipientId();
-               Collection<Post> posts = configurationSoneParser.parsePosts(
-                               createPostBuilderFactory());
-               assertThat(posts, contains(
-                               isPost("P0", 1000L, "T0", Optional.<String>absent())));
-       }
-
-       private void setupPostWithInvalidRecipientId() {
-               setupPost("0", "P0", 1000L, "T0", "123");
-               setupPost("1", null, 0L, null, null);
-       }
-
-       @Test
-       public void postRepliesAreParsedCorrectly() {
-               setupPostReplies();
-               PostReplyBuilderFactory postReplyBuilderFactory =
-                               new PostReplyBuilderFactory() {
-                                       @Override
-                                       public PostReplyBuilder newPostReplyBuilder() {
-                                               return new TestPostReplyBuilder();
-                                       }
-                               };
-               Collection<PostReply> postReplies =
-                               configurationSoneParser.parsePostReplies(
-                                               postReplyBuilderFactory);
-               assertThat(postReplies, hasSize(2));
-               assertThat(postReplies,
-                               containsInAnyOrder(isPostReply("R0", "P0", 1000L, "T0"),
-                                               isPostReply("R1", "P1", 1001L, "T1")));
-       }
-
-       private void setupPostReplies() {
-               setupPostReply("0", "R0", "P0", 1000L, "T0");
-               setupPostReply("1", "R1", "P1", 1001L, "T1");
-               setupPostReply("2", null, null, 0L, null);
-       }
-
-       private void setupPostReply(String postReplyNumber, String postReplyId,
-                       String postId, long time, String text) {
-               setupString("Sone/1/Replies/" + postReplyNumber + "/ID", postReplyId);
-               setupString("Sone/1/Replies/" + postReplyNumber + "/Post/ID", postId);
-               setupLong("Sone/1/Replies/" + postReplyNumber + "/Time", time);
-               setupString("Sone/1/Replies/" + postReplyNumber + "/Text", text);
-       }
-
-       @Test(expected = InvalidPostReplyFound.class)
-       public void missingPostIdIsRecognized() {
-               setupPostReplyWithMissingPostId();
-               configurationSoneParser.parsePostReplies(null);
-       }
-
-       private void setupPostReplyWithMissingPostId() {
-               setupPostReply("0", "R0", null, 1000L, "T0");
-       }
-
-       @Test(expected = InvalidPostReplyFound.class)
-       public void missingPostReplyTimeIsRecognized() {
-               setupPostReplyWithMissingPostReplyTime();
-               configurationSoneParser.parsePostReplies(null);
-       }
-
-       private void setupPostReplyWithMissingPostReplyTime() {
-               setupPostReply("0", "R0", "P0", 0L, "T0");
-       }
-
-       @Test(expected = InvalidPostReplyFound.class)
-       public void missingPostReplyTextIsRecognized() {
-               setupPostReplyWithMissingPostReplyText();
-               configurationSoneParser.parsePostReplies(null);
-       }
-
-       private void setupPostReplyWithMissingPostReplyText() {
-               setupPostReply("0", "R0", "P0", 1000L, null);
-       }
-
-       @Test
-       public void likedPostIdsParsedCorrectly() {
-               setupLikedPostIds();
-               Set<String> likedPostIds =
-                               configurationSoneParser.parseLikedPostIds();
-               assertThat(likedPostIds, containsInAnyOrder("P1", "P2", "P3"));
-       }
-
-       private void setupLikedPostIds() {
-               setupString("Sone/1/Likes/Post/0/ID", "P1");
-               setupString("Sone/1/Likes/Post/1/ID", "P2");
-               setupString("Sone/1/Likes/Post/2/ID", "P3");
-               setupString("Sone/1/Likes/Post/3/ID", null);
-       }
-
-       @Test
-       public void likedPostReplyIdsAreParsedCorrectly() {
-               setupLikedPostReplyIds();
-               Set<String> likedPostReplyIds =
-                               configurationSoneParser.parseLikedPostReplyIds();
-               assertThat(likedPostReplyIds, containsInAnyOrder("R1", "R2", "R3"));
-       }
-
-       private void setupLikedPostReplyIds() {
-               setupString("Sone/1/Likes/Reply/0/ID", "R1");
-               setupString("Sone/1/Likes/Reply/1/ID", "R2");
-               setupString("Sone/1/Likes/Reply/2/ID", "R3");
-               setupString("Sone/1/Likes/Reply/3/ID", null);
-       }
-
-       @Test
-       public void friendsAreParsedCorrectly() {
-               setupFriends();
-               Set<String> friends = configurationSoneParser.parseFriends();
-               assertThat(friends, containsInAnyOrder("F1", "F2", "F3"));
-       }
-
-       private void setupFriends() {
-               setupString("Sone/1/Friends/0/ID", "F1");
-               setupString("Sone/1/Friends/1/ID", "F2");
-               setupString("Sone/1/Friends/2/ID", "F3");
-               setupString("Sone/1/Friends/3/ID", null);
-       }
-
-       @Test
-       public void topLevelAlbumsAreParsedCorrectly() {
-               setupTopLevelAlbums();
-               AlbumBuilderFactory albumBuilderFactory = createAlbumBuilderFactory();
-               List<Album> topLevelAlbums =
-                               configurationSoneParser.parseTopLevelAlbums(
-                                               albumBuilderFactory);
-               assertThat(topLevelAlbums, hasSize(2));
-               Album firstAlbum = topLevelAlbums.get(0);
-               assertThat(firstAlbum, isAlbum("A1", null, "T1", "D1"));
-               assertThat(firstAlbum.getAlbums(), emptyIterable());
-               assertThat(firstAlbum.getImages(), emptyIterable());
-               Album secondAlbum = topLevelAlbums.get(1);
-               assertThat(secondAlbum, isAlbum("A2", null, "T2", "D2"));
-               assertThat(secondAlbum.getAlbums(), hasSize(1));
-               assertThat(secondAlbum.getImages(), emptyIterable());
-               Album thirdAlbum = secondAlbum.getAlbums().get(0);
-               assertThat(thirdAlbum, isAlbum("A3", "A2", "T3", "D3"));
-               assertThat(thirdAlbum.getAlbums(), emptyIterable());
-               assertThat(thirdAlbum.getImages(), emptyIterable());
-       }
-
-       private void setupTopLevelAlbums() {
-               setupAlbum(0, "A1", null, "T1", "D1", "I1");
-               setupAlbum(1, "A2", null, "T2", "D2", null);
-               setupAlbum(2, "A3", "A2", "T3", "D3", "I3");
-               setupAlbum(3, null, null, null, null, null);
-       }
-
-       private void setupAlbum(int albumNumber, String albumId,
-                       String parentAlbumId,
-                       String title, String description, String imageId) {
-               final String albumPrefix = "Sone/1/Albums/" + albumNumber;
-               setupString(albumPrefix + "/ID", albumId);
-               setupString(albumPrefix + "/Title", title);
-               setupString(albumPrefix + "/Description", description);
-               setupString(albumPrefix + "/Parent", parentAlbumId);
-               setupString(albumPrefix + "/AlbumImage", imageId);
-       }
-
-       private AlbumBuilderFactory createAlbumBuilderFactory() {
-               AlbumBuilderFactory albumBuilderFactory =
-                               mock(AlbumBuilderFactory.class);
-               when(albumBuilderFactory.newAlbumBuilder()).thenAnswer(
-                               new Answer<AlbumBuilder>() {
-                                       @Override
-                                       public AlbumBuilder answer(InvocationOnMock invocation) {
-                                               return new TestAlbumBuilder();
-                                       }
-                               });
-               return albumBuilderFactory;
-       }
-
-       @Test(expected = InvalidAlbumFound.class)
-       public void albumWithInvalidTitleIsRecognized() {
-               setupAlbum(0, "A1", null, null, "D1", "I1");
-               configurationSoneParser.parseTopLevelAlbums(
-                               createAlbumBuilderFactory());
-       }
-
-       @Test(expected = InvalidAlbumFound.class)
-       public void albumWithInvalidDescriptionIsRecognized() {
-               setupAlbum(0, "A1", null, "T1", null, "I1");
-               configurationSoneParser.parseTopLevelAlbums(
-                               createAlbumBuilderFactory());
-       }
-
-       @Test(expected = InvalidParentAlbumFound.class)
-       public void albumWithInvalidParentIsRecognized() {
-               setupAlbum(0, "A1", "A0", "T1", "D1", "I1");
-               configurationSoneParser.parseTopLevelAlbums(
-                               createAlbumBuilderFactory());
-       }
-
-       @Test
-       public void imagesAreParsedCorrectly() {
-               setupTopLevelAlbums();
-               configurationSoneParser.parseTopLevelAlbums(
-                               createAlbumBuilderFactory());
-               setupImages();
-               configurationSoneParser.parseImages(createImageBuilderFactory());
-               Map<String, Album> albums = configurationSoneParser.getAlbums();
-               assertThat(albums.get("A1").getImages(),
-                               contains(isImage("I1", 1000L, "K1", "T1", "D1", 16, 9)));
-               assertThat(albums.get("A2").getImages(), contains(
-                               isImage("I2", 2000L, "K2", "T2", "D2", 16 * 2, 9 * 2)));
-               assertThat(albums.get("A3").getImages(), contains(
-                               isImage("I3", 3000L, "K3", "T3", "D3", 16 * 3, 9 * 3)));
-       }
-
-       private void setupImages() {
-               setupImage(0, "I1", "A1", 1000L, "K1", "T1", "D1", 16, 9);
-               setupImage(1, "I2", "A2", 2000L, "K2", "T2", "D2", 16 * 2, 9 * 2);
-               setupImage(2, "I3", "A3", 3000L, "K3", "T3", "D3", 16 * 3, 9 * 3);
-               setupImage(3, null, null, 0L, null, null, null, 0, 0);
-       }
-
-       private void setupImage(int imageNumber, String id,
-                       String parentAlbumId, Long creationTime, String key, String title,
-                       String description, Integer width, Integer height) {
-               final String imagePrefix = "Sone/1/Images/" + imageNumber;
-               setupString(imagePrefix + "/ID", id);
-               setupString(imagePrefix + "/Album", parentAlbumId);
-               setupLong(imagePrefix + "/CreationTime", creationTime);
-               setupString(imagePrefix + "/Key", key);
-               setupString(imagePrefix + "/Title", title);
-               setupString(imagePrefix + "/Description", description);
-               setupInteger(imagePrefix + "/Width", width);
-               setupInteger(imagePrefix + "/Height", height);
-       }
-
-       private ImageBuilderFactory createImageBuilderFactory() {
-               ImageBuilderFactory imageBuilderFactory =
-                               mock(ImageBuilderFactory.class);
-               when(imageBuilderFactory.newImageBuilder()).thenAnswer(
-                               new Answer<ImageBuilder>() {
-                                       @Override
-                                       public ImageBuilder answer(InvocationOnMock invocation)
-                                       throws Throwable {
-                                               return new TestImageBuilder();
-                                       }
-                               });
-               return imageBuilderFactory;
-       }
-
-       @Test(expected = InvalidImageFound.class)
-       public void missingAlbumIdIsRecognized() {
-               setupTopLevelAlbums();
-               configurationSoneParser.parseTopLevelAlbums(
-                               createAlbumBuilderFactory());
-               setupImage(0, "I1", null, 1000L, "K1", "T1", "D1", 16, 9);
-               configurationSoneParser.parseImages(createImageBuilderFactory());
-       }
-
-       @Test(expected = InvalidParentAlbumFound.class)
-       public void invalidAlbumIdIsRecognized() {
-               setupTopLevelAlbums();
-               configurationSoneParser.parseTopLevelAlbums(
-                               createAlbumBuilderFactory());
-               setupImage(0, "I1", "A4", 1000L, "K1", "T1", "D1", 16, 9);
-               configurationSoneParser.parseImages(createImageBuilderFactory());
-       }
-
-       @Test(expected = InvalidImageFound.class)
-       public void missingCreationTimeIsRecognized() {
-               setupTopLevelAlbums();
-               configurationSoneParser.parseTopLevelAlbums(
-                               createAlbumBuilderFactory());
-               setupImage(0, "I1", "A1", null, "K1", "T1", "D1", 16, 9);
-               configurationSoneParser.parseImages(createImageBuilderFactory());
-       }
-
-       @Test(expected = InvalidImageFound.class)
-       public void missingKeyIsRecognized() {
-               setupTopLevelAlbums();
-               configurationSoneParser.parseTopLevelAlbums(
-                               createAlbumBuilderFactory());
-               setupImage(0, "I1", "A1", 1000L, null, "T1", "D1", 16, 9);
-               configurationSoneParser.parseImages(createImageBuilderFactory());
-       }
-
-       @Test(expected = InvalidImageFound.class)
-       public void missingTitleIsRecognized() {
-               setupTopLevelAlbums();
-               configurationSoneParser.parseTopLevelAlbums(
-                               createAlbumBuilderFactory());
-               setupImage(0, "I1", "A1", 1000L, "K1", null, "D1", 16, 9);
-               configurationSoneParser.parseImages(createImageBuilderFactory());
-       }
-
-       @Test(expected = InvalidImageFound.class)
-       public void missingDescriptionIsRecognized() {
-               setupTopLevelAlbums();
-               configurationSoneParser.parseTopLevelAlbums(
-                               createAlbumBuilderFactory());
-               setupImage(0, "I1", "A1", 1000L, "K1", "T1", null, 16, 9);
-               configurationSoneParser.parseImages(createImageBuilderFactory());
-       }
-
-       @Test(expected = InvalidImageFound.class)
-       public void missingWidthIsRecognized() {
-               setupTopLevelAlbums();
-               configurationSoneParser.parseTopLevelAlbums(
-                               createAlbumBuilderFactory());
-               setupImage(0, "I1", "A1", 1000L, "K1", "T1", "D1", null, 9);
-               configurationSoneParser.parseImages(createImageBuilderFactory());
-       }
-
-       @Test(expected = InvalidImageFound.class)
-       public void missingHeightIsRecognized() {
-               setupTopLevelAlbums();
-               configurationSoneParser.parseTopLevelAlbums(
-                               createAlbumBuilderFactory());
-               setupImage(0, "I1", "A1", 1000L, "K1", "T1", "D1", 16, null);
-               configurationSoneParser.parseImages(createImageBuilderFactory());
-       }
-
-}
diff --git a/src/test/java/net/pterodactylus/sone/core/CoreTest.java b/src/test/java/net/pterodactylus/sone/core/CoreTest.java
deleted file mode 100644 (file)
index 66ef743..0000000
+++ /dev/null
@@ -1,161 +0,0 @@
-package net.pterodactylus.sone.core;
-
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.inOrder;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-import static org.mockito.hamcrest.MockitoHamcrest.argThat;
-
-import net.pterodactylus.sone.core.Core.MarkPostKnown;
-import net.pterodactylus.sone.core.Core.MarkReplyKnown;
-import net.pterodactylus.sone.core.event.PostRemovedEvent;
-import net.pterodactylus.sone.core.event.PostReplyRemovedEvent;
-import net.pterodactylus.sone.core.event.SoneRemovedEvent;
-import net.pterodactylus.sone.data.Post;
-import net.pterodactylus.sone.data.PostReply;
-import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.sone.database.Database;
-import net.pterodactylus.sone.freenet.wot.Identity;
-import net.pterodactylus.sone.freenet.wot.IdentityManager;
-import net.pterodactylus.sone.freenet.wot.OwnIdentity;
-import net.pterodactylus.sone.freenet.wot.event.IdentityRemovedEvent;
-import net.pterodactylus.util.config.Configuration;
-
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableSet;
-import com.google.common.eventbus.EventBus;
-import org.hamcrest.Description;
-import org.hamcrest.Matcher;
-import org.hamcrest.TypeSafeDiagnosingMatcher;
-import org.junit.Test;
-import org.mockito.InOrder;
-
-/**
- * Unit test for {@link Core} and its subclasses.
- */
-public class CoreTest {
-
-       @Test
-       public void markPostKnownMarksPostAsKnown() {
-               Core core = mock(Core.class);
-               Post post = mock(Post.class);
-               MarkPostKnown markPostKnown = core.new MarkPostKnown(post);
-               markPostKnown.run();
-               verify(core).markPostKnown(eq(post));
-       }
-
-       @Test
-       public void markReplyKnownMarksReplyAsKnown() {
-               Core core = mock(Core.class);
-               PostReply postReply = mock(PostReply.class);
-               MarkReplyKnown markReplyKnown = core.new MarkReplyKnown(postReply);
-               markReplyKnown.run();
-               verify(core).markReplyKnown(eq(postReply));
-       }
-
-       @Test
-       public void removingAnIdentitySendsRemovalEventsForAllSoneElements() {
-               // given
-               Configuration configuration = mock(Configuration.class);
-               FreenetInterface freenetInterface = mock(FreenetInterface.class);
-               IdentityManager identityManager = mock(IdentityManager.class);
-               SoneDownloader soneDownloader = mock(SoneDownloader.class);
-               ImageInserter imageInserter = mock(ImageInserter.class);
-               UpdateChecker updateChecker = mock(UpdateChecker.class);
-               WebOfTrustUpdater webOfTrustUpdater = mock(WebOfTrustUpdater.class);
-               EventBus eventBus = mock(EventBus.class);
-               Database database = mock(Database.class);
-               Core core = new Core(configuration, freenetInterface, identityManager, soneDownloader, imageInserter, updateChecker, webOfTrustUpdater, eventBus, database);
-               OwnIdentity ownIdentity = mock(OwnIdentity.class);
-               Identity identity = mock(Identity.class);
-               when(identity.getId()).thenReturn("sone-id");
-               Sone sone = mock(Sone.class);
-               when(database.getSone("sone-id")).thenReturn(sone);
-               PostReply postReply1 = mock(PostReply.class);
-               PostReply postReply2 = mock(PostReply.class);
-               when(sone.getReplies()).thenReturn(ImmutableSet.of(postReply1, postReply2));
-               Post post1 = mock(Post.class);
-               Post post2 = mock(Post.class);
-               when(sone.getPosts()).thenReturn(ImmutableList.of(post1, post2));
-
-               // when
-               core.identityRemoved(new IdentityRemovedEvent(ownIdentity, identity));
-
-               // then
-               InOrder inOrder = inOrder(eventBus, database);
-               inOrder.verify(eventBus).post(argThat(isPostReplyRemoved(postReply1)));
-               inOrder.verify(eventBus).post(argThat(isPostReplyRemoved(postReply2)));
-               inOrder.verify(eventBus).post(argThat(isPostRemoved(post1)));
-               inOrder.verify(eventBus).post(argThat(isPostRemoved(post2)));
-               inOrder.verify(eventBus).post(argThat(isSoneRemoved(sone)));
-               inOrder.verify(database).removeSone(sone);
-       }
-
-       private Matcher<Object> isPostRemoved(final Post post) {
-               return new TypeSafeDiagnosingMatcher<Object>() {
-                       @Override
-                       protected boolean matchesSafely(Object item, Description mismatchDescription) {
-                               if (!(item instanceof PostRemovedEvent)) {
-                                       mismatchDescription.appendText("is not PostRemovedEvent");
-                                       return false;
-                               }
-                               if (((PostRemovedEvent) item).post() != post) {
-                                       mismatchDescription.appendText("post is ").appendValue(((PostRemovedEvent) item).post());
-                                       return false;
-                               }
-                               return true;
-                       }
-
-                       @Override
-                       public void describeTo(Description description) {
-                               description.appendText("is PostRemovedEvent and post is ").appendValue(post);
-                       }
-               };
-       }
-
-       private Matcher<Object> isPostReplyRemoved(final PostReply postReply) {
-               return new TypeSafeDiagnosingMatcher<Object>() {
-                       @Override
-                       protected boolean matchesSafely(Object item, Description mismatchDescription) {
-                               if (!(item instanceof PostReplyRemovedEvent)) {
-                                       mismatchDescription.appendText("is not PostReplyRemovedEvent");
-                                       return false;
-                               }
-                               if (((PostReplyRemovedEvent) item).postReply() != postReply) {
-                                       mismatchDescription.appendText("post reply is ").appendValue(((PostReplyRemovedEvent) item).postReply());
-                                       return false;
-                               }
-                               return true;
-                       }
-
-                       @Override
-                       public void describeTo(Description description) {
-                               description.appendText("is PostReplyRemovedEvent and post is ").appendValue(postReply);
-                       }
-               };
-       }
-
-       private Matcher<Object> isSoneRemoved(final Sone sone) {
-               return new TypeSafeDiagnosingMatcher<Object>() {
-                       @Override
-                       protected boolean matchesSafely(Object item, Description mismatchDescription) {
-                               if (!(item instanceof SoneRemovedEvent)) {
-                                       mismatchDescription.appendText("is not SoneRemovedEvent");
-                                       return false;
-                               }
-                               if (((SoneRemovedEvent) item).sone() != sone) {
-                                       mismatchDescription.appendText("sone is ").appendValue(((SoneRemovedEvent) item).sone());
-                                       return false;
-                               }
-                               return true;
-                       }
-
-                       @Override
-                       public void describeTo(Description description) {
-                               description.appendText("is SoneRemovedEvent and sone is ").appendValue(sone);
-                       }
-               };
-       }
-
-}
diff --git a/src/test/java/net/pterodactylus/sone/core/FreenetInterfaceTest.java b/src/test/java/net/pterodactylus/sone/core/FreenetInterfaceTest.java
deleted file mode 100644 (file)
index c91de68..0000000
+++ /dev/null
@@ -1,507 +0,0 @@
-package net.pterodactylus.sone.core;
-
-import static freenet.client.FetchException.FetchExceptionMode.ALL_DATA_NOT_FOUND;
-import static freenet.keys.InsertableClientSSK.createRandom;
-import static freenet.node.RequestStarter.INTERACTIVE_PRIORITY_CLASS;
-import static freenet.node.RequestStarter.PREFETCH_PRIORITY_CLASS;
-import static net.pterodactylus.sone.test.Matchers.delivers;
-import static net.pterodactylus.sone.test.TestUtil.setFinalField;
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.is;
-import static org.hamcrest.Matchers.notNullValue;
-import static org.hamcrest.Matchers.nullValue;
-import static org.mockito.ArgumentCaptor.forClass;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyBoolean;
-import static org.mockito.ArgumentMatchers.anyLong;
-import static org.mockito.ArgumentMatchers.anyShort;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.doNothing;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.verifyNoMoreInteractions;
-import static org.mockito.Mockito.when;
-import static org.mockito.Mockito.withSettings;
-
-import java.io.IOException;
-import java.net.MalformedURLException;
-import java.util.HashMap;
-
-import net.pterodactylus.sone.core.FreenetInterface.BackgroundFetchCallback;
-import net.pterodactylus.sone.core.FreenetInterface.Callback;
-import net.pterodactylus.sone.core.FreenetInterface.InsertToken;
-import net.pterodactylus.sone.core.FreenetInterface.InsertTokenSupplier;
-import net.pterodactylus.sone.core.event.ImageInsertAbortedEvent;
-import net.pterodactylus.sone.core.event.ImageInsertFailedEvent;
-import net.pterodactylus.sone.core.event.ImageInsertFinishedEvent;
-import net.pterodactylus.sone.core.event.ImageInsertStartedEvent;
-import net.pterodactylus.sone.data.Image;
-import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.sone.data.TemporaryImage;
-import net.pterodactylus.sone.data.impl.ImageImpl;
-import net.pterodactylus.sone.test.TestUtil;
-
-import freenet.client.ClientMetadata;
-import freenet.client.FetchContext;
-import freenet.client.FetchException;
-import freenet.client.FetchException.FetchExceptionMode;
-import freenet.client.FetchResult;
-import freenet.client.HighLevelSimpleClient;
-import freenet.client.InsertBlock;
-import freenet.client.InsertContext;
-import freenet.client.InsertException;
-import freenet.client.InsertException.InsertExceptionMode;
-import freenet.client.Metadata;
-import freenet.client.async.ClientContext;
-import freenet.client.async.ClientGetCallback;
-import freenet.client.async.ClientGetter;
-import freenet.client.async.ClientPutter;
-import freenet.client.async.SnoopMetadata;
-import freenet.client.async.USKCallback;
-import freenet.client.async.USKManager;
-import freenet.crypt.DummyRandomSource;
-import freenet.crypt.RandomSource;
-import freenet.keys.FreenetURI;
-import freenet.keys.InsertableClientSSK;
-import freenet.keys.USK;
-import freenet.node.Node;
-import freenet.node.NodeClientCore;
-import freenet.node.RequestClient;
-import freenet.support.Base64;
-import freenet.support.api.Bucket;
-import freenet.support.io.ArrayBucket;
-import freenet.support.io.ResumeFailedException;
-
-import com.google.common.eventbus.EventBus;
-import org.junit.Before;
-import org.junit.Test;
-import org.mockito.ArgumentCaptor;
-import org.mockito.ArgumentMatchers;
-
-/**
- * Unit test for {@link FreenetInterface}.
- */
-public class FreenetInterfaceTest {
-
-       private final EventBus eventBus = mock(EventBus.class);
-       private final Node node = mock(Node.class);
-       private final NodeClientCore nodeClientCore = mock(NodeClientCore.class);
-       private final HighLevelSimpleClient highLevelSimpleClient = mock(HighLevelSimpleClient.class, withSettings().extraInterfaces(RequestClient.class));
-       private final RandomSource randomSource = new DummyRandomSource();
-       private final USKManager uskManager = mock(USKManager.class);
-       private FreenetInterface freenetInterface;
-       private final Sone sone = mock(Sone.class);
-       private final ArgumentCaptor<USKCallback> callbackCaptor = forClass(USKCallback.class);
-       private final Image image = mock(Image.class);
-       private InsertToken insertToken;
-       private final Bucket bucket = mock(Bucket.class);
-       private final ArgumentCaptor<ClientGetCallback> clientGetCallback = forClass(ClientGetCallback.class);
-       private final FreenetURI uri = new FreenetURI("KSK@pgl.png");
-       private final FetchResult fetchResult = mock(FetchResult.class);
-       private final BackgroundFetchCallback backgroundFetchCallback = mock(BackgroundFetchCallback.class);
-       private final ClientGetter clientGetter = mock(ClientGetter.class);
-
-       public FreenetInterfaceTest() throws MalformedURLException {
-       }
-
-       @Before
-       public void setupHighLevelSimpleClient() throws Exception {
-               when(highLevelSimpleClient.getFetchContext()).thenReturn(mock(FetchContext.class));
-               when(highLevelSimpleClient.fetch(eq(uri), anyLong(), any(ClientGetCallback.class), any(FetchContext.class), anyShort())).thenReturn( clientGetter);
-       }
-
-       @Before
-       public void setupFreenetInterface() {
-               when(nodeClientCore.makeClient(anyShort(), anyBoolean(), anyBoolean())).thenReturn(highLevelSimpleClient);
-               setFinalField(node, "clientCore", nodeClientCore);
-               setFinalField(node, "random", randomSource);
-               setFinalField(nodeClientCore, "uskManager", uskManager);
-               setFinalField(nodeClientCore, "clientContext", mock(ClientContext.class));
-               freenetInterface = new FreenetInterface(eventBus, node);
-               insertToken = freenetInterface.new InsertToken(image);
-               insertToken.setBucket(bucket);
-       }
-
-       @Before
-       public void setupSone() {
-               InsertableClientSSK insertSsk = createRandom(randomSource, "test-0");
-               when(sone.getId()).thenReturn(Base64.encode(insertSsk.getURI().getRoutingKey()));
-               when(sone.getRequestUri()).thenReturn(insertSsk.getURI().uskForSSK());
-       }
-
-       @Before
-       public void setupCallbackCaptorAndUskManager() {
-               doNothing().when(uskManager).subscribe(any(USK.class), callbackCaptor.capture(), anyBoolean(), any(RequestClient.class));
-       }
-
-       @Test
-       public void canFetchUri() throws MalformedURLException, FetchException {
-               FreenetURI freenetUri = new FreenetURI("KSK@GPLv3.txt");
-               FetchResult fetchResult = createFetchResult();
-               when(highLevelSimpleClient.fetch(freenetUri)).thenReturn(fetchResult);
-               Fetched fetched = freenetInterface.fetchUri(freenetUri);
-               assertThat(fetched, notNullValue());
-               assertThat(fetched.getFetchResult(), is(fetchResult));
-               assertThat(fetched.getFreenetUri(), is(freenetUri));
-       }
-
-       @Test
-       public void fetchFollowsRedirect() throws MalformedURLException, FetchException {
-               FreenetURI freenetUri = new FreenetURI("KSK@GPLv2.txt");
-               FreenetURI newFreenetUri = new FreenetURI("KSK@GPLv3.txt");
-               FetchResult fetchResult = createFetchResult();
-               FetchException fetchException = new FetchException(FetchExceptionMode.PERMANENT_REDIRECT, newFreenetUri);
-               when(highLevelSimpleClient.fetch(freenetUri)).thenThrow(fetchException);
-               when(highLevelSimpleClient.fetch(newFreenetUri)).thenReturn(fetchResult);
-               Fetched fetched = freenetInterface.fetchUri(freenetUri);
-               assertThat(fetched.getFetchResult(), is(fetchResult));
-               assertThat(fetched.getFreenetUri(), is(newFreenetUri));
-       }
-
-       @Test
-       public void fetchReturnsNullOnFetchExceptions() throws MalformedURLException, FetchException {
-               FreenetURI freenetUri = new FreenetURI("KSK@GPLv2.txt");
-               FetchException fetchException = new FetchException(ALL_DATA_NOT_FOUND);
-               when(highLevelSimpleClient.fetch(freenetUri)).thenThrow(fetchException);
-               Fetched fetched = freenetInterface.fetchUri(freenetUri);
-               assertThat(fetched, nullValue());
-       }
-
-       private FetchResult createFetchResult() {
-               ClientMetadata clientMetadata = new ClientMetadata("text/plain");
-               Bucket bucket = new ArrayBucket("Some Data.".getBytes());
-               return new FetchResult(clientMetadata, bucket);
-       }
-
-       @Test
-       public void insertingAnImage() throws SoneException, InsertException, IOException {
-               TemporaryImage temporaryImage = new TemporaryImage("image-id");
-               temporaryImage.setMimeType("image/png");
-               byte[] imageData = new byte[] { 1, 2, 3, 4 };
-               temporaryImage.setImageData(imageData);
-               Image image = new ImageImpl("image-id");
-               InsertToken insertToken = freenetInterface.new InsertToken(image);
-               InsertContext insertContext = mock(InsertContext.class);
-               when(highLevelSimpleClient.getInsertContext(anyBoolean())).thenReturn(insertContext);
-               ClientPutter clientPutter = mock(ClientPutter.class);
-               ArgumentCaptor<InsertBlock> insertBlockCaptor = forClass(InsertBlock.class);
-               when(highLevelSimpleClient.insert(insertBlockCaptor.capture(), eq((String) null), eq(false), eq(insertContext), eq(insertToken), anyShort())).thenReturn(clientPutter);
-               freenetInterface.insertImage(temporaryImage, image, insertToken);
-               assertThat(insertBlockCaptor.getValue().getData().getInputStream(), delivers(new byte[] { 1, 2, 3, 4 }));
-               assertThat(TestUtil.<ClientPutter>getPrivateField(insertToken, "clientPutter"), is(clientPutter));
-               verify(eventBus).post(any(ImageInsertStartedEvent.class));
-       }
-
-       @Test(expected = SoneInsertException.class)
-       public void insertExceptionCausesASoneException() throws InsertException, SoneException, IOException {
-               TemporaryImage temporaryImage = new TemporaryImage("image-id");
-               temporaryImage.setMimeType("image/png");
-               byte[] imageData = new byte[] { 1, 2, 3, 4 };
-               temporaryImage.setImageData(imageData);
-               Image image = new ImageImpl("image-id");
-               InsertToken insertToken = freenetInterface.new InsertToken(image);
-               InsertContext insertContext = mock(InsertContext.class);
-               when(highLevelSimpleClient.getInsertContext(anyBoolean())).thenReturn(insertContext);
-               ArgumentCaptor<InsertBlock> insertBlockCaptor = forClass(InsertBlock.class);
-               when(highLevelSimpleClient.insert(insertBlockCaptor.capture(), eq((String) null), eq(false), eq(insertContext), eq(insertToken), anyShort())).thenThrow(InsertException.class);
-               freenetInterface.insertImage(temporaryImage, image, insertToken);
-       }
-
-       @Test
-       public void insertingADirectory() throws InsertException, SoneException {
-               FreenetURI freenetUri = mock(FreenetURI.class);
-               HashMap<String, Object> manifestEntries = new HashMap<>();
-               String defaultFile = "index.html";
-               FreenetURI resultingUri = mock(FreenetURI.class);
-               when(highLevelSimpleClient.insertManifest(eq(freenetUri), eq(manifestEntries), eq(defaultFile))).thenReturn(resultingUri);
-               assertThat(freenetInterface.insertDirectory(freenetUri, manifestEntries, defaultFile), is(resultingUri));
-       }
-
-       @Test(expected = SoneException.class)
-       public void insertExceptionIsForwardedAsSoneException() throws InsertException, SoneException {
-               when(highLevelSimpleClient.insertManifest(ArgumentMatchers.<FreenetURI>any(), ArgumentMatchers.<HashMap<String, Object>>any(), ArgumentMatchers.<String>any())).thenThrow(InsertException.class);
-               freenetInterface.insertDirectory(null, null, null);
-       }
-
-       @Test
-       public void soneWithWrongRequestUriWillNotBeSubscribed() throws MalformedURLException {
-               when(sone.getRequestUri()).thenReturn(new FreenetURI("KSK@GPLv3.txt"));
-               freenetInterface.registerUsk(new FreenetURI("KSK@GPLv3.txt"), null);
-               verify(uskManager, never()).subscribe(any(USK.class), any(USKCallback.class), anyBoolean(), any(RequestClient.class));
-       }
-
-       @Test
-       public void registeringAUsk() {
-               FreenetURI freenetUri = createRandom(randomSource, "test-0").getURI().uskForSSK();
-               Callback callback = mock(Callback.class);
-               freenetInterface.registerUsk(freenetUri, callback);
-               verify(uskManager).subscribe(any(USK.class), any(USKCallback.class), anyBoolean(), any(RequestClient.class));
-       }
-
-       @Test
-       public void registeringANonUskKeyWillNotBeSubscribed() throws MalformedURLException {
-               FreenetURI freenetUri = new FreenetURI("KSK@GPLv3.txt");
-               Callback callback = mock(Callback.class);
-               freenetInterface.registerUsk(freenetUri, callback);
-               verify(uskManager, never()).subscribe(any(USK.class), any(USKCallback.class), anyBoolean(), any(RequestClient.class));
-       }
-
-       @Test
-       public void registeringAnActiveUskWillSubscribeToItCorrectly() {
-               FreenetURI freenetUri = createRandom(randomSource, "test-0").getURI().uskForSSK();
-               final USKCallback uskCallback = mock(USKCallback.class);
-               freenetInterface.registerActiveUsk(freenetUri, uskCallback);
-               verify(uskManager).subscribe(any(USK.class), eq(uskCallback), eq(true), any(RequestClient.class));
-       }
-
-       @Test
-       public void registeringAnInactiveUskWillSubscribeToItCorrectly() {
-               FreenetURI freenetUri = createRandom(randomSource, "test-0").getURI().uskForSSK();
-               final USKCallback uskCallback = mock(USKCallback.class);
-               freenetInterface.registerPassiveUsk(freenetUri, uskCallback);
-               verify(uskManager).subscribe(any(USK.class), eq(uskCallback), eq(false), any(RequestClient.class));
-       }
-
-       @Test
-       public void registeringAnActiveNonUskWillNotSubscribeToAUsk()
-       throws MalformedURLException {
-               FreenetURI freenetUri = createRandom(randomSource, "test-0").getURI();
-               freenetInterface.registerActiveUsk(freenetUri, null);
-               verify(uskManager, never()).subscribe(any(USK.class), any(USKCallback.class), anyBoolean(), any(RequestClient.class));
-       }
-
-       @Test
-       public void registeringAnInactiveNonUskWillNotSubscribeToAUsk()
-       throws MalformedURLException {
-               FreenetURI freenetUri = createRandom(randomSource, "test-0").getURI();
-               freenetInterface.registerPassiveUsk(freenetUri, null);
-               verify(uskManager, never()).subscribe(any(USK.class), any(USKCallback.class), anyBoolean(), any(RequestClient.class));
-       }
-
-       @Test
-       public void unregisteringANotRegisteredUskDoesNothing() {
-               FreenetURI freenetURI = createRandom(randomSource, "test-0").getURI().uskForSSK();
-               freenetInterface.unregisterUsk(freenetURI);
-               verify(uskManager, never()).unsubscribe(any(USK.class), any(USKCallback.class));
-       }
-
-       @Test
-       public void unregisteringARegisteredUsk() {
-               FreenetURI freenetURI = createRandom(randomSource, "test-0").getURI().uskForSSK();
-               Callback callback = mock(Callback.class);
-               freenetInterface.registerUsk(freenetURI, callback);
-               freenetInterface.unregisterUsk(freenetURI);
-               verify(uskManager).unsubscribe(any(USK.class), any(USKCallback.class));
-       }
-
-       @Test
-       public void unregisteringANotRegisteredSoneDoesNothing() {
-               freenetInterface.unregisterUsk(sone);
-               verify(uskManager, never()).unsubscribe(any(USK.class), any(USKCallback.class));
-       }
-
-       @Test
-       public void unregisteringARegisteredSoneUnregistersTheSone()
-       throws MalformedURLException {
-               freenetInterface.registerActiveUsk(sone.getRequestUri(), mock(USKCallback.class));
-               freenetInterface.unregisterUsk(sone);
-               verify(uskManager).unsubscribe(any(USK.class), any(USKCallback.class));
-       }
-
-       @Test
-       public void unregisteringASoneWithAWrongRequestKeyWillNotUnsubscribe() throws MalformedURLException {
-               when(sone.getRequestUri()).thenReturn(new FreenetURI("KSK@GPLv3.txt"));
-               freenetInterface.registerUsk(sone.getRequestUri(), null);
-               freenetInterface.unregisterUsk(sone);
-               verify(uskManager, never()).unsubscribe(any(USK.class), any(USKCallback.class));
-       }
-
-       @Test
-       public void callbackForNormalUskUsesDifferentPriorities() {
-               Callback callback = mock(Callback.class);
-               FreenetURI soneUri = createRandom(randomSource, "test-0").getURI().uskForSSK();
-               freenetInterface.registerUsk(soneUri, callback);
-               assertThat(callbackCaptor.getValue().getPollingPriorityNormal(), is(PREFETCH_PRIORITY_CLASS));
-               assertThat(callbackCaptor.getValue().getPollingPriorityProgress(), is(INTERACTIVE_PRIORITY_CLASS));
-       }
-
-       @Test
-       public void callbackForNormalUskForwardsImportantParameters() throws MalformedURLException {
-               Callback callback = mock(Callback.class);
-               FreenetURI uri = createRandom(randomSource, "test-0").getURI().uskForSSK();
-               freenetInterface.registerUsk(uri, callback);
-               USK key = mock(USK.class);
-               when(key.getURI()).thenReturn(uri);
-               callbackCaptor.getValue().onFoundEdition(3, key, null, false, (short) 0, null, true, true);
-               verify(callback).editionFound(eq(uri), eq(3L), eq(true), eq(true));
-       }
-
-       @Test
-       public void fetchedRetainsUriAndFetchResult() {
-               FreenetURI freenetUri = mock(FreenetURI.class);
-               FetchResult fetchResult = mock(FetchResult.class);
-               Fetched fetched = new Fetched(freenetUri, fetchResult);
-               assertThat(fetched.getFreenetUri(), is(freenetUri));
-               assertThat(fetched.getFetchResult(), is(fetchResult));
-       }
-
-       @Test
-       public void cancellingAnInsertWillFireImageInsertAbortedEvent() {
-               ClientPutter clientPutter = mock(ClientPutter.class);
-               insertToken.setClientPutter(clientPutter);
-               ArgumentCaptor<ImageInsertStartedEvent> imageInsertStartedEvent = forClass(ImageInsertStartedEvent.class);
-               verify(eventBus).post(imageInsertStartedEvent.capture());
-               assertThat(imageInsertStartedEvent.getValue().image(), is(image));
-               insertToken.cancel();
-               ArgumentCaptor<ImageInsertAbortedEvent> imageInsertAbortedEvent = forClass(ImageInsertAbortedEvent.class);
-               verify(eventBus, times(2)).post(imageInsertAbortedEvent.capture());
-               verify(bucket).free();
-               assertThat(imageInsertAbortedEvent.getValue().image(), is(image));
-       }
-
-       @Test
-       public void failureWithoutExceptionSendsFailedEvent() {
-               insertToken.onFailure(null, null);
-               ArgumentCaptor<ImageInsertFailedEvent> imageInsertFailedEvent = forClass(ImageInsertFailedEvent.class);
-               verify(eventBus).post(imageInsertFailedEvent.capture());
-               verify(bucket).free();
-               assertThat(imageInsertFailedEvent.getValue().image(), is(image));
-               assertThat(imageInsertFailedEvent.getValue().cause(), nullValue());
-       }
-
-       @Test
-       public void failureSendsFailedEventWithException() {
-               InsertException insertException = new InsertException(InsertExceptionMode.INTERNAL_ERROR, "Internal error", null);
-               insertToken.onFailure(insertException, null);
-               ArgumentCaptor<ImageInsertFailedEvent> imageInsertFailedEvent = forClass(ImageInsertFailedEvent.class);
-               verify(eventBus).post(imageInsertFailedEvent.capture());
-               verify(bucket).free();
-               assertThat(imageInsertFailedEvent.getValue().image(), is(image));
-               assertThat(imageInsertFailedEvent.getValue().cause(), is((Throwable) insertException));
-       }
-
-       @Test
-       public void failureBecauseCancelledByUserSendsAbortedEvent() {
-               InsertException insertException = new InsertException(InsertExceptionMode.CANCELLED, null);
-               insertToken.onFailure(insertException, null);
-               ArgumentCaptor<ImageInsertAbortedEvent> imageInsertAbortedEvent = forClass(ImageInsertAbortedEvent.class);
-               verify(eventBus).post(imageInsertAbortedEvent.capture());
-               verify(bucket).free();
-               assertThat(imageInsertAbortedEvent.getValue().image(), is(image));
-       }
-
-       @Test
-       public void ignoredMethodsDoNotThrowExceptions() throws ResumeFailedException {
-               insertToken.onResume(null);
-               insertToken.onFetchable(null);
-               insertToken.onGeneratedMetadata(null, null);
-       }
-
-       @Test
-       public void generatedUriIsPostedOnSuccess() {
-               FreenetURI generatedUri = mock(FreenetURI.class);
-               insertToken.onGeneratedURI(generatedUri, null);
-               insertToken.onSuccess(null);
-               ArgumentCaptor<ImageInsertFinishedEvent> imageInsertFinishedEvent = forClass(ImageInsertFinishedEvent.class);
-               verify(eventBus).post(imageInsertFinishedEvent.capture());
-               verify(bucket).free();
-               assertThat(imageInsertFinishedEvent.getValue().image(), is(image));
-               assertThat(imageInsertFinishedEvent.getValue().resultingUri(), is(generatedUri));
-       }
-
-       @Test
-       public void insertTokenSupplierSuppliesInsertTokens() {
-               InsertTokenSupplier insertTokenSupplier = new InsertTokenSupplier(freenetInterface);
-               assertThat(insertTokenSupplier.apply(image), notNullValue());
-       }
-
-       @Test
-       public void backgroundFetchCanBeStarted() throws Exception {
-               freenetInterface.startFetch(uri, backgroundFetchCallback);
-               verify(highLevelSimpleClient).fetch(eq(uri), anyLong(), any(ClientGetCallback.class), any(FetchContext.class), anyShort());
-       }
-
-       @Test
-       public void backgroundFetchRegistersSnoopAndRestartsTheRequest() throws Exception {
-               freenetInterface.startFetch(uri, backgroundFetchCallback);
-               verify(clientGetter).setMetaSnoop(any(SnoopMetadata.class));
-               verify(clientGetter).restart(eq(uri), anyBoolean(), any(ClientContext.class));
-       }
-
-       @Test
-       public void requestIsNotCancelledForImageMimeType() {
-               verifySnoopCancelsRequestForMimeType("image/png", false);
-               verify(backgroundFetchCallback, never()).failed(uri);
-       }
-
-       @Test
-       public void requestIsCancelledForNullMimeType() {
-               verifySnoopCancelsRequestForMimeType(null, true);
-               verify(backgroundFetchCallback, never()).shouldCancel(eq(uri), ArgumentMatchers.<String>any(), anyLong());
-               verify(backgroundFetchCallback).failed(uri);
-       }
-
-       @Test
-       public void requestIsCancelledForVideoMimeType() {
-               verifySnoopCancelsRequestForMimeType("video/mkv", true);
-               verify(backgroundFetchCallback).failed(uri);
-       }
-
-       @Test
-       public void requestIsCancelledForAudioMimeType() {
-               verifySnoopCancelsRequestForMimeType("audio/mpeg", true);
-               verify(backgroundFetchCallback).failed(uri);
-       }
-
-       @Test
-       public void requestIsCancelledForTextMimeType() {
-               verifySnoopCancelsRequestForMimeType("text/plain", true);
-               verify(backgroundFetchCallback).failed(uri);
-       }
-
-       private void verifySnoopCancelsRequestForMimeType(String mimeType, boolean cancel) {
-               when(backgroundFetchCallback.shouldCancel(eq(uri), eq(mimeType), anyLong())).thenReturn(cancel);
-               freenetInterface.startFetch(uri, backgroundFetchCallback);
-               ArgumentCaptor<SnoopMetadata> snoopMetadata = forClass(SnoopMetadata.class);
-               verify(clientGetter).setMetaSnoop(snoopMetadata.capture());
-               Metadata metadata = mock(Metadata.class);
-               when(metadata.getMIMEType()).thenReturn(mimeType);
-               assertThat(snoopMetadata.getValue().snoopMetadata(metadata, mock(ClientContext.class)), is(cancel));
-       }
-
-       @Test
-       public void callbackOfBackgroundFetchIsNotifiedOnSuccess() throws Exception {
-               freenetInterface.startFetch(uri, backgroundFetchCallback);
-               verify(highLevelSimpleClient).fetch(eq(uri), anyLong(), clientGetCallback.capture(), any(FetchContext.class), anyShort());
-               when(fetchResult.getMimeType()).thenReturn("image/png");
-               when(fetchResult.asByteArray()).thenReturn(new byte[] { 1, 2, 3, 4, 5 });
-               clientGetCallback.getValue().onSuccess(fetchResult, mock(ClientGetter.class));
-               verify(backgroundFetchCallback).loaded(uri, "image/png", new byte[] { 1, 2, 3, 4, 5 });
-               verifyNoMoreInteractions(backgroundFetchCallback);
-       }
-
-       @Test
-       public void callbackOfBackgroundFetchIsNotifiedOnFailure() throws Exception {
-               freenetInterface.startFetch(uri, backgroundFetchCallback);
-               verify(highLevelSimpleClient).fetch(eq(uri), anyLong(), clientGetCallback.capture(), any(FetchContext.class), anyShort());
-               when(fetchResult.getMimeType()).thenReturn("image/png");
-               when(fetchResult.asByteArray()).thenReturn(new byte[] { 1, 2, 3, 4, 5 });
-               clientGetCallback.getValue().onFailure(new FetchException(ALL_DATA_NOT_FOUND), mock(ClientGetter.class));
-               verify(backgroundFetchCallback).failed(uri);
-               verifyNoMoreInteractions(backgroundFetchCallback);
-       }
-
-       @Test
-       public void callbackOfBackgroundFetchIsNotifiedAsFailureIfBucketCanNotBeLoaded() throws Exception {
-               freenetInterface.startFetch(uri, backgroundFetchCallback);
-               verify(highLevelSimpleClient).fetch(eq(uri), anyLong(), clientGetCallback.capture(), any(FetchContext.class), anyShort());
-               when(fetchResult.getMimeType()).thenReturn("image/png");
-               when(fetchResult.asByteArray()).thenThrow(IOException.class);
-               clientGetCallback.getValue().onSuccess(fetchResult, mock(ClientGetter.class));
-               verify(backgroundFetchCallback).failed(uri);
-               verifyNoMoreInteractions(backgroundFetchCallback);
-       }
-
-}
diff --git a/src/test/java/net/pterodactylus/sone/core/PreferencesLoaderTest.java b/src/test/java/net/pterodactylus/sone/core/PreferencesLoaderTest.java
deleted file mode 100644 (file)
index d550f25..0000000
+++ /dev/null
@@ -1,80 +0,0 @@
-package net.pterodactylus.sone.core;
-
-import static net.pterodactylus.sone.fcp.FcpInterface.FullAccessRequired.WRITING;
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.is;
-import static org.hamcrest.Matchers.not;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
-
-import net.pterodactylus.sone.test.TestValue;
-import net.pterodactylus.util.config.Configuration;
-
-import com.google.common.eventbus.EventBus;
-import org.junit.Before;
-import org.junit.Test;
-
-/**
- * Unit test for {@link PreferencesLoader}.
- */
-public class PreferencesLoaderTest {
-
-       private final EventBus eventBus = mock(EventBus.class);
-       private final Preferences preferences = new Preferences(eventBus);
-       private final Configuration configuration = mock(Configuration.class);
-       private final PreferencesLoader preferencesLoader =
-                       new PreferencesLoader(preferences);
-
-       @Before
-       public void setupConfiguration() {
-               setupIntValue("InsertionDelay", 15);
-               setupIntValue("PostsPerPage", 25);
-               setupIntValue("ImagesPerPage", 12);
-               setupIntValue("CharactersPerPost", 150);
-               setupIntValue("PostCutOffLength", 300);
-               setupBooleanValue("RequireFullAccess", true);
-               setupIntValue("PositiveTrust", 50);
-               setupIntValue("NegativeTrust", -50);
-               when(configuration.getStringValue("Option/TrustComment")).thenReturn(
-                               TestValue.from("Trusted"));
-               setupBooleanValue("ActivateFcpInterface", true);
-               setupIntValue("FcpFullAccessRequired", 1);
-       }
-
-       private void setupIntValue(String optionName, int value) {
-               when(configuration.getIntValue("Option/" + optionName)).thenReturn(
-                               TestValue.from(value));
-       }
-
-       private void setupBooleanValue(String optionName, boolean value) {
-               when(configuration.getBooleanValue(
-                               "Option/" + optionName)).thenReturn(
-                               TestValue.from(value));
-       }
-
-       @Test
-       public void configurationIsLoadedCorrectly() {
-               setupConfiguration();
-               preferencesLoader.loadFrom(configuration);
-               assertThat(preferences.getInsertionDelay(), is(15));
-               assertThat(preferences.getPostsPerPage(), is(25));
-               assertThat(preferences.getImagesPerPage(), is(12));
-               assertThat(preferences.getCharactersPerPost(), is(150));
-               assertThat(preferences.getPostCutOffLength(), is(300));
-               assertThat(preferences.getRequireFullAccess(), is(true));
-               assertThat(preferences.getPositiveTrust(), is(50));
-               assertThat(preferences.getNegativeTrust(), is(-50));
-               assertThat(preferences.getTrustComment(), is("Trusted"));
-               assertThat(preferences.getFcpInterfaceActive(), is(true));
-               assertThat(preferences.getFcpFullAccessRequired(), is(WRITING));
-       }
-
-       @Test
-       public void configurationIsLoadedCorrectlyWithCutOffLengthMinusOne() {
-           setupConfiguration();
-               setupIntValue("PostCutOffLength", -1);
-               preferencesLoader.loadFrom(configuration);
-               assertThat(preferences.getPostCutOffLength(), not(is(-1)));
-       }
-
-}
diff --git a/src/test/java/net/pterodactylus/sone/core/SoneInserterTest.java b/src/test/java/net/pterodactylus/sone/core/SoneInserterTest.java
deleted file mode 100644 (file)
index 31d7b6b..0000000
+++ /dev/null
@@ -1,286 +0,0 @@
-package net.pterodactylus.sone.core;
-
-import static com.google.common.io.ByteStreams.toByteArray;
-import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
-import static java.lang.System.currentTimeMillis;
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.containsString;
-import static org.hamcrest.Matchers.instanceOf;
-import static org.hamcrest.Matchers.is;
-import static org.hamcrest.Matchers.nullValue;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.doAnswer;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-import static org.mockito.hamcrest.MockitoHamcrest.argThat;
-
-import java.io.IOException;
-import java.util.HashMap;
-import java.util.Map;
-
-import net.pterodactylus.sone.core.SoneInserter.ManifestCreator;
-import net.pterodactylus.sone.core.event.InsertionDelayChangedEvent;
-import net.pterodactylus.sone.core.event.SoneEvent;
-import net.pterodactylus.sone.core.event.SoneInsertAbortedEvent;
-import net.pterodactylus.sone.core.event.SoneInsertedEvent;
-import net.pterodactylus.sone.core.event.SoneInsertingEvent;
-import net.pterodactylus.sone.data.Album;
-import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.sone.main.SonePlugin;
-
-import freenet.keys.FreenetURI;
-import freenet.support.api.ManifestElement;
-
-import com.google.common.base.Charsets;
-import com.google.common.base.Optional;
-import com.google.common.eventbus.AsyncEventBus;
-import com.google.common.eventbus.EventBus;
-import org.junit.Before;
-import org.junit.Test;
-import org.mockito.ArgumentCaptor;
-import org.mockito.invocation.InvocationOnMock;
-import org.mockito.stubbing.Answer;
-
-/**
- * Unit test for {@link SoneInserter} and its subclasses.
- */
-public class SoneInserterTest {
-
-       private final Core core = mock(Core.class);
-       private final EventBus eventBus = mock(EventBus.class);
-       private final FreenetInterface freenetInterface = mock(FreenetInterface.class);
-
-       @Before
-       public void setupCore() {
-               UpdateChecker updateChecker = mock(UpdateChecker.class);
-               when(core.getUpdateChecker()).thenReturn(updateChecker);
-               when(core.getSone(anyString())).thenReturn(null);
-       }
-
-       @Test
-       public void insertionDelayIsForwardedToSoneInserter() {
-               EventBus eventBus = new AsyncEventBus(directExecutor());
-               eventBus.register(new SoneInserter(core, eventBus, freenetInterface, "SoneId"));
-               eventBus.post(new InsertionDelayChangedEvent(15));
-               assertThat(SoneInserter.getInsertionDelay().get(), is(15));
-       }
-
-       private Sone createSone(FreenetURI insertUri, String fingerprint) {
-               Sone sone = mock(Sone.class);
-               when(sone.getInsertUri()).thenReturn(insertUri);
-               when(sone.getFingerprint()).thenReturn(fingerprint);
-               when(sone.getRootAlbum()).thenReturn(mock(Album.class));
-               when(core.getSone(anyString())).thenReturn(sone);
-               return sone;
-       }
-
-       @Test
-       public void isModifiedIsTrueIfModificationDetectorSaysSo() {
-               SoneModificationDetector soneModificationDetector = mock(SoneModificationDetector.class);
-               when(soneModificationDetector.isModified()).thenReturn(true);
-               SoneInserter soneInserter = new SoneInserter(core, eventBus, freenetInterface, "SoneId", soneModificationDetector, 1);
-               assertThat(soneInserter.isModified(), is(true));
-       }
-
-       @Test
-       public void isModifiedIsFalseIfModificationDetectorSaysSo() {
-               SoneModificationDetector soneModificationDetector = mock(SoneModificationDetector.class);
-               SoneInserter soneInserter = new SoneInserter(core, eventBus, freenetInterface, "SoneId", soneModificationDetector, 1);
-               assertThat(soneInserter.isModified(), is(false));
-       }
-
-       @Test
-       public void lastFingerprintIsStoredCorrectly() {
-               SoneInserter soneInserter = new SoneInserter(core, eventBus, freenetInterface, "SoneId");
-               soneInserter.setLastInsertFingerprint("last-fingerprint");
-               assertThat(soneInserter.getLastInsertFingerprint(), is("last-fingerprint"));
-       }
-
-       @Test
-       public void soneInserterStopsWhenItShould() {
-               SoneInserter soneInserter = new SoneInserter(core, eventBus, freenetInterface, "SoneId");
-               soneInserter.stop();
-               soneInserter.serviceRun();
-       }
-
-       @Test
-       public void soneInserterInsertsASoneIfItIsEligible() throws SoneException {
-               FreenetURI insertUri = mock(FreenetURI.class);
-               final FreenetURI finalUri = mock(FreenetURI.class);
-               String fingerprint = "fingerprint";
-               Sone sone = createSone(insertUri, fingerprint);
-               SoneModificationDetector soneModificationDetector = mock(SoneModificationDetector.class);
-               when(soneModificationDetector.isEligibleForInsert()).thenReturn(true);
-               when(freenetInterface.insertDirectory(eq(insertUri), any(HashMap.class), eq("index.html"))).thenReturn(finalUri);
-               final SoneInserter soneInserter = new SoneInserter(core, eventBus, freenetInterface, "SoneId", soneModificationDetector, 1);
-               doAnswer(new Answer<Void>() {
-                       @Override
-                       public Void answer(InvocationOnMock invocation) throws Throwable {
-                               soneInserter.stop();
-                               return null;
-                       }
-               }).when(core).touchConfiguration();
-               soneInserter.serviceRun();
-               ArgumentCaptor<SoneEvent> soneEvents = ArgumentCaptor.forClass(SoneEvent.class);
-               verify(freenetInterface).insertDirectory(eq(insertUri), any(HashMap.class), eq("index.html"));
-               verify(eventBus, times(2)).post(soneEvents.capture());
-               assertThat(soneEvents.getAllValues().get(0), instanceOf(SoneInsertingEvent.class));
-               assertThat(soneEvents.getAllValues().get(0).sone(), is(sone));
-               assertThat(soneEvents.getAllValues().get(1), instanceOf(SoneInsertedEvent.class));
-               assertThat(soneEvents.getAllValues().get(1).sone(), is(sone));
-       }
-
-       @Test
-       public void soneInserterBailsOutIfItIsStoppedWhileInserting() throws SoneException {
-               FreenetURI insertUri = mock(FreenetURI.class);
-               final FreenetURI finalUri = mock(FreenetURI.class);
-               String fingerprint = "fingerprint";
-               Sone sone = createSone(insertUri, fingerprint);
-               SoneModificationDetector soneModificationDetector = mock(SoneModificationDetector.class);
-               when(soneModificationDetector.isEligibleForInsert()).thenReturn(true);
-               final SoneInserter soneInserter = new SoneInserter(core, eventBus, freenetInterface, "SoneId", soneModificationDetector, 1);
-               when(freenetInterface.insertDirectory(eq(insertUri), any(HashMap.class), eq("index.html"))).thenAnswer(new Answer<FreenetURI>() {
-                       @Override
-                       public FreenetURI answer(InvocationOnMock invocation) throws Throwable {
-                               soneInserter.stop();
-                               return finalUri;
-                       }
-               });
-               soneInserter.serviceRun();
-               ArgumentCaptor<SoneEvent> soneEvents = ArgumentCaptor.forClass(SoneEvent.class);
-               verify(freenetInterface).insertDirectory(eq(insertUri), any(HashMap.class), eq("index.html"));
-               verify(eventBus, times(2)).post(soneEvents.capture());
-               assertThat(soneEvents.getAllValues().get(0), instanceOf(SoneInsertingEvent.class));
-               assertThat(soneEvents.getAllValues().get(0).sone(), is(sone));
-               assertThat(soneEvents.getAllValues().get(1), instanceOf(SoneInsertedEvent.class));
-               assertThat(soneEvents.getAllValues().get(1).sone(), is(sone));
-               verify(core, never()).touchConfiguration();
-       }
-
-       @Test
-       public void soneInserterDoesNotInsertSoneIfItIsNotEligible() throws SoneException {
-               FreenetURI insertUri = mock(FreenetURI.class);
-               String fingerprint = "fingerprint";
-               Sone sone = createSone(insertUri, fingerprint);
-               SoneModificationDetector soneModificationDetector = mock(SoneModificationDetector.class);
-               final SoneInserter soneInserter = new SoneInserter(core, eventBus, freenetInterface, "SoneId", soneModificationDetector, 1);
-               new Thread(new Runnable() {
-                       @Override
-                       public void run() {
-                               try {
-                                       Thread.sleep(500);
-                               } catch (InterruptedException ie1) {
-                                       throw new RuntimeException(ie1);
-                               }
-                               soneInserter.stop();
-                       }
-               }).start();
-               soneInserter.serviceRun();
-               verify(freenetInterface, never()).insertDirectory(eq(insertUri), any(HashMap.class), eq("index.html"));
-               verify(eventBus, never()).post(argThat(org.hamcrest.Matchers.any(SoneEvent.class)));
-       }
-
-       @Test
-       public void soneInserterPostsAbortedEventIfAnExceptionOccurs() throws SoneException {
-               FreenetURI insertUri = mock(FreenetURI.class);
-               String fingerprint = "fingerprint";
-               Sone sone = createSone(insertUri, fingerprint);
-               SoneModificationDetector soneModificationDetector = mock(SoneModificationDetector.class);
-               when(soneModificationDetector.isEligibleForInsert()).thenReturn(true);
-               final SoneInserter soneInserter = new SoneInserter(core, eventBus, freenetInterface, "SoneId", soneModificationDetector, 1);
-               final SoneException soneException = new SoneException(new Exception());
-               when(freenetInterface.insertDirectory(eq(insertUri), any(HashMap.class), eq("index.html"))).thenAnswer(new Answer<FreenetURI>() {
-                       @Override
-                       public FreenetURI answer(InvocationOnMock invocation) throws Throwable {
-                               soneInserter.stop();
-                               throw soneException;
-                       }
-               });
-               soneInserter.serviceRun();
-               ArgumentCaptor<SoneEvent> soneEvents = ArgumentCaptor.forClass(SoneEvent.class);
-               verify(freenetInterface).insertDirectory(eq(insertUri), any(HashMap.class), eq("index.html"));
-               verify(eventBus, times(2)).post(soneEvents.capture());
-               assertThat(soneEvents.getAllValues().get(0), instanceOf(SoneInsertingEvent.class));
-               assertThat(soneEvents.getAllValues().get(0).sone(), is(sone));
-               assertThat(soneEvents.getAllValues().get(1), instanceOf(SoneInsertAbortedEvent.class));
-               assertThat(soneEvents.getAllValues().get(1).sone(), is(sone));
-               verify(core, never()).touchConfiguration();
-       }
-
-       @Test
-       public void soneInserterExitsIfSoneIsUnknown() {
-               SoneModificationDetector soneModificationDetector =
-                               mock(SoneModificationDetector.class);
-               SoneInserter soneInserter =
-                               new SoneInserter(core, eventBus, freenetInterface, "SoneId",
-                                               soneModificationDetector, 1);
-               when(soneModificationDetector.isEligibleForInsert()).thenReturn(true);
-               when(core.getSone("SoneId")).thenReturn(null);
-               soneInserter.serviceRun();
-       }
-
-       @Test
-       public void soneInserterCatchesExceptionAndContinues() {
-               SoneModificationDetector soneModificationDetector =
-                               mock(SoneModificationDetector.class);
-               final SoneInserter soneInserter =
-                               new SoneInserter(core, eventBus, freenetInterface, "SoneId",
-                                               soneModificationDetector, 1);
-               Answer<Optional<Sone>> stopInserterAndThrowException =
-                               new Answer<Optional<Sone>>() {
-                                       @Override
-                                       public Optional<Sone> answer(
-                                                       InvocationOnMock invocation) {
-                                               soneInserter.stop();
-                                               throw new NullPointerException();
-                                       }
-                               };
-               when(soneModificationDetector.isEligibleForInsert()).thenAnswer(
-                               stopInserterAndThrowException);
-               soneInserter.serviceRun();
-       }
-
-       @Test
-       public void templateIsRenderedCorrectlyForManifestElement()
-       throws IOException {
-               Map<String, Object> soneProperties = new HashMap<>();
-               soneProperties.put("id", "SoneId");
-               ManifestCreator manifestCreator = new ManifestCreator(core, soneProperties);
-               long now = currentTimeMillis();
-               when(core.getStartupTime()).thenReturn(now);
-               ManifestElement manifestElement = manifestCreator.createManifestElement("test.txt", "plain/text; charset=utf-8", "sone-inserter-manifest.txt");
-               assertThat(manifestElement.getName(), is("test.txt"));
-               assertThat(manifestElement.getMimeTypeOverride(), is("plain/text; charset=utf-8"));
-               String templateContent = new String(toByteArray(manifestElement.getData().getInputStream()), Charsets.UTF_8);
-               assertThat(templateContent, containsString("Sone Version: " + SonePlugin.getPluginVersion() + "\n"));
-               assertThat(templateContent, containsString("Core Startup: " + now + "\n"));
-               assertThat(templateContent, containsString("Sone ID: " + "SoneId" + "\n"));
-       }
-
-       @Test
-       public void invalidTemplateReturnsANullManifestElement() {
-               Map<String, Object> soneProperties = new HashMap<>();
-               ManifestCreator manifestCreator = new ManifestCreator(core, soneProperties);
-               assertThat(manifestCreator.createManifestElement("test.txt",
-                               "plain/text; charset=utf-8",
-                               "sone-inserter-invalid-manifest.txt"),
-                               nullValue());
-       }
-
-       @Test
-       public void errorWhileRenderingTemplateReturnsANullManifestElement() {
-               Map<String, Object> soneProperties = new HashMap<>();
-               ManifestCreator manifestCreator = new ManifestCreator(core, soneProperties);
-               when(core.toString()).thenThrow(NullPointerException.class);
-               assertThat(manifestCreator.createManifestElement("test.txt",
-                               "plain/text; charset=utf-8",
-                               "sone-inserter-faulty-manifest.txt"),
-                               nullValue());
-       }
-
-}
diff --git a/src/test/java/net/pterodactylus/sone/core/SoneParserTest.java b/src/test/java/net/pterodactylus/sone/core/SoneParserTest.java
deleted file mode 100644 (file)
index 400b9ae..0000000
+++ /dev/null
@@ -1,820 +0,0 @@
-package net.pterodactylus.sone.core;
-
-import static com.google.common.base.Optional.of;
-import static freenet.keys.InsertableClientSSK.createRandom;
-import static java.lang.System.currentTimeMillis;
-import static java.util.UUID.randomUUID;
-import static java.util.concurrent.TimeUnit.DAYS;
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.hasSize;
-import static org.hamcrest.Matchers.is;
-import static org.hamcrest.Matchers.not;
-import static org.hamcrest.Matchers.notNullValue;
-import static org.hamcrest.Matchers.nullValue;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyLong;
-import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.Mockito.doAnswer;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
-
-import java.io.InputStream;
-import java.net.MalformedURLException;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-
-import net.pterodactylus.sone.data.Album;
-import net.pterodactylus.sone.data.Album.Modifier;
-import net.pterodactylus.sone.data.Client;
-import net.pterodactylus.sone.data.Image;
-import net.pterodactylus.sone.data.Post;
-import net.pterodactylus.sone.data.PostReply;
-import net.pterodactylus.sone.data.Profile;
-import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.sone.database.AlbumBuilder;
-import net.pterodactylus.sone.database.Database;
-import net.pterodactylus.sone.database.ImageBuilder;
-import net.pterodactylus.sone.database.PostBuilder;
-import net.pterodactylus.sone.database.PostReplyBuilder;
-import net.pterodactylus.sone.database.SoneBuilder;
-import net.pterodactylus.sone.database.memory.MemorySoneBuilder;
-import net.pterodactylus.sone.freenet.wot.Identity;
-import net.pterodactylus.sone.freenet.wot.OwnIdentity;
-
-import freenet.crypt.DummyRandomSource;
-import freenet.keys.FreenetURI;
-import freenet.keys.InsertableClientSSK;
-
-import com.google.common.base.Optional;
-import com.google.common.collect.ArrayListMultimap;
-import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.ListMultimap;
-import org.junit.Before;
-import org.junit.Test;
-import org.mockito.invocation.InvocationOnMock;
-import org.mockito.stubbing.Answer;
-
-/**
- * Unit test for {@link SoneParser}.
- */
-public class SoneParserTest {
-
-       private final Database database = mock(Database.class);
-       private final SoneParser soneParser = new SoneParser(database);
-       private final Sone sone = mock(Sone.class);
-       private FreenetURI requestUri = mock(FreenetURI.class);
-       private final PostBuilder postBuilder = mock(PostBuilder.class);
-       private final List<Post> createdPosts = new ArrayList<>();
-       private Post post = mock(Post.class);
-       private final PostReplyBuilder postReplyBuilder = mock(PostReplyBuilder.class);
-       private final Set<PostReply> createdPostReplies = new HashSet<>();
-       private PostReply postReply = mock(PostReply.class);
-       private final AlbumBuilder albumBuilder = mock(AlbumBuilder.class);
-       private final ListMultimap<Album, Album>
-                       nestedAlbums = ArrayListMultimap.create();
-       private final ListMultimap<Album, Image> albumImages = ArrayListMultimap.create();
-       private Album album = mock(Album.class);
-       private final Map<String, Album> albums = new HashMap<>();
-       private final ImageBuilder imageBuilder = mock(ImageBuilder.class);
-       private Image image = mock(Image.class);
-       private final Map<String, Image> images = new HashMap<>();
-
-       @Before
-       public void setupSone() {
-               setupSone(this.sone, Identity.class);
-       }
-
-       private void setupSone(Sone sone, Class<? extends Identity> identityClass) {
-               Identity identity = mock(identityClass);
-               InsertableClientSSK clientSSK =
-                               createRandom(new DummyRandomSource(), "WoT");
-               when(identity.getRequestUri()).thenReturn(clientSSK.getURI().toString());
-               when(identity.getId()).thenReturn("identity");
-               when(sone.getId()).thenReturn("identity");
-               when(sone.getIdentity()).thenReturn(identity);
-               requestUri = clientSSK.getURI().setKeyType("USK").setDocName("Sone");
-               when(sone.getRequestUri()).thenAnswer(new Answer<FreenetURI>() {
-                       @Override
-                       public FreenetURI answer(InvocationOnMock invocation)
-                       throws Throwable {
-                               return requestUri;
-                       }
-               });
-               when(sone.getTime())
-                               .thenReturn(currentTimeMillis() - DAYS.toMillis(1));
-       }
-
-       @Before
-       public void setupSoneBuilder() {
-               when(database.newSoneBuilder()).thenAnswer(new Answer<SoneBuilder>() {
-                       @Override
-                       public SoneBuilder answer(InvocationOnMock invocation) {
-                               return new MemorySoneBuilder(null);
-                       }
-               });
-       }
-
-       @Before
-       public void setupPost() {
-               when(post.getRecipientId()).thenReturn(Optional.<String>absent());
-       }
-
-       @Before
-       public void setupPostBuilder() {
-               when(postBuilder.withId(anyString())).thenAnswer(new Answer<PostBuilder>() {
-                       @Override
-                       public PostBuilder answer(InvocationOnMock invocation) throws Throwable {
-                               when(post.getId()).thenReturn((String) invocation.getArguments()[0]);
-                               return postBuilder;
-                       }
-               });
-               when(postBuilder.from(anyString())).thenAnswer(new Answer<PostBuilder>() {
-                       @Override
-                       public PostBuilder answer(InvocationOnMock invocation) throws Throwable {
-                               final Sone sone = mock(Sone.class);
-                               when(sone.getId()).thenReturn((String) invocation.getArguments()[0]);
-                               when(post.getSone()).thenReturn(sone);
-                               return postBuilder;
-                       }
-               });
-               when(postBuilder.withTime(anyLong())).thenAnswer(new Answer<PostBuilder>() {
-                       @Override
-                       public PostBuilder answer(InvocationOnMock invocation) throws Throwable {
-                               when(post.getTime()).thenReturn((Long) invocation.getArguments()[0]);
-                               return postBuilder;
-                       }
-               });
-               when(postBuilder.withText(anyString())).thenAnswer(new Answer<PostBuilder>() {
-                       @Override
-                       public PostBuilder answer(InvocationOnMock invocation) throws Throwable {
-                               when(post.getText()).thenReturn((String) invocation.getArguments()[0]);
-                               return postBuilder;
-                       }
-               });
-               when(postBuilder.to(anyString())).thenAnswer(new Answer<PostBuilder>() {
-                       @Override
-                       public PostBuilder answer(InvocationOnMock invocation) throws Throwable {
-                               when(post.getRecipientId()).thenReturn(of((String) invocation.getArguments()[0]));
-                               return postBuilder;
-                       }
-               });
-               when(postBuilder.build()).thenAnswer(new Answer<Post>() {
-                       @Override
-                       public Post answer(InvocationOnMock invocation) throws Throwable {
-                               Post post = SoneParserTest.this.post;
-                               SoneParserTest.this.post = mock(Post.class);
-                               setupPost();
-                               createdPosts.add(post);
-                               return post;
-                       }
-               });
-               when(database.newPostBuilder()).thenReturn(postBuilder);
-       }
-
-       @Before
-       public void setupPostReplyBuilder() {
-               when(postReplyBuilder.withId(anyString())).thenAnswer(new Answer<PostReplyBuilder>() {
-                       @Override
-                       public PostReplyBuilder answer(InvocationOnMock invocation) throws Throwable {
-                               when(postReply.getId()).thenReturn((String) invocation.getArguments()[0]);
-                               return postReplyBuilder;
-                       }
-               });
-               when(postReplyBuilder.from(anyString())).thenAnswer(
-                               new Answer<PostReplyBuilder>() {
-                                       @Override
-                                       public PostReplyBuilder answer(
-                                                       InvocationOnMock invocation) throws Throwable {
-                                               Sone sone = when(mock(Sone.class).getId()).thenReturn(
-                                                               (String) invocation.getArguments()[0])
-                                                               .getMock();
-                                               when(postReply.getSone()).thenReturn(sone);
-                                               return postReplyBuilder;
-                                       }
-                               });
-               when(postReplyBuilder.to(anyString())).thenAnswer(
-                               new Answer<PostReplyBuilder>() {
-                                       @Override
-                                       public PostReplyBuilder answer(
-                                                       InvocationOnMock invocation) throws Throwable {
-                                               when(postReply.getPostId()).thenReturn(
-                                                               (String) invocation.getArguments()[0]);
-                                               Post post = when(mock(Post.class).getId()).thenReturn(
-                                                               (String) invocation.getArguments()[0])
-                                                               .getMock();
-                                               when(postReply.getPost()).thenReturn(of(post));
-                                               return postReplyBuilder;
-                                       }
-                               });
-               when(postReplyBuilder.withTime(anyLong())).thenAnswer(
-                               new Answer<PostReplyBuilder>() {
-                                       @Override
-                                       public PostReplyBuilder answer(
-                                                       InvocationOnMock invocation) throws Throwable {
-                                               when(postReply.getTime()).thenReturn(
-                                                               (Long) invocation.getArguments()[0]);
-                                               return postReplyBuilder;
-                                       }
-                               });
-               when(postReplyBuilder.withText(anyString())).thenAnswer(new Answer<PostReplyBuilder>() {
-                       @Override
-                       public PostReplyBuilder answer(InvocationOnMock invocation) throws Throwable {
-                               when(postReply.getText()).thenReturn((String) invocation.getArguments()[0]);
-                               return postReplyBuilder;
-                       }
-               });
-               when(postReplyBuilder.build()).thenAnswer(new Answer<PostReply>() {
-                       @Override
-                       public PostReply answer(InvocationOnMock invocation) throws Throwable {
-                               PostReply postReply = SoneParserTest.this.postReply;
-                               createdPostReplies.add(postReply);
-                               SoneParserTest.this.postReply = mock(PostReply.class);
-                               return postReply;
-                       }
-               });
-               when(database.newPostReplyBuilder()).thenReturn(postReplyBuilder);
-       }
-
-       @Before
-       public void setupAlbum() {
-               final Album album = SoneParserTest.this.album;
-               doAnswer(new Answer<Void>() {
-                       @Override
-                       public Void answer(InvocationOnMock invocation) {
-                               nestedAlbums.put(album, (Album) invocation.getArguments()[0]);
-                               return null;
-                       }
-               }).when(album).addAlbum(any(Album.class));
-               doAnswer(new Answer<Void>() {
-                       @Override
-                       public Void answer(InvocationOnMock invocation) {
-                               albumImages.put(album, (Image) invocation.getArguments()[0]);
-                               return null;
-                       }
-               }).when(album).addImage(any(Image.class));
-               when(album.getAlbums()).thenAnswer(new Answer<List<Album>>() {
-                       @Override
-                       public List<Album> answer(InvocationOnMock invocation) {
-                               return nestedAlbums.get(album);
-                       }
-               });
-               when(album.getImages()).thenAnswer(new Answer<List<Image>>() {
-                       @Override
-                       public List<Image> answer(InvocationOnMock invocation) {
-                               return albumImages.get(album);
-                       }
-               });
-               final Modifier albumModifier = new Modifier() {
-                       private String title = album.getTitle();
-                       private String description = album.getDescription();
-
-                       @Override
-                       public Modifier setTitle(String title) {
-                               this.title = title;
-                               return this;
-                       }
-
-                       @Override
-                       public Modifier setDescription(String description) {
-                               this.description = description;
-                               return this;
-                       }
-
-                       @Override
-                       public Album update() throws IllegalStateException {
-                               when(album.getTitle()).thenReturn(title);
-                               when(album.getDescription()).thenReturn(description);
-                               return album;
-                       }
-               };
-               when(album.modify()).thenReturn(albumModifier);
-       }
-
-       @Before
-       public void setupAlbumBuilder() {
-               when(albumBuilder.withId(anyString())).thenAnswer(new Answer<AlbumBuilder>() {
-                       @Override
-                       public AlbumBuilder answer(InvocationOnMock invocation) {
-                               when(album.getId()).thenReturn((String) invocation.getArguments()[0]);
-                               return albumBuilder;
-                       }
-               });
-               when(albumBuilder.randomId()).thenAnswer(new Answer<AlbumBuilder>() {
-                       @Override
-                       public AlbumBuilder answer(InvocationOnMock invocation) {
-                               when(album.getId()).thenReturn(randomUUID().toString());
-                               return albumBuilder;
-                       }
-               });
-               when(albumBuilder.by(any(Sone.class))).thenAnswer(new Answer<AlbumBuilder>() {
-                       @Override
-                       public AlbumBuilder answer(InvocationOnMock invocation) {
-                               when(album.getSone()).thenReturn((Sone) invocation.getArguments()[0]);
-                               return albumBuilder;
-                       }
-               });
-               when(albumBuilder.build()).thenAnswer(new Answer<Album>() {
-                       @Override
-                       public Album answer(InvocationOnMock invocation) {
-                               Album album = SoneParserTest.this.album;
-                               albums.put(album.getId(), album);
-                               SoneParserTest.this.album = mock(Album.class);
-                               setupAlbum();
-                               return album;
-                       }
-               });
-               when(database.newAlbumBuilder()).thenReturn(albumBuilder);
-       }
-
-       @Before
-       public void setupAlbums() {
-               when(database.getAlbum(anyString())).thenAnswer(new Answer<Album>() {
-                       @Override
-                       public Album answer(InvocationOnMock invocation)
-                       throws Throwable {
-                               return albums.get(invocation.getArguments()[0]);
-                       }
-               });
-       }
-
-       @Before
-       public void setupImage() {
-               final Image image = SoneParserTest.this.image;
-               Image.Modifier modifier = new Image.Modifier() {
-                       private Sone sone = image.getSone();
-                       private long creationTime = image.getCreationTime();
-                       private String key = image.getKey();
-                       private String title = image.getTitle();
-                       private String description = image.getDescription();
-                       private int width = image.getWidth();
-                       private int height = image.getHeight();
-
-                       @Override
-                       public Image.Modifier setSone(Sone sone) {
-                               this.sone = sone;
-                               return this;
-                       }
-
-                       @Override
-                       public Image.Modifier setCreationTime(long creationTime) {
-                               this.creationTime = creationTime;
-                               return this;
-                       }
-
-                       @Override
-                       public Image.Modifier setKey(String key) {
-                               this.key = key;
-                               return this;
-                       }
-
-                       @Override
-                       public Image.Modifier setTitle(String title) {
-                               this.title = title;
-                               return this;
-                       }
-
-                       @Override
-                       public Image.Modifier setDescription(String description) {
-                               this.description = description;
-                               return this;
-                       }
-
-                       @Override
-                       public Image.Modifier setWidth(int width) {
-                               this.width = width;
-                               return this;
-                       }
-
-                       @Override
-                       public Image.Modifier setHeight(int height) {
-                               this.height = height;
-                               return this;
-                       }
-
-                       @Override
-                       public Image update() throws IllegalStateException {
-                               when(image.getSone()).thenReturn(sone);
-                               when(image.getCreationTime()).thenReturn(creationTime);
-                               when(image.getKey()).thenReturn(key);
-                               when(image.getTitle()).thenReturn(title);
-                               when(image.getDescription()).thenReturn(description);
-                               when(image.getWidth()).thenReturn(width);
-                               when(image.getHeight()).thenReturn(height);
-                               return image;
-                       }
-               };
-               when(image.getSone()).thenReturn(sone);
-               when(image.modify()).thenReturn(modifier);
-       }
-
-       @Before
-       public void setupImageBuilder() {
-               when(imageBuilder.randomId()).thenAnswer(new Answer<ImageBuilder>() {
-                       @Override
-                       public ImageBuilder answer(InvocationOnMock invocation) {
-                               when(image.getId()).thenReturn(randomUUID().toString());
-                               return imageBuilder;
-                       }
-               });
-               when(imageBuilder.withId(anyString())).thenAnswer(new Answer<ImageBuilder>() {
-                       @Override
-                       public ImageBuilder answer(InvocationOnMock invocation) {
-                               when(image.getId()).thenReturn(
-                                               (String) invocation.getArguments()[0]);
-                               return imageBuilder;
-                       }
-               });
-               when(imageBuilder.build()).thenAnswer(new Answer<Image>() {
-                       @Override
-                       public Image answer(InvocationOnMock invocation) {
-                               Image image = SoneParserTest.this.image;
-                               images.put(image.getId(), image);
-                               SoneParserTest.this.image = mock(Image.class);
-                               setupImage();
-                               return image;
-                       }
-               });
-               when(database.newImageBuilder()).thenReturn(imageBuilder);
-       }
-
-       @Before
-       public void setupImages() {
-               when(database.getImage(anyString())).thenAnswer(new Answer<Image>() {
-                       @Override
-                       public Image answer(InvocationOnMock invocation)
-                       throws Throwable {
-                               return images.get(invocation.getArguments()[0]);
-                       }
-               });
-       }
-       @Test
-       public void parsingASoneFailsWhenDocumentIsNotXml() throws SoneException {
-               InputStream inputStream = getClass().getResourceAsStream("sone-parser-not-xml.xml");
-               assertThat(soneParser.parseSone(sone, inputStream), nullValue());
-       }
-
-       @Test
-       public void parsingASoneFailsWhenDocumentHasNegativeProtocolVersion() throws SoneException {
-               InputStream inputStream = getClass().getResourceAsStream("sone-parser-negative-protocol-version.xml");
-               assertThat(soneParser.parseSone(sone, inputStream), nullValue());
-       }
-
-       @Test
-       public void parsingASoneFailsWhenProtocolVersionIsTooLarge() throws SoneException {
-               InputStream inputStream = getClass().getResourceAsStream("sone-parser-too-large-protocol-version.xml");
-               assertThat(soneParser.parseSone(sone, inputStream), nullValue());
-       }
-
-       @Test
-       public void parsingASoneFailsWhenThereIsNoTime() throws SoneException {
-               InputStream inputStream = getClass().getResourceAsStream("sone-parser-no-time.xml");
-               assertThat(soneParser.parseSone(sone, inputStream), nullValue());
-       }
-
-       @Test
-       public void parsingASoneFailsWhenTimeIsNotNumeric() throws SoneException {
-               InputStream inputStream = getClass().getResourceAsStream("sone-parser-time-not-numeric.xml");
-               assertThat(soneParser.parseSone(sone, inputStream), nullValue());
-       }
-
-       @Test
-       public void parsingASoneFailsWhenProfileIsMissing() throws SoneException {
-               InputStream inputStream = getClass().getResourceAsStream("sone-parser-no-profile.xml");
-               assertThat(soneParser.parseSone(sone, inputStream), nullValue());
-       }
-
-       @Test
-       public void parsingASoneFailsWhenProfileFieldIsMissingAFieldName() throws SoneException {
-               InputStream inputStream = getClass().getResourceAsStream("sone-parser-profile-missing-field-name.xml");
-               assertThat(soneParser.parseSone(sone, inputStream), nullValue());
-       }
-
-       @Test
-       public void parsingASoneFailsWhenProfileFieldNameIsEmpty() throws SoneException {
-               InputStream inputStream = getClass().getResourceAsStream("sone-parser-profile-empty-field-name.xml");
-               assertThat(soneParser.parseSone(sone, inputStream), nullValue());
-       }
-
-       @Test
-       public void parsingASoneFailsWhenProfileFieldNameIsNotUnique() throws SoneException {
-               InputStream inputStream = getClass().getResourceAsStream("sone-parser-profile-duplicate-field-name.xml");
-               assertThat(soneParser.parseSone(sone, inputStream), nullValue());
-       }
-
-       @Test
-       public void parsingASoneSucceedsWithoutPayload() throws SoneException {
-               InputStream inputStream = getClass().getResourceAsStream("sone-parser-no-payload.xml");
-               assertThat(soneParser.parseSone(sone, inputStream).getTime(), is(
-                               1407197508000L));
-       }
-
-       @Test
-       public void parsingALocalSoneSucceedsWithoutPayload() throws SoneException {
-               InputStream inputStream = getClass().getResourceAsStream("sone-parser-no-payload.xml");
-               Sone localSone = mock(Sone.class);
-               setupSone(localSone, OwnIdentity.class);
-               when(localSone.isLocal()).thenReturn(true);
-               Sone parsedSone = soneParser.parseSone(localSone, inputStream);
-               assertThat(parsedSone.getTime(), is(1407197508000L));
-               assertThat(parsedSone.isLocal(), is(true));
-       }
-
-       @Test
-       public void parsingASoneSucceedsWithoutProtocolVersion() throws SoneException {
-               InputStream inputStream = getClass().getResourceAsStream("sone-parser-missing-protocol-version.xml");
-               assertThat(soneParser.parseSone(sone, inputStream), not(
-                               nullValue()));
-       }
-
-       @Test
-       public void parsingASoneFailsWithMissingClientName() throws SoneException {
-               InputStream inputStream = getClass().getResourceAsStream("sone-parser-missing-client-name.xml");
-               assertThat(soneParser.parseSone(sone, inputStream), nullValue());
-       }
-
-       @Test
-       public void parsingASoneFailsWithMissingClientVersion() throws SoneException {
-               InputStream inputStream = getClass().getResourceAsStream("sone-parser-missing-client-version.xml");
-               assertThat(soneParser.parseSone(sone, inputStream), nullValue());
-       }
-
-       @Test
-       public void parsingASoneSucceedsWithClientInfo() throws SoneException {
-               InputStream inputStream = getClass().getResourceAsStream("sone-parser-with-client-info.xml");
-               assertThat(soneParser.parseSone(sone, inputStream).getClient(), is(new Client("some-client", "some-version")));
-       }
-
-       @Test
-       public void parsingASoneSucceedsWithProfile() throws SoneException,
-       MalformedURLException {
-               InputStream inputStream = getClass().getResourceAsStream("sone-parser-with-profile.xml");
-               final Profile profile = soneParser.parseSone(sone, inputStream).getProfile();
-               assertThat(profile.getFirstName(), is("first"));
-               assertThat(profile.getMiddleName(), is("middle"));
-               assertThat(profile.getLastName(), is("last"));
-               assertThat(profile.getBirthDay(), is(18));
-               assertThat(profile.getBirthMonth(), is(12));
-               assertThat(profile.getBirthYear(), is(1976));
-       }
-
-       @Test
-       public void parsingASoneSucceedsWithoutProfileFields() throws SoneException, MalformedURLException {
-               InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-fields.xml");
-               assertThat(soneParser.parseSone(sone, inputStream), notNullValue());
-       }
-
-       @Test
-       public void parsingASoneFailsWithoutPostId() throws SoneException, MalformedURLException {
-               InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-post-id.xml");
-               assertThat(soneParser.parseSone(sone, inputStream), nullValue());
-       }
-
-       @Test
-       public void parsingASoneFailsWithoutPostTime() throws SoneException, MalformedURLException {
-               InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-post-time.xml");
-               assertThat(soneParser.parseSone(sone, inputStream), nullValue());
-       }
-
-       @Test
-       public void parsingASoneFailsWithoutPostText() throws SoneException, MalformedURLException {
-               InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-post-text.xml");
-               assertThat(soneParser.parseSone(sone, inputStream), nullValue());
-       }
-
-       @Test
-       public void parsingASoneFailsWithInvalidPostTime() throws SoneException, MalformedURLException {
-               InputStream inputStream = getClass().getResourceAsStream("sone-parser-with-invalid-post-time.xml");
-               assertThat(soneParser.parseSone(sone, inputStream), nullValue());
-       }
-
-       @Test
-       public void parsingASoneSucceedsWithValidPostTime() throws SoneException, MalformedURLException {
-               InputStream inputStream = getClass().getResourceAsStream("sone-parser-with-valid-post-time.xml");
-               final List<Post> posts = soneParser.parseSone(sone, inputStream).getPosts();
-               assertThat(posts, is(createdPosts));
-               assertThat(posts.get(0).getSone().getId(), is(sone.getId()));
-               assertThat(posts.get(0).getId(), is("post-id"));
-               assertThat(posts.get(0).getTime(), is(1407197508000L));
-               assertThat(posts.get(0).getRecipientId(), is(Optional.<String>absent()));
-               assertThat(posts.get(0).getText(), is("text"));
-       }
-
-       @Test
-       public void parsingASoneSucceedsWithRecipient() throws SoneException, MalformedURLException {
-               InputStream inputStream = getClass().getResourceAsStream("sone-parser-with-recipient.xml");
-               final List<Post> posts = soneParser.parseSone(sone, inputStream).getPosts();
-               assertThat(posts, is(createdPosts));
-               assertThat(posts.get(0).getSone().getId(), is(sone.getId()));
-               assertThat(posts.get(0).getId(), is("post-id"));
-               assertThat(posts.get(0).getTime(), is(1407197508000L));
-               assertThat(posts.get(0).getRecipientId(), is(of(
-                               "1234567890123456789012345678901234567890123")));
-               assertThat(posts.get(0).getText(), is("text"));
-       }
-
-       @Test
-       public void parsingASoneSucceedsWithInvalidRecipient() throws SoneException, MalformedURLException {
-               InputStream inputStream = getClass().getResourceAsStream("sone-parser-with-invalid-recipient.xml");
-               final List<Post> posts = soneParser.parseSone(sone, inputStream).getPosts();
-               assertThat(posts, is(createdPosts));
-               assertThat(posts.get(0).getSone().getId(), is(sone.getId()));
-               assertThat(posts.get(0).getId(), is("post-id"));
-               assertThat(posts.get(0).getTime(), is(1407197508000L));
-               assertThat(posts.get(0).getRecipientId(), is(Optional.<String>absent()));
-               assertThat(posts.get(0).getText(), is("text"));
-       }
-
-       @Test
-       public void parsingASoneFailsWithoutPostReplyId() throws SoneException, MalformedURLException {
-               InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-post-reply-id.xml");
-               assertThat(soneParser.parseSone(sone, inputStream), nullValue());
-       }
-
-       @Test
-       public void parsingASoneFailsWithoutPostReplyPostId() throws SoneException, MalformedURLException {
-               InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-post-reply-post-id.xml");
-               assertThat(soneParser.parseSone(sone, inputStream), nullValue());
-       }
-
-       @Test
-       public void parsingASoneFailsWithoutPostReplyTime() throws SoneException, MalformedURLException {
-               InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-post-reply-time.xml");
-               assertThat(soneParser.parseSone(sone, inputStream), nullValue());
-       }
-
-       @Test
-       public void parsingASoneFailsWithoutPostReplyText() throws SoneException, MalformedURLException {
-               InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-post-reply-text.xml");
-               assertThat(soneParser.parseSone(sone, inputStream), nullValue());
-       }
-
-       @Test
-       public void parsingASoneFailsWithInvalidPostReplyTime() throws SoneException, MalformedURLException {
-               InputStream inputStream = getClass().getResourceAsStream("sone-parser-with-invalid-post-reply-time.xml");
-               assertThat(soneParser.parseSone(sone, inputStream), nullValue());
-       }
-
-       @Test
-       public void parsingASoneSucceedsWithValidPostReplyTime() throws SoneException, MalformedURLException {
-               InputStream inputStream = getClass().getResourceAsStream("sone-parser-with-valid-post-reply-time.xml");
-               final Set<PostReply> postReplies = soneParser.parseSone(sone, inputStream).getReplies();
-               assertThat(postReplies, is(createdPostReplies));
-               PostReply postReply = createdPostReplies.iterator().next();
-               assertThat(postReply.getId(), is("reply-id"));
-               assertThat(postReply.getPostId(), is("post-id"));
-               assertThat(postReply.getPost().get().getId(), is("post-id"));
-               assertThat(postReply.getSone().getId(), is("identity"));
-               assertThat(postReply.getTime(), is(1407197508000L));
-               assertThat(postReply.getText(), is("reply-text"));
-       }
-
-       @Test
-       public void parsingASoneSucceedsWithoutLikedPostIds() throws SoneException, MalformedURLException {
-               InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-liked-post-ids.xml");
-               assertThat(soneParser.parseSone(sone, inputStream), not(
-                               nullValue()));
-       }
-
-       @Test
-       public void parsingASoneSucceedsWithLikedPostIds() throws SoneException, MalformedURLException {
-               InputStream inputStream = getClass().getResourceAsStream("sone-parser-with-liked-post-ids.xml");
-               assertThat(soneParser.parseSone(sone, inputStream).getLikedPostIds(), is(
-                               (Set<String>) ImmutableSet.of("liked-post-id")));
-       }
-
-       @Test
-       public void parsingASoneSucceedsWithoutLikedPostReplyIds() throws SoneException, MalformedURLException {
-               InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-liked-post-reply-ids.xml");
-               assertThat(soneParser.parseSone(sone, inputStream), not(
-                               nullValue()));
-       }
-
-       @Test
-       public void parsingASoneSucceedsWithLikedPostReplyIds() throws SoneException, MalformedURLException {
-               InputStream inputStream = getClass().getResourceAsStream("sone-parser-with-liked-post-reply-ids.xml");
-               assertThat(soneParser.parseSone(sone, inputStream).getLikedReplyIds(), is(
-                               (Set<String>) ImmutableSet.of("liked-post-reply-id")));
-       }
-
-       @Test
-       public void parsingASoneSucceedsWithoutAlbums() throws SoneException, MalformedURLException {
-               InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-albums.xml");
-               assertThat(soneParser.parseSone(sone, inputStream), not(
-                               nullValue()));
-       }
-
-       @Test
-       public void parsingASoneFailsWithoutAlbumId() throws SoneException, MalformedURLException {
-               InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-album-id.xml");
-               assertThat(soneParser.parseSone(sone, inputStream), nullValue());
-       }
-
-       @Test
-       public void parsingASoneFailsWithoutAlbumTitle() throws SoneException, MalformedURLException {
-               InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-album-title.xml");
-               assertThat(soneParser.parseSone(sone, inputStream), nullValue());
-       }
-
-       @Test
-       public void parsingASoneSucceedsWithNestedAlbums() throws SoneException, MalformedURLException {
-               InputStream inputStream = getClass().getResourceAsStream("sone-parser-with-multiple-albums.xml");
-               final Sone parsedSone = soneParser.parseSone(sone, inputStream);
-               assertThat(parsedSone, not(nullValue()));
-               assertThat(parsedSone.getRootAlbum().getAlbums(), hasSize(1));
-               Album album = parsedSone.getRootAlbum().getAlbums().get(0);
-               assertThat(album.getId(), is("album-id-1"));
-               assertThat(album.getTitle(), is("album-title"));
-               assertThat(album.getDescription(), is("album-description"));
-               assertThat(album.getAlbums(), hasSize(1));
-               Album nestedAlbum = album.getAlbums().get(0);
-               assertThat(nestedAlbum.getId(), is("album-id-2"));
-               assertThat(nestedAlbum.getTitle(), is("album-title-2"));
-               assertThat(nestedAlbum.getDescription(), is("album-description-2"));
-               assertThat(nestedAlbum.getAlbums(), hasSize(0));
-       }
-
-       @Test
-       public void parsingASoneFailsWithInvalidParentAlbumId() throws SoneException, MalformedURLException {
-               InputStream inputStream = getClass().getResourceAsStream("sone-parser-with-invalid-parent-album-id.xml");
-               assertThat(soneParser.parseSone(sone, inputStream), nullValue());
-       }
-
-       @Test
-       public void parsingASoneSucceedsWithoutImages() throws SoneException, MalformedURLException {
-               InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-images.xml");
-               assertThat(soneParser.parseSone(sone, inputStream), not(
-                               nullValue()));
-       }
-
-       @Test
-       public void parsingASoneFailsWithoutImageId() throws SoneException, MalformedURLException {
-               InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-image-id.xml");
-               assertThat(soneParser.parseSone(sone, inputStream), nullValue());
-       }
-
-       @Test
-       public void parsingASoneFailsWithoutImageTime() throws SoneException, MalformedURLException {
-               InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-image-time.xml");
-               assertThat(soneParser.parseSone(sone, inputStream), nullValue());
-       }
-
-       @Test
-       public void parsingASoneFailsWithoutImageKey() throws SoneException, MalformedURLException {
-               InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-image-key.xml");
-               assertThat(soneParser.parseSone(sone, inputStream), nullValue());
-       }
-
-       @Test
-       public void parsingASoneFailsWithoutImageTitle() throws SoneException, MalformedURLException {
-               InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-image-title.xml");
-               assertThat(soneParser.parseSone(sone, inputStream), nullValue());
-       }
-
-       @Test
-       public void parsingASoneFailsWithoutImageWidth() throws SoneException, MalformedURLException {
-               InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-image-width.xml");
-               assertThat(soneParser.parseSone(sone, inputStream), nullValue());
-       }
-
-       @Test
-       public void parsingASoneFailsWithoutImageHeight() throws SoneException, MalformedURLException {
-               InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-image-height.xml");
-               assertThat(soneParser.parseSone(sone, inputStream), nullValue());
-       }
-
-       @Test
-       public void parsingASoneFailsWithInvalidImageWidth() throws SoneException, MalformedURLException {
-               InputStream inputStream = getClass().getResourceAsStream("sone-parser-with-invalid-image-width.xml");
-               assertThat(soneParser.parseSone(sone, inputStream), nullValue());
-       }
-
-       @Test
-       public void parsingASoneFailsWithInvalidImageHeight() throws SoneException, MalformedURLException {
-               InputStream inputStream = getClass().getResourceAsStream("sone-parser-with-invalid-image-height.xml");
-               assertThat(soneParser.parseSone(sone, inputStream), nullValue());
-       }
-
-       @Test
-       public void parsingASoneSucceedsWithImage() throws SoneException, MalformedURLException {
-               InputStream inputStream = getClass().getResourceAsStream("sone-parser-with-image.xml");
-               final Sone sone = soneParser.parseSone(this.sone, inputStream);
-               assertThat(sone, not(nullValue()));
-               assertThat(sone.getRootAlbum().getAlbums(), hasSize(1));
-               assertThat(sone.getRootAlbum().getAlbums().get(0).getImages(), hasSize(1));
-               Image image = sone.getRootAlbum().getAlbums().get(0).getImages().get(0);
-               assertThat(image.getId(), is("image-id"));
-               assertThat(image.getCreationTime(), is(1407197508000L));
-               assertThat(image.getKey(), is("KSK@GPLv3.txt"));
-               assertThat(image.getTitle(), is("image-title"));
-               assertThat(image.getDescription(), is("image-description"));
-               assertThat(image.getWidth(), is(1920));
-               assertThat(image.getHeight(), is(1080));
-               assertThat(sone.getProfile().getAvatar(), is("image-id"));
-       }
-
-
-}
diff --git a/src/test/java/net/pterodactylus/sone/core/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());
-       }
-
-}
diff --git a/src/test/java/net/pterodactylus/sone/core/UpdateCheckerTest.java b/src/test/java/net/pterodactylus/sone/core/UpdateCheckerTest.java
deleted file mode 100644 (file)
index e7f1afe..0000000
+++ /dev/null
@@ -1,260 +0,0 @@
-package net.pterodactylus.sone.core;
-
-import static java.lang.Long.MAX_VALUE;
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.instanceOf;
-import static org.hamcrest.Matchers.is;
-import static org.mockito.ArgumentCaptor.forClass;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-import static org.mockito.hamcrest.MockitoHamcrest.argThat;
-
-import java.io.IOException;
-import java.io.InputStream;
-
-import net.pterodactylus.sone.core.FreenetInterface.Callback;
-import net.pterodactylus.sone.core.event.UpdateFoundEvent;
-import net.pterodactylus.sone.main.PluginHomepage;
-import net.pterodactylus.util.version.Version;
-
-import freenet.client.ClientMetadata;
-import freenet.client.FetchResult;
-import freenet.keys.FreenetURI;
-import freenet.support.api.Bucket;
-import freenet.support.io.ArrayBucket;
-
-import com.google.common.eventbus.EventBus;
-import org.junit.Before;
-import org.junit.Test;
-import org.mockito.ArgumentCaptor;
-import org.mockito.invocation.InvocationOnMock;
-import org.mockito.stubbing.Answer;
-
-/**
- * Unit test for {@link UpdateChecker}.
- */
-public class UpdateCheckerTest {
-
-       private final EventBus eventBus = mock(EventBus.class);
-       private final FreenetInterface freenetInterface = mock(FreenetInterface.class);
-       private final Version currentVersion = new Version(1, 0, 0);
-       private final PluginHomepage pluginHomepage = new PluginHomepage("KSK@homepage");
-       private final UpdateChecker updateChecker = new UpdateChecker(eventBus, freenetInterface, currentVersion, pluginHomepage);
-
-       @Before
-       public void startUpdateChecker() {
-               updateChecker.start();
-       }
-
-       @Test
-       public void newUpdateCheckerDoesNotHaveALatestVersion() {
-               assertThat(updateChecker.hasLatestVersion(), is(false));
-               assertThat(updateChecker.getLatestVersion(), is(currentVersion));
-       }
-
-       @Test
-       public void startingAnUpdateCheckerRegisterAUsk() {
-               verify(freenetInterface).registerUsk(any(FreenetURI.class), any(Callback.class));
-       }
-
-       @Test
-       public void stoppingAnUpdateCheckerUnregistersAUsk() {
-               updateChecker.stop();
-               verify(freenetInterface).unregisterUsk(any(FreenetURI.class));
-       }
-
-       @Test
-       public void callbackDoesNotDownloadIfNewEditionIsNotFound() {
-               setupCallbackWithEdition(MAX_VALUE, false, false);
-               verify(freenetInterface, never()).fetchUri(any(FreenetURI.class));
-               verify(eventBus, never()).post(argThat(instanceOf(UpdateFoundEvent.class)));
-       }
-
-       private void setupCallbackWithEdition(long edition, boolean newKnownGood, boolean newSlot) {
-               ArgumentCaptor<FreenetURI> uri = forClass(FreenetURI.class);
-               ArgumentCaptor<Callback> callback = forClass(Callback.class);
-               verify(freenetInterface).registerUsk(uri.capture(), callback.capture());
-               callback.getValue().editionFound(uri.getValue(), edition, newKnownGood, newSlot);
-       }
-
-       @Test
-       public void callbackStartsIfNewEditionIsFound() {
-               setupFetchResult(createFutureFetchResult());
-               setupCallbackWithEdition(MAX_VALUE, true, false);
-               verifyAFreenetUriIsFetched();
-               verifyEventIsFired(new Version(99, 0, 0), 11865368297000L, false);
-               verifyThatUpdateCheckerKnowsLatestVersion(new Version(99, 0, 0), 11865368297000L);
-       }
-
-       private FetchResult createFutureFetchResult() {
-               ClientMetadata clientMetadata = new ClientMetadata("application/xml");
-               Bucket fetched = new ArrayBucket(("# MapConfigurationBackendVersion=1\n" +
-                               "CurrentVersion/Version: 99.0.0\n" +
-                               "CurrentVersion/ReleaseTime: 11865368297000\n" +
-                               "DisruptiveVersion/0.1.2: true").getBytes());
-               return new FetchResult(clientMetadata, fetched);
-       }
-
-       private void verifyEventIsFired(Version version, long releaseTime, boolean disruptive) {
-               ArgumentCaptor<UpdateFoundEvent> updateFoundEvent = forClass(UpdateFoundEvent.class);
-               verify(eventBus, times(1)).post(updateFoundEvent.capture());
-               assertThat(updateFoundEvent.getValue().version(), is(version));
-               assertThat(updateFoundEvent.getValue().releaseTime(), is(releaseTime));
-               assertThat(updateFoundEvent.getValue().disruptive(), is(disruptive));
-       }
-
-       private void verifyThatUpdateCheckerKnowsLatestVersion(Version version, long releaseTime) {
-               assertThat(updateChecker.getLatestVersion(), is(version));
-               assertThat(updateChecker.getLatestVersionDate(), is(releaseTime));
-               assertThat(updateChecker.hasLatestVersion(), is(true));
-       }
-
-       @Test
-       public void callbackDoesNotStartIfNoNewEditionIsFound() {
-               setupFetchResult(createPastFetchResult());
-               setupCallbackWithEdition(updateChecker.getLatestEdition(), true, false);
-               verifyAFreenetUriIsFetched();
-               verifyNoUpdateFoundEventIsFired();
-       }
-
-       private void setupFetchResult(final FetchResult pastFetchResult) {
-               when(freenetInterface.fetchUri(any(FreenetURI.class))).thenAnswer(new Answer<Fetched>() {
-                       @Override
-                       public Fetched answer(InvocationOnMock invocation) throws Throwable {
-                               FreenetURI freenetUri = (FreenetURI) invocation.getArguments()[0];
-                               return new Fetched(freenetUri, pastFetchResult);
-                       }
-               });
-       }
-
-       private FetchResult createPastFetchResult() {
-               ClientMetadata clientMetadata = new ClientMetadata("application/xml");
-               Bucket fetched = new ArrayBucket(("# MapConfigurationBackendVersion=1\n" +
-                               "CurrentVersion/Version: 0.2\n" +
-                               "CurrentVersion/ReleaseTime: 1289417883000").getBytes());
-               return new FetchResult(clientMetadata, fetched);
-       }
-
-       @Test
-       public void invalidUpdateFileDoesNotStartCallback() {
-               setupFetchResult(createInvalidFetchResult());
-               setupCallbackWithEdition(MAX_VALUE, true, false);
-               verifyAFreenetUriIsFetched();
-               verifyNoUpdateFoundEventIsFired();
-       }
-
-       private FetchResult createInvalidFetchResult() {
-               ClientMetadata clientMetadata = new ClientMetadata("text/plain");
-               Bucket fetched = new ArrayBucket("Some other data.".getBytes());
-               return new FetchResult(clientMetadata, fetched);
-       }
-
-       @Test
-       public void nonExistingPropertiesWillNotCauseUpdateToBeFound() {
-               setupCallbackWithEdition(MAX_VALUE, true, false);
-               verifyAFreenetUriIsFetched();
-               verifyNoUpdateFoundEventIsFired();
-       }
-
-       private void verifyNoUpdateFoundEventIsFired() {
-               verify(eventBus, never()).post(any(UpdateFoundEvent.class));
-       }
-
-       private void verifyAFreenetUriIsFetched() {
-               verify(freenetInterface).fetchUri(any(FreenetURI.class));
-       }
-
-       @Test
-       public void brokenBucketDoesNotCauseUpdateToBeFound() {
-               setupFetchResult(createBrokenBucketFetchResult());
-               setupCallbackWithEdition(MAX_VALUE, true, false);
-               verifyAFreenetUriIsFetched();
-               verifyNoUpdateFoundEventIsFired();
-       }
-
-       private FetchResult createBrokenBucketFetchResult() {
-               ClientMetadata clientMetadata = new ClientMetadata("text/plain");
-               Bucket fetched = new ArrayBucket("Some other data.".getBytes()) {
-                       @Override
-                       public InputStream getInputStream() {
-                               try {
-                                       return when(mock(InputStream.class).read()).thenThrow(IOException.class).getMock();
-                               } catch (IOException ioe1) {
-                                       /* won’t throw here. */
-                                       return null;
-                               }
-                       }
-               };
-               return new FetchResult(clientMetadata, fetched);
-       }
-
-       @Test
-       public void invalidTimeDoesNotCauseAnUpdateToBeFound() {
-               setupFetchResult(createInvalidTimeFetchResult());
-               setupCallbackWithEdition(MAX_VALUE, true, false);
-               verifyAFreenetUriIsFetched();
-               verifyNoUpdateFoundEventIsFired();
-       }
-
-       private FetchResult createInvalidTimeFetchResult() {
-               ClientMetadata clientMetadata = new ClientMetadata("application/xml");
-               Bucket fetched = new ArrayBucket(("# MapConfigurationBackendVersion=1\n" +
-                               "CurrentVersion/Version: 0.2\n" +
-                               "CurrentVersion/ReleaseTime: invalid").getBytes());
-               return new FetchResult(clientMetadata, fetched);
-       }
-
-       @Test
-       public void invalidPropertiesDoesNotCauseAnUpdateToBeFound() {
-               setupFetchResult(createMissingTimeFetchResult());
-               setupCallbackWithEdition(MAX_VALUE, true, false);
-               verifyAFreenetUriIsFetched();
-               verifyNoUpdateFoundEventIsFired();
-       }
-
-       private FetchResult createMissingTimeFetchResult() {
-               ClientMetadata clientMetadata = new ClientMetadata("application/xml");
-               Bucket fetched = new ArrayBucket(("# MapConfigurationBackendVersion=1\n" +
-                               "CurrentVersion/Version: 0.2\n").getBytes());
-               return new FetchResult(clientMetadata, fetched);
-       }
-
-       @Test
-       public void invalidVersionDoesNotCauseAnUpdateToBeFound() {
-               setupFetchResult(createInvalidVersionFetchResult());
-               setupCallbackWithEdition(MAX_VALUE, true, false);
-               verifyAFreenetUriIsFetched();
-               verifyNoUpdateFoundEventIsFired();
-       }
-
-       private FetchResult createInvalidVersionFetchResult() {
-               ClientMetadata clientMetadata = new ClientMetadata("application/xml");
-               Bucket fetched = new ArrayBucket(("# MapConfigurationBackendVersion=1\n" +
-                               "CurrentVersion/Version: foo\n" +
-                               "CurrentVersion/ReleaseTime: 1289417883000").getBytes());
-               return new FetchResult(clientMetadata, fetched);
-       }
-
-       @Test
-       public void disruptiveVersionGetsNotification() {
-               setupFetchResult(createDisruptiveVersionFetchResult());
-               setupCallbackWithEdition(MAX_VALUE, true, false);
-               verifyAFreenetUriIsFetched();
-               verifyEventIsFired(new Version(1, 2, 3), 1289417883000L, true);
-               verifyThatUpdateCheckerKnowsLatestVersion(new Version(1, 2, 3), 1289417883000L);
-       }
-
-       private FetchResult createDisruptiveVersionFetchResult() {
-               ClientMetadata clientMetadata = new ClientMetadata("application/xml");
-               Bucket fetched = new ArrayBucket(("# MapConfigurationBackendVersion=1\n" +
-                               "CurrentVersion/Version: 1.2.3\n" +
-                               "CurrentVersion/ReleaseTime: 1289417883000\n" +
-                               "DisruptiveVersion/1.2.3: true").getBytes());
-               return new FetchResult(clientMetadata, fetched);
-       }
-
-}
index 664e4a1..5f8d93d 100644 (file)
@@ -6,28 +6,18 @@ 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.doAnswer;
-import static org.mockito.Mockito.doThrow;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
+import static org.mockito.Mockito.*;
 
 import java.util.concurrent.CountDownLatch;
 
 import net.pterodactylus.sone.core.WebOfTrustUpdaterImpl.AddContextJob;
 import net.pterodactylus.sone.core.WebOfTrustUpdaterImpl.RemoveContextJob;
 import net.pterodactylus.sone.core.WebOfTrustUpdaterImpl.SetPropertyJob;
-import net.pterodactylus.sone.core.WebOfTrustUpdaterImpl.SetTrustJob;
 import net.pterodactylus.sone.core.WebOfTrustUpdaterImpl.WebOfTrustContextUpdateJob;
 import net.pterodactylus.sone.core.WebOfTrustUpdaterImpl.WebOfTrustUpdateJob;
 import net.pterodactylus.sone.freenet.plugin.PluginException;
-import net.pterodactylus.sone.freenet.wot.Identity;
 import net.pterodactylus.sone.freenet.wot.OwnIdentity;
-import net.pterodactylus.sone.freenet.wot.Trust;
 import net.pterodactylus.sone.freenet.wot.WebOfTrustConnector;
-import net.pterodactylus.sone.freenet.wot.WebOfTrustException;
 
 import org.junit.Test;
 import org.mockito.invocation.InvocationOnMock;
@@ -39,9 +29,6 @@ import org.mockito.stubbing.Answer;
 public class WebOfTrustUpdaterTest {
 
        private static final String CONTEXT = "test-context";
-       private static final Integer SCORE = 50;
-       private static final Integer OTHER_SCORE = 25;
-       private static final String TRUST_COMMENT = "set in a test";
        private static final String PROPERTY_NAME = "test-property";
        private final WebOfTrustConnector webOfTrustConnector = mock(WebOfTrustConnector.class);
        private final WebOfTrustUpdaterImpl webOfTrustUpdater = new WebOfTrustUpdaterImpl(webOfTrustConnector);
@@ -51,7 +38,6 @@ public class WebOfTrustUpdaterTest {
        private final WebOfTrustContextUpdateJob contextUpdateJob = webOfTrustUpdater.new WebOfTrustContextUpdateJob(ownIdentity, CONTEXT);
        private final AddContextJob addContextJob = webOfTrustUpdater.new AddContextJob(ownIdentity, CONTEXT);
        private final RemoveContextJob removeContextJob = webOfTrustUpdater.new RemoveContextJob(ownIdentity, CONTEXT);
-       private final Identity trustee = when(mock(Identity.class).getId()).thenReturn("trustee-id").getMock();
 
        private WebOfTrustUpdateJob createWebOfTrustUpdateJob(final boolean success) {
                return webOfTrustUpdater.new WebOfTrustUpdateJob() {
@@ -235,81 +221,6 @@ public class WebOfTrustUpdaterTest {
        }
 
        @Test
-       public void setTrustJobSetsTrust() throws PluginException {
-               SetTrustJob setTrustJob = webOfTrustUpdater.new SetTrustJob(ownIdentity, trustee, SCORE, TRUST_COMMENT);
-               setTrustJob.run();
-               verify(webOfTrustConnector).setTrust(eq(ownIdentity), eq(trustee), eq(SCORE), eq(TRUST_COMMENT));
-               verify(trustee).setTrust(eq(ownIdentity), eq(new Trust(SCORE, null, 0)));
-               assertThat(setTrustJob.waitForCompletion(), is(true));
-       }
-
-       @Test
-       public void settingNullTrustRemovesTrust() throws WebOfTrustException {
-               SetTrustJob setTrustJob = webOfTrustUpdater.new SetTrustJob(ownIdentity, trustee, null, TRUST_COMMENT);
-               setTrustJob.run();
-               verify(webOfTrustConnector).removeTrust(eq(ownIdentity), eq(trustee));
-               verify(trustee).removeTrust(eq(ownIdentity));
-               assertThat(setTrustJob.waitForCompletion(), is(true));
-       }
-
-       @Test
-       public void exceptionWhileSettingTrustIsCaught() throws PluginException {
-               doThrow(PluginException.class).when(webOfTrustConnector).setTrust(eq(ownIdentity), eq(trustee), eq(SCORE), eq(TRUST_COMMENT));
-               SetTrustJob setTrustJob = webOfTrustUpdater.new SetTrustJob(ownIdentity, trustee, SCORE, TRUST_COMMENT);
-               setTrustJob.run();
-               verify(webOfTrustConnector).setTrust(eq(ownIdentity), eq(trustee), eq(SCORE), eq(TRUST_COMMENT));
-               verify(trustee, never()).setTrust(eq(ownIdentity), eq(new Trust(SCORE, null, 0)));
-               assertThat(setTrustJob.waitForCompletion(), is(false));
-       }
-
-       @Test
-       public void setTrustJobsWithDifferentClassesAreNotEqual() {
-               SetTrustJob firstSetTrustJob = webOfTrustUpdater.new SetTrustJob(ownIdentity, trustee, SCORE, TRUST_COMMENT);
-               SetTrustJob secondSetTrustJob = webOfTrustUpdater.new SetTrustJob(ownIdentity, trustee, SCORE, TRUST_COMMENT) {
-               };
-               assertThat(firstSetTrustJob, not(is(secondSetTrustJob)));
-               assertThat(secondSetTrustJob, not(is(firstSetTrustJob)));
-       }
-
-       @Test
-       public void setTrustJobsWithDifferentTrustersAreNotEqual() {
-               SetTrustJob firstSetTrustJob = webOfTrustUpdater.new SetTrustJob(ownIdentity, trustee, SCORE, TRUST_COMMENT);
-               SetTrustJob secondSetTrustJob = webOfTrustUpdater.new SetTrustJob(mock(OwnIdentity.class), trustee, SCORE, TRUST_COMMENT);
-               assertThat(firstSetTrustJob, not(is(secondSetTrustJob)));
-               assertThat(secondSetTrustJob, not(is(firstSetTrustJob)));
-       }
-
-       @Test
-       public void setTrustJobsWithDifferentTrusteesAreNotEqual() {
-               SetTrustJob firstSetTrustJob = webOfTrustUpdater.new SetTrustJob(ownIdentity, trustee, SCORE, TRUST_COMMENT);
-               SetTrustJob secondSetTrustJob = webOfTrustUpdater.new SetTrustJob(ownIdentity, mock(Identity.class), SCORE, TRUST_COMMENT);
-               assertThat(firstSetTrustJob, not(is(secondSetTrustJob)));
-               assertThat(secondSetTrustJob, not(is(firstSetTrustJob)));
-       }
-
-       @Test
-       public void setTrustJobsWithDifferentScoreAreEqual() {
-               SetTrustJob firstSetTrustJob = webOfTrustUpdater.new SetTrustJob(ownIdentity, trustee, SCORE, TRUST_COMMENT);
-               SetTrustJob secondSetTrustJob = webOfTrustUpdater.new SetTrustJob(ownIdentity, trustee, OTHER_SCORE, TRUST_COMMENT);
-               assertThat(firstSetTrustJob, is(secondSetTrustJob));
-               assertThat(secondSetTrustJob, is(firstSetTrustJob));
-               assertThat(firstSetTrustJob.hashCode(), is(secondSetTrustJob.hashCode()));
-       }
-
-       @Test
-       public void setTrustJobDoesNotEqualNull() {
-               SetTrustJob setTrustJob = webOfTrustUpdater.new SetTrustJob(ownIdentity, trustee, SCORE, TRUST_COMMENT);
-               assertThat(setTrustJob, not(is((Object) null)));
-       }
-
-       @Test
-       public void toStringOfSetTrustJobContainsIdsOfTrusterAndTrustee() {
-               SetTrustJob setTrustJob = webOfTrustUpdater.new SetTrustJob(ownIdentity, trustee, SCORE, TRUST_COMMENT);
-               assertThat(setTrustJob.toString(), containsString(ownIdentity.getId()));
-               assertThat(setTrustJob.toString(), containsString(trustee.getId()));
-       }
-
-       @Test
        public void webOfTrustUpdaterStopsWhenItShould() {
                webOfTrustUpdater.stop();
                webOfTrustUpdater.serviceRun();
@@ -404,48 +315,4 @@ public class WebOfTrustUpdaterTest {
                verify(ownIdentity).removeContext(eq(CONTEXT));
        }
 
-       @Test
-       public void setTrustSetsTrust() throws InterruptedException, PluginException {
-               final CountDownLatch trustSetTrigger = new CountDownLatch(1);
-               doAnswer(new Answer<Void>() {
-                       @Override
-                       public Void answer(InvocationOnMock invocation) throws Throwable {
-                               trustSetTrigger.countDown();
-                               return null;
-                       }
-               }).when(trustee).setTrust(eq(ownIdentity), eq(new Trust(SCORE, null, 0)));
-               webOfTrustUpdater.start();
-               webOfTrustUpdater.setTrust(ownIdentity, trustee, SCORE, TRUST_COMMENT);
-               assertThat(trustSetTrigger.await(1, SECONDS), is(true));
-               verify(trustee).setTrust(eq(ownIdentity), eq(new Trust(SCORE, null, 0)));
-               verify(webOfTrustConnector).setTrust(eq(ownIdentity), eq(trustee), eq(SCORE), eq(TRUST_COMMENT));
-       }
-
-       @Test
-       public void setTrustRequestsAreCoalesced() throws InterruptedException, PluginException {
-               final CountDownLatch trustSetTrigger = new CountDownLatch(1);
-               doAnswer(new Answer<Void>() {
-                       @Override
-                       public Void answer(InvocationOnMock invocation) throws Throwable {
-                               trustSetTrigger.countDown();
-                               return null;
-                       }
-               }).when(trustee).setTrust(eq(ownIdentity), eq(new Trust(SCORE, null, 0)));
-               for (int i = 1; i <= 2; i++) {
-                       /* this is so fucking volatile. */
-                       if (i > 1) {
-                               sleep(200);
-                       }
-                       new Thread(new Runnable() {
-                               public void run() {
-                                       webOfTrustUpdater.setTrust(ownIdentity, trustee, SCORE, TRUST_COMMENT);
-                               }
-                       }).start();
-               }
-               webOfTrustUpdater.start();
-               assertThat(trustSetTrigger.await(1, SECONDS), is(true));
-               verify(trustee).setTrust(eq(ownIdentity), eq(new Trust(SCORE, null, 0)));
-               verify(webOfTrustConnector).setTrust(eq(ownIdentity), eq(trustee), eq(SCORE), eq(TRUST_COMMENT));
-       }
-
 }
diff --git a/src/test/java/net/pterodactylus/sone/freenet/KeyTest.java b/src/test/java/net/pterodactylus/sone/freenet/KeyTest.java
deleted file mode 100644 (file)
index 80e1e45..0000000
+++ /dev/null
@@ -1,67 +0,0 @@
-package net.pterodactylus.sone.freenet;
-
-import static freenet.support.Base64.encode;
-import static net.pterodactylus.sone.freenet.Key.from;
-import static net.pterodactylus.sone.freenet.Key.routingKey;
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.is;
-
-import java.net.MalformedURLException;
-
-import freenet.keys.FreenetURI;
-
-import org.junit.Test;
-
-/**
- * Unit test for {@link Key}.
- */
-public class KeyTest {
-
-       private final FreenetURI uri;
-       private final Key key;
-
-       public KeyTest() throws MalformedURLException {
-               uri = new FreenetURI(
-                               "SSK@NfUYvxDwU9vqb2mh-qdT~DYJ6U0XNbxMGGoLe0aCHJs,Miglsgix0VR56ZiPl4NgjnUd~UdrnHqIvXJ3KKHmxmI,AQACAAE/some-site-12/foo/bar.html");
-               key = from(uri);
-       }
-
-       @Test
-       public void keyCanBeCreatedFromFreenetUri() throws MalformedURLException {
-               assertThat(key.getRoutingKey(),
-                               is("NfUYvxDwU9vqb2mh-qdT~DYJ6U0XNbxMGGoLe0aCHJs"));
-               assertThat(key.getCryptoKey(),
-                               is("Miglsgix0VR56ZiPl4NgjnUd~UdrnHqIvXJ3KKHmxmI"));
-               assertThat(key.getExtra(), is("AQACAAE"));
-       }
-
-       @Test
-       public void keyCanBeConvertedToUsk() throws MalformedURLException {
-               FreenetURI uskUri = key.toUsk("other-site", 15, "some", "path.html");
-               assertThat(uskUri.toString(),
-                               is("USK@NfUYvxDwU9vqb2mh-qdT~DYJ6U0XNbxMGGoLe0aCHJs,Miglsgix0VR56ZiPl4NgjnUd~UdrnHqIvXJ3KKHmxmI,AQACAAE/other-site/15/some/path.html"));
-       }
-
-       @Test
-       public void keyCanBeConvertedToSskWithoutEdition()
-       throws MalformedURLException {
-               FreenetURI uskUri = key.toSsk("other-site", "some", "path.html");
-               assertThat(uskUri.toString(),
-                               is("SSK@NfUYvxDwU9vqb2mh-qdT~DYJ6U0XNbxMGGoLe0aCHJs,Miglsgix0VR56ZiPl4NgjnUd~UdrnHqIvXJ3KKHmxmI,AQACAAE/other-site/some/path.html"));
-       }
-
-       @Test
-       public void keyCanBeConvertedToSskWithEdition()
-       throws MalformedURLException {
-               FreenetURI uskUri = key.toSsk("other-site", 15, "some", "path.html");
-               assertThat(uskUri.toString(),
-                               is("SSK@NfUYvxDwU9vqb2mh-qdT~DYJ6U0XNbxMGGoLe0aCHJs,Miglsgix0VR56ZiPl4NgjnUd~UdrnHqIvXJ3KKHmxmI,AQACAAE/other-site-15/some/path.html"));
-       }
-
-       @Test
-       public void routingKeyIsExtractCorrectly() {
-               assertThat(routingKey(uri),
-                               is("NfUYvxDwU9vqb2mh-qdT~DYJ6U0XNbxMGGoLe0aCHJs"));
-       }
-
-}
diff --git a/src/test/java/net/pterodactylus/sone/freenet/wot/DefaultIdentityTest.java b/src/test/java/net/pterodactylus/sone/freenet/wot/DefaultIdentityTest.java
deleted file mode 100644 (file)
index 9d59570..0000000
+++ /dev/null
@@ -1,150 +0,0 @@
-/*
- * Sone - DefaultIdentityTest.java - Copyright © 2013–2019 David Roden
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.sone.freenet.wot;
-
-import static com.google.common.collect.ImmutableMap.of;
-import static java.util.Arrays.asList;
-import static net.pterodactylus.sone.test.Matchers.matchesRegex;
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.containsInAnyOrder;
-import static org.hamcrest.Matchers.empty;
-import static org.hamcrest.Matchers.hasEntry;
-import static org.hamcrest.Matchers.is;
-import static org.hamcrest.Matchers.not;
-import static org.hamcrest.Matchers.nullValue;
-import static org.hamcrest.collection.IsIterableContainingInOrder.contains;
-import static org.mockito.Mockito.mock;
-
-import java.util.Collections;
-
-import org.junit.Test;
-
-/**
- * Unit test for {@link DefaultIdentity}.
- */
-public class DefaultIdentityTest {
-
-       protected final DefaultIdentity identity = createIdentity();
-
-       protected DefaultIdentity createIdentity() {
-               return new DefaultIdentity("Id", "Nickname", "RequestURI");
-       }
-
-       @Test
-       public void identityCanBeCreated() {
-               assertThat(identity.getId(), is("Id"));
-               assertThat(identity.getNickname(), is("Nickname"));
-               assertThat(identity.getRequestUri(), is("RequestURI"));
-               assertThat(identity.getContexts(), empty());
-               assertThat(identity.getProperties(), is(Collections.<String, String>emptyMap()));
-       }
-
-       @Test
-       public void contextsAreAddedCorrectly() {
-               identity.addContext("Test");
-               assertThat(identity.getContexts(), contains("Test"));
-               assertThat(identity.hasContext("Test"), is(true));
-       }
-
-       @Test
-       public void contextsAreRemovedCorrectly() {
-               identity.addContext("Test");
-               identity.removeContext("Test");
-               assertThat(identity.getContexts(), empty());
-               assertThat(identity.hasContext("Test"), is(false));
-       }
-
-       @Test
-       public void contextsAreSetCorrectlyInBulk() {
-               identity.addContext("Test");
-               identity.setContexts(asList("Test1", "Test2"));
-               assertThat(identity.getContexts(), containsInAnyOrder("Test1", "Test2"));
-               assertThat(identity.hasContext("Test"), is(false));
-               assertThat(identity.hasContext("Test1"), is(true));
-               assertThat(identity.hasContext("Test2"), is(true));
-       }
-
-       @Test
-       public void propertiesAreAddedCorrectly() {
-               identity.setProperty("Key", "Value");
-               assertThat(identity.getProperties().size(), is(1));
-               assertThat(identity.getProperties(), hasEntry("Key", "Value"));
-               assertThat(identity.getProperty("Key"), is("Value"));
-       }
-
-       @Test
-       public void propertiesAreRemovedCorrectly() {
-               identity.setProperty("Key", "Value");
-               identity.removeProperty("Key");
-               assertThat(identity.getProperties(), is(Collections.<String, String>emptyMap()));
-               assertThat(identity.getProperty("Key"), nullValue());
-       }
-
-       @Test
-       public void propertiesAreSetCorrectlyInBulk() {
-               identity.setProperty("Key", "Value");
-               identity.setProperties(of("Key1", "Value1", "Key2", "Value2"));
-               assertThat(identity.getProperties().size(), is(2));
-               assertThat(identity.getProperty("Key"), nullValue());
-               assertThat(identity.getProperty("Key1"), is("Value1"));
-               assertThat(identity.getProperty("Key2"), is("Value2"));
-       }
-
-       @Test
-       public void trustRelationshipsAreAddedCorrectly() {
-               OwnIdentity ownIdentity = mock(OwnIdentity.class);
-               Trust trust = mock(Trust.class);
-               identity.setTrust(ownIdentity, trust);
-               assertThat(identity.getTrust(ownIdentity), is(trust));
-       }
-
-       @Test
-       public void trustRelationshipsAreRemovedCorrectly() {
-               OwnIdentity ownIdentity = mock(OwnIdentity.class);
-               Trust trust = mock(Trust.class);
-               identity.setTrust(ownIdentity, trust);
-               identity.removeTrust(ownIdentity);
-               assertThat(identity.getTrust(ownIdentity), nullValue());
-       }
-
-       @Test
-       public void identitiesWithTheSameIdAreEqual() {
-               DefaultIdentity identity2 = new DefaultIdentity("Id", "Nickname2", "RequestURI2");
-               assertThat(identity2, is(identity));
-               assertThat(identity, is(identity2));
-       }
-
-       @Test
-       public void twoEqualIdentitiesHaveTheSameHashCode() {
-               DefaultIdentity identity2 = new DefaultIdentity("Id", "Nickname2", "RequestURI2");
-               assertThat(identity.hashCode(), is(identity2.hashCode()));
-       }
-
-       @Test
-       public void nullDoesNotMatchAnIdentity() {
-               assertThat(identity, not(is((Object) null)));
-       }
-
-       @Test
-       public void toStringContainsIdAndNickname() {
-               String identityString = identity.toString();
-               assertThat(identityString, matchesRegex(".*\\bId\\b.*"));
-               assertThat(identityString, matchesRegex(".*\\bNickname\\b.*"));
-       }
-
-}
diff --git a/src/test/java/net/pterodactylus/sone/freenet/wot/DefaultOwnIdentityTest.java b/src/test/java/net/pterodactylus/sone/freenet/wot/DefaultOwnIdentityTest.java
deleted file mode 100644 (file)
index 138ced3..0000000
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * Sone - DefaultOwnIdentityTest.java - Copyright © 2013–2019 David Roden
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.sone.freenet.wot;
-
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.is;
-
-import org.junit.Test;
-
-/**
- * Unit test for {@link DefaultOwnIdentity}.
- */
-public class DefaultOwnIdentityTest extends DefaultIdentityTest {
-
-       @Override
-       protected DefaultIdentity createIdentity() {
-               return new DefaultOwnIdentity("Id", "Nickname", "RequestURI", "InsertURI");
-       }
-
-       @Test
-       public void ownIdentityCanBeCreated() {
-               assertThat(((OwnIdentity) identity).getInsertUri(), is("InsertURI"));
-       }
-
-}
diff --git a/src/test/java/net/pterodactylus/sone/freenet/wot/Identities.java b/src/test/java/net/pterodactylus/sone/freenet/wot/Identities.java
deleted file mode 100644 (file)
index 7ed2b59..0000000
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- * Sone - Identities.java - Copyright © 2013–2019 David Roden
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.sone.freenet.wot;
-
-import java.util.Collection;
-import java.util.Map;
-
-/**
- * Creates {@link Identity}s and {@link OwnIdentity}s.
- */
-public class Identities {
-
-       public static OwnIdentity createOwnIdentity(String id, Collection<String> contexts, Map<String, String> properties) {
-               DefaultOwnIdentity ownIdentity = new DefaultOwnIdentity(id, "Nickname" + id, "Request" + id, "Insert" + id);
-               setContextsAndPropertiesOnIdentity(ownIdentity, contexts, properties);
-               return ownIdentity;
-       }
-
-       public static Identity createIdentity(String id, Collection<String> contexts, Map<String, String> properties) {
-               DefaultIdentity identity = new DefaultIdentity(id, "Nickname" + id, "Request" + id);
-               setContextsAndPropertiesOnIdentity(identity, contexts, properties);
-               return identity;
-       }
-
-       private static void setContextsAndPropertiesOnIdentity(Identity identity, Collection<String> contexts, Map<String, String> properties) {
-               identity.setContexts(contexts);
-               identity.setProperties(properties);
-       }
-
-}
diff --git a/src/test/java/net/pterodactylus/sone/freenet/wot/IdentityChangeDetectorTest.java b/src/test/java/net/pterodactylus/sone/freenet/wot/IdentityChangeDetectorTest.java
deleted file mode 100644 (file)
index e1eb1c8..0000000
+++ /dev/null
@@ -1,188 +0,0 @@
-/*
- * Sone - IdentityChangeDetectorTest.java - Copyright © 2013–2019 David Roden
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.sone.freenet.wot;
-
-import static com.google.common.collect.ImmutableMap.of;
-import static com.google.common.collect.Lists.newArrayList;
-import static java.util.Arrays.asList;
-import static net.pterodactylus.sone.freenet.wot.Identities.createIdentity;
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.containsInAnyOrder;
-import static org.hamcrest.Matchers.empty;
-
-import java.util.Collection;
-
-import net.pterodactylus.sone.freenet.wot.IdentityChangeDetector.IdentityProcessor;
-
-import org.junit.Before;
-import org.junit.Test;
-
-/**
- * Unit test for {@link IdentityChangeDetector}.
- */
-public class IdentityChangeDetectorTest {
-
-       private final IdentityChangeDetector identityChangeDetector = new IdentityChangeDetector(createOldIdentities());
-       private final Collection<Identity> newIdentities = newArrayList();
-       private final Collection<Identity> removedIdentities = newArrayList();
-       private final Collection<Identity> changedIdentities = newArrayList();
-       private final Collection<Identity> unchangedIdentities = newArrayList();
-
-       @Before
-       public void setup() {
-               identityChangeDetector.onNewIdentity(new IdentityProcessor() {
-                       @Override
-                       public void processIdentity(Identity identity) {
-                               newIdentities.add(identity);
-                       }
-               });
-               identityChangeDetector.onRemovedIdentity(new IdentityProcessor() {
-                       @Override
-                       public void processIdentity(Identity identity) {
-                               removedIdentities.add(identity);
-                       }
-               });
-               identityChangeDetector.onChangedIdentity(new IdentityProcessor() {
-                       @Override
-                       public void processIdentity(Identity identity) {
-                               changedIdentities.add(identity);
-                       }
-               });
-               identityChangeDetector.onUnchangedIdentity(new IdentityProcessor() {
-                       @Override
-                       public void processIdentity(Identity identity) {
-                               unchangedIdentities.add(identity);
-                       }
-               });
-       }
-
-       @Test
-       public void noDifferencesAreDetectedWhenSendingTheOldIdentitiesAgain() {
-               identityChangeDetector.detectChanges(createOldIdentities());
-               assertThat(newIdentities, empty());
-               assertThat(removedIdentities, empty());
-               assertThat(changedIdentities, empty());
-               assertThat(unchangedIdentities, containsInAnyOrder(createIdentity1(), createIdentity2(), createIdentity3()));
-       }
-
-       @Test
-       public void detectThatAnIdentityWasRemoved() {
-               identityChangeDetector.detectChanges(asList(createIdentity1(), createIdentity3()));
-               assertThat(newIdentities, empty());
-               assertThat(removedIdentities, containsInAnyOrder(createIdentity2()));
-               assertThat(changedIdentities, empty());
-               assertThat(unchangedIdentities, containsInAnyOrder(createIdentity1(), createIdentity3()));
-       }
-
-       @Test
-       public void detectThatAnIdentityWasAdded() {
-               identityChangeDetector.detectChanges(asList(createIdentity1(), createIdentity2(), createIdentity3(), createIdentity4()));
-               assertThat(newIdentities, containsInAnyOrder(createIdentity4()));
-               assertThat(removedIdentities, empty());
-               assertThat(changedIdentities, empty());
-               assertThat(unchangedIdentities, containsInAnyOrder(createIdentity1(), createIdentity2(), createIdentity3()));
-       }
-
-       @Test
-       public void detectThatAContextWasRemoved() {
-               Identity identity2 = createIdentity2();
-               identity2.removeContext("Context C");
-               identityChangeDetector.detectChanges(asList(createIdentity1(), identity2, createIdentity3()));
-               assertThat(newIdentities, empty());
-               assertThat(removedIdentities, empty());
-               assertThat(changedIdentities, containsInAnyOrder(identity2));
-               assertThat(unchangedIdentities, containsInAnyOrder(createIdentity1(), createIdentity3()));
-       }
-
-       @Test
-       public void detectThatAContextWasAdded() {
-               Identity identity2 = createIdentity2();
-               identity2.addContext("Context C1");
-               identityChangeDetector.detectChanges(asList(createIdentity1(), identity2, createIdentity3()));
-               assertThat(newIdentities, empty());
-               assertThat(removedIdentities, empty());
-               assertThat(changedIdentities, containsInAnyOrder(identity2));
-               assertThat(unchangedIdentities, containsInAnyOrder(createIdentity1(), createIdentity3()));
-       }
-
-       @Test
-       public void detectThatAPropertyWasRemoved() {
-               Identity identity1 = createIdentity1();
-               identity1.removeProperty("Key A");
-               identityChangeDetector.detectChanges(asList(identity1, createIdentity2(), createIdentity3()));
-               assertThat(newIdentities, empty());
-               assertThat(removedIdentities, empty());
-               assertThat(changedIdentities, containsInAnyOrder(identity1));
-               assertThat(unchangedIdentities, containsInAnyOrder(createIdentity2(), createIdentity3()));
-       }
-
-       @Test
-       public void detectThatAPropertyWasAdded() {
-               Identity identity3 = createIdentity3();
-               identity3.setProperty("Key A", "Value A");
-               identityChangeDetector.detectChanges(asList(createIdentity1(), createIdentity2(), identity3));
-               assertThat(newIdentities, empty());
-               assertThat(removedIdentities, empty());
-               assertThat(changedIdentities, containsInAnyOrder(identity3));
-               assertThat(unchangedIdentities, containsInAnyOrder(createIdentity1(), createIdentity2()));
-       }
-
-       @Test
-       public void detectThatAPropertyWasChanged() {
-               Identity identity3 = createIdentity3();
-               identity3.setProperty("Key E", "Value F");
-               identityChangeDetector.detectChanges(asList(createIdentity1(), createIdentity2(), identity3));
-               assertThat(newIdentities, empty());
-               assertThat(removedIdentities, empty());
-               assertThat(changedIdentities, containsInAnyOrder(identity3));
-               assertThat(unchangedIdentities, containsInAnyOrder(createIdentity1(), createIdentity2()));
-       }
-
-       @Test
-       public void noRemovedIdentitiesAreDetectedWithoutAnIdentityProcessor() {
-               identityChangeDetector.onRemovedIdentity(null);
-               identityChangeDetector.detectChanges(asList(createIdentity1(), createIdentity3()));
-       }
-
-       @Test
-       public void noAddedIdentitiesAreDetectedWithoutAnIdentityProcessor() {
-               identityChangeDetector.onNewIdentity(null);
-               identityChangeDetector.detectChanges(asList(createIdentity1(), createIdentity2(), createIdentity3(), createIdentity4()));
-       }
-
-       private static Collection<Identity> createOldIdentities() {
-               return asList(createIdentity1(), createIdentity2(), createIdentity3());
-       }
-
-       private static Identity createIdentity1() {
-               return createIdentity("Test1", asList("Context A", "Context B"), of("Key A", "Value A", "Key B", "Value B"));
-       }
-
-       private static Identity createIdentity2() {
-               return createIdentity("Test2", asList("Context C", "Context D"), of("Key C", "Value C", "Key D", "Value D"));
-       }
-
-       private static Identity createIdentity3() {
-               return createIdentity("Test3", asList("Context E", "Context F"), of("Key E", "Value E", "Key F", "Value F"));
-       }
-
-       private static Identity createIdentity4() {
-               return createIdentity("Test4", asList("Context G", "Context H"), of("Key G", "Value G", "Key H", "Value H"));
-       }
-
-}
diff --git a/src/test/java/net/pterodactylus/sone/freenet/wot/IdentityChangeEventSenderTest.java b/src/test/java/net/pterodactylus/sone/freenet/wot/IdentityChangeEventSenderTest.java
deleted file mode 100644 (file)
index ff442a4..0000000
+++ /dev/null
@@ -1,91 +0,0 @@
-/*
- * Sone - IdentityChangeEventSenderTest.java - Copyright © 2013–2019 David Roden
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.sone.freenet.wot;
-
-import static com.google.common.collect.ImmutableMap.of;
-import static java.util.Arrays.asList;
-import static net.pterodactylus.sone.freenet.wot.Identities.createIdentity;
-import static net.pterodactylus.sone.freenet.wot.Identities.createOwnIdentity;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-import net.pterodactylus.sone.freenet.wot.event.IdentityAddedEvent;
-import net.pterodactylus.sone.freenet.wot.event.IdentityRemovedEvent;
-import net.pterodactylus.sone.freenet.wot.event.IdentityUpdatedEvent;
-import net.pterodactylus.sone.freenet.wot.event.OwnIdentityAddedEvent;
-import net.pterodactylus.sone.freenet.wot.event.OwnIdentityRemovedEvent;
-
-import com.google.common.eventbus.EventBus;
-import org.junit.Test;
-
-/**
- * Unit test for {@link IdentityChangeEventSender}.
- */
-public class IdentityChangeEventSenderTest {
-
-       private final EventBus eventBus = mock(EventBus.class);
-       private final List<OwnIdentity> ownIdentities = asList(
-                       createOwnIdentity("O1", asList("Test"), of("KeyA", "ValueA")),
-                       createOwnIdentity("O2", asList("Test2"), of("KeyB", "ValueB")),
-                       createOwnIdentity("O3", asList("Test3"), of("KeyC", "ValueC"))
-       );
-       private final List<Identity> identities = asList(
-                       createIdentity("I1", Collections.<String>emptyList(), Collections.<String, String>emptyMap()),
-                       createIdentity("I2", Collections.<String>emptyList(), Collections.<String, String>emptyMap()),
-                       createIdentity("I3", Collections.<String>emptyList(), Collections.<String, String>emptyMap()),
-                       createIdentity("I2", asList("Test"), Collections.<String, String>emptyMap())
-       );
-       private final IdentityChangeEventSender identityChangeEventSender = new IdentityChangeEventSender(eventBus, createOldIdentities());
-
-       @Test
-       public void addingAnOwnIdentityIsDetectedAndReportedCorrectly() {
-               Map<OwnIdentity, Collection<Identity>> newIdentities = createNewIdentities();
-               identityChangeEventSender.detectChanges(newIdentities);
-               verify(eventBus).post(eq(new OwnIdentityRemovedEvent(ownIdentities.get(0))));
-               verify(eventBus).post(eq(new IdentityRemovedEvent(ownIdentities.get(0), identities.get(0))));
-               verify(eventBus).post(eq(new IdentityRemovedEvent(ownIdentities.get(0), identities.get(1))));
-               verify(eventBus).post(eq(new OwnIdentityAddedEvent(ownIdentities.get(2))));
-               verify(eventBus).post(eq(new IdentityAddedEvent(ownIdentities.get(2), identities.get(1))));
-               verify(eventBus).post(eq(new IdentityAddedEvent(ownIdentities.get(2), identities.get(2))));
-               verify(eventBus).post(eq(new IdentityRemovedEvent(ownIdentities.get(1), identities.get(0))));
-               verify(eventBus).post(eq(new IdentityAddedEvent(ownIdentities.get(1), identities.get(2))));
-               verify(eventBus).post(eq(new IdentityUpdatedEvent(ownIdentities.get(1), identities.get(1))));
-       }
-
-       private Map<OwnIdentity, Collection<Identity>> createNewIdentities() {
-               Map<OwnIdentity, Collection<Identity>> oldIdentities = new HashMap<>();
-               oldIdentities.put(ownIdentities.get(1), asList(identities.get(3), identities.get(2)));
-               oldIdentities.put(ownIdentities.get(2), asList(identities.get(1), identities.get(2)));
-               return oldIdentities;
-       }
-
-       private Map<OwnIdentity, Collection<Identity>> createOldIdentities() {
-               Map<OwnIdentity, Collection<Identity>> oldIdentities = new HashMap<>();
-               oldIdentities.put(ownIdentities.get(0), asList(identities.get(0), identities.get(1)));
-               oldIdentities.put(ownIdentities.get(1), asList(identities.get(0), identities.get(1)));
-               return oldIdentities;
-       }
-
-}
diff --git a/src/test/java/net/pterodactylus/sone/freenet/wot/IdentityLoaderTest.java b/src/test/java/net/pterodactylus/sone/freenet/wot/IdentityLoaderTest.java
deleted file mode 100644 (file)
index 8744c15..0000000
+++ /dev/null
@@ -1,154 +0,0 @@
-/*
- * Sone - IdentityLoaderTest.java - Copyright © 2013–2019 David Roden
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.sone.freenet.wot;
-
-import static com.google.common.base.Optional.of;
-import static com.google.common.collect.Lists.newArrayList;
-import static com.google.common.collect.Sets.newHashSet;
-import static java.util.Arrays.asList;
-import static java.util.Collections.emptySet;
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.containsInAnyOrder;
-import static org.hamcrest.Matchers.hasSize;
-import static org.hamcrest.Matchers.is;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import java.util.Collection;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-
-import com.google.common.base.Optional;
-import com.google.common.collect.ImmutableMap;
-import org.hamcrest.Matchers;
-import org.junit.Before;
-import org.junit.Test;
-
-/**
- * Unit test for {@link IdentityLoader}.
- */
-public class IdentityLoaderTest {
-
-       private final WebOfTrustConnector webOfTrustConnector = mock(WebOfTrustConnector.class);
-       private final IdentityLoader identityLoader = new IdentityLoader(webOfTrustConnector, of(new Context("Test")));
-       private final IdentityLoader identityLoaderWithoutContext = new IdentityLoader(webOfTrustConnector);
-
-       @Before
-       public void setup() throws WebOfTrustException {
-               List<OwnIdentity> ownIdentities = createOwnIdentities();
-               when(webOfTrustConnector.loadAllOwnIdentities()).thenReturn(newHashSet(ownIdentities));
-               when(webOfTrustConnector.loadTrustedIdentities(eq(ownIdentities.get(0)), any(Optional.class))).thenReturn(createTrustedIdentitiesForFirstOwnIdentity());
-               when(webOfTrustConnector.loadTrustedIdentities(eq(ownIdentities.get(1)), any(Optional.class))).thenReturn(createTrustedIdentitiesForSecondOwnIdentity());
-               when(webOfTrustConnector.loadTrustedIdentities(eq(ownIdentities.get(2)), any(Optional.class))).thenReturn(createTrustedIdentitiesForThirdOwnIdentity());
-               when(webOfTrustConnector.loadTrustedIdentities(eq(ownIdentities.get(3)), any(Optional.class))).thenReturn(createTrustedIdentitiesForFourthOwnIdentity());
-       }
-
-       private List<OwnIdentity> createOwnIdentities() {
-               return newArrayList(
-                               createOwnIdentity("O1", "ON1", "OR1", "OI1", asList("Test", "Test2"), ImmutableMap.of("KeyA", "ValueA", "KeyB", "ValueB")),
-                               createOwnIdentity("O2", "ON2", "OR2", "OI2", asList("Test"), ImmutableMap.of("KeyC", "ValueC")),
-                               createOwnIdentity("O3", "ON3", "OR3", "OI3", asList("Test2"), ImmutableMap.of("KeyE", "ValueE", "KeyD", "ValueD")),
-                               createOwnIdentity("O4", "ON4", "OR$", "OI4", asList("Test"), ImmutableMap.of("KeyA", "ValueA", "KeyD", "ValueD"))
-               );
-       }
-
-       private Set<Identity> createTrustedIdentitiesForFirstOwnIdentity() {
-               return newHashSet(
-                               createIdentity("I11", "IN11", "IR11", asList("Test"), ImmutableMap.of("KeyA", "ValueA"))
-               );
-       }
-
-       private Set<Identity> createTrustedIdentitiesForSecondOwnIdentity() {
-               return newHashSet(
-                               createIdentity("I21", "IN21", "IR21", asList("Test", "Test2"), ImmutableMap.of("KeyB", "ValueB"))
-               );
-       }
-
-       private Set<Identity> createTrustedIdentitiesForThirdOwnIdentity() {
-               return newHashSet(
-                               createIdentity("I31", "IN31", "IR31", asList("Test", "Test3"), ImmutableMap.of("KeyC", "ValueC"))
-               );
-       }
-
-       private Set<Identity> createTrustedIdentitiesForFourthOwnIdentity() {
-               return emptySet();
-       }
-
-       private OwnIdentity createOwnIdentity(String id, String nickname, String requestUri, String insertUri, List<String> contexts, ImmutableMap<String, String> properties) {
-               OwnIdentity ownIdentity = new DefaultOwnIdentity(id, nickname, requestUri, insertUri);
-               ownIdentity.setContexts(contexts);
-               ownIdentity.setProperties(properties);
-               return ownIdentity;
-       }
-
-       private Identity createIdentity(String id, String nickname, String requestUri, List<String> contexts, ImmutableMap<String, String> properties) {
-               Identity identity = new DefaultIdentity(id, nickname, requestUri);
-               identity.setContexts(contexts);
-               identity.setProperties(properties);
-               return identity;
-       }
-
-       @Test
-       public void loadingIdentities() throws WebOfTrustException {
-               List<OwnIdentity> ownIdentities = createOwnIdentities();
-               Map<OwnIdentity, Collection<Identity>> identities = identityLoader.loadIdentities();
-               verify(webOfTrustConnector).loadAllOwnIdentities();
-               verify(webOfTrustConnector).loadTrustedIdentities(eq(ownIdentities.get(0)), eq(of("Test")));
-               verify(webOfTrustConnector).loadTrustedIdentities(eq(ownIdentities.get(1)), eq(of("Test")));
-               verify(webOfTrustConnector, never()).loadTrustedIdentities(eq(ownIdentities.get(2)), any(Optional.class));
-               verify(webOfTrustConnector).loadTrustedIdentities(eq(ownIdentities.get(3)), eq(of("Test")));
-               assertThat(identities.keySet(), hasSize(4));
-               assertThat(identities.keySet(), containsInAnyOrder(ownIdentities.get(0), ownIdentities.get(1), ownIdentities.get(2), ownIdentities.get(3)));
-               verifyIdentitiesForOwnIdentity(identities, ownIdentities.get(0), createTrustedIdentitiesForFirstOwnIdentity());
-               verifyIdentitiesForOwnIdentity(identities, ownIdentities.get(1), createTrustedIdentitiesForSecondOwnIdentity());
-               verifyIdentitiesForOwnIdentity(identities, ownIdentities.get(2), Collections.<Identity>emptySet());
-               verifyIdentitiesForOwnIdentity(identities, ownIdentities.get(3), createTrustedIdentitiesForFourthOwnIdentity());
-       }
-
-       @Test
-       public void loadingIdentitiesWithoutContext() throws WebOfTrustException {
-               List<OwnIdentity> ownIdentities = createOwnIdentities();
-               Map<OwnIdentity, Collection<Identity>> identities = identityLoaderWithoutContext.loadIdentities();
-               verify(webOfTrustConnector).loadAllOwnIdentities();
-               verify(webOfTrustConnector).loadTrustedIdentities(eq(ownIdentities.get(0)), eq(Optional.<String>absent()));
-               verify(webOfTrustConnector).loadTrustedIdentities(eq(ownIdentities.get(1)), eq(Optional.<String>absent()));
-               verify(webOfTrustConnector).loadTrustedIdentities(eq(ownIdentities.get(2)), eq(Optional.<String>absent()));
-               verify(webOfTrustConnector).loadTrustedIdentities(eq(ownIdentities.get(3)), eq(Optional.<String>absent()));
-               assertThat(identities.keySet(), hasSize(4));
-               OwnIdentity firstOwnIdentity = ownIdentities.get(0);
-               OwnIdentity secondOwnIdentity = ownIdentities.get(1);
-               OwnIdentity thirdOwnIdentity = ownIdentities.get(2);
-               OwnIdentity fourthOwnIdentity = ownIdentities.get(3);
-               assertThat(identities.keySet(), containsInAnyOrder(firstOwnIdentity, secondOwnIdentity, thirdOwnIdentity, fourthOwnIdentity));
-               verifyIdentitiesForOwnIdentity(identities, firstOwnIdentity, createTrustedIdentitiesForFirstOwnIdentity());
-               verifyIdentitiesForOwnIdentity(identities, secondOwnIdentity, createTrustedIdentitiesForSecondOwnIdentity());
-               verifyIdentitiesForOwnIdentity(identities, thirdOwnIdentity, createTrustedIdentitiesForThirdOwnIdentity());
-               verifyIdentitiesForOwnIdentity(identities, fourthOwnIdentity, createTrustedIdentitiesForFourthOwnIdentity());
-       }
-
-       private void verifyIdentitiesForOwnIdentity(Map<OwnIdentity, Collection<Identity>> identities, OwnIdentity ownIdentity, Set<Identity> trustedIdentities) {
-               assertThat(identities.get(ownIdentity), Matchers.<Collection<Identity>>is(trustedIdentities));
-       }
-
-}
diff --git a/src/test/java/net/pterodactylus/sone/freenet/wot/IdentityManagerTest.java b/src/test/java/net/pterodactylus/sone/freenet/wot/IdentityManagerTest.java
deleted file mode 100644 (file)
index 45e95db..0000000
+++ /dev/null
@@ -1,37 +0,0 @@
-package net.pterodactylus.sone.freenet.wot;
-
-import static com.google.common.base.Optional.of;
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.is;
-import static org.mockito.Mockito.doThrow;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-
-import net.pterodactylus.sone.freenet.plugin.PluginException;
-
-import com.google.common.eventbus.EventBus;
-import org.junit.Test;
-
-/**
- * Unit test for {@link IdentityManagerImpl}.
- */
-public class IdentityManagerTest {
-
-       private final EventBus eventBus = mock(EventBus.class);
-       private final WebOfTrustConnector webOfTrustConnector = mock(WebOfTrustConnector.class);
-       private final IdentityManager identityManager = new IdentityManagerImpl(eventBus, webOfTrustConnector, new IdentityLoader(webOfTrustConnector, of(new Context("Test"))));
-
-       @Test
-       public void identityManagerPingsWotConnector() throws PluginException {
-               assertThat(identityManager.isConnected(), is(true));
-               verify(webOfTrustConnector).ping();
-       }
-
-       @Test
-       public void disconnectedWotConnectorIsRecognized() throws PluginException {
-               doThrow(PluginException.class).when(webOfTrustConnector).ping();
-               assertThat(identityManager.isConnected(), is(false));
-               verify(webOfTrustConnector).ping();
-       }
-
-}
diff --git a/src/test/java/net/pterodactylus/sone/freenet/wot/event/IdentityEventTest.java b/src/test/java/net/pterodactylus/sone/freenet/wot/event/IdentityEventTest.java
deleted file mode 100644 (file)
index 4fa5bc9..0000000
+++ /dev/null
@@ -1,52 +0,0 @@
-package net.pterodactylus.sone.freenet.wot.event;
-
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.is;
-import static org.hamcrest.Matchers.not;
-import static org.mockito.Mockito.mock;
-
-import net.pterodactylus.sone.freenet.wot.Identity;
-import net.pterodactylus.sone.freenet.wot.OwnIdentity;
-
-import org.junit.Test;
-
-/**
- * Unit test for {@link IdentityEvent}.
- */
-public class IdentityEventTest {
-
-       private final OwnIdentity ownIdentity = mock(OwnIdentity.class);
-       private final Identity identity = mock(Identity.class);
-       private final IdentityEvent identityEvent = createIdentityEvent(ownIdentity, identity);
-
-       private IdentityEvent createIdentityEvent(final OwnIdentity ownIdentity, final Identity identity) {
-               return new IdentityEvent(ownIdentity, identity) {
-               };
-       }
-
-       @Test
-       public void identityEventRetainsIdentities() {
-               assertThat(identityEvent.ownIdentity(), is(ownIdentity));
-               assertThat(identityEvent.identity(), is(identity));
-       }
-
-       @Test
-       public void eventsWithTheSameIdentityHaveTheSameHashCode() {
-               IdentityEvent secondIdentityEvent = createIdentityEvent(ownIdentity, identity);
-               assertThat(identityEvent.hashCode(), is(secondIdentityEvent.hashCode()));
-       }
-
-       @Test
-       public void eventsWithTheSameIdentitiesAreEqual() {
-               IdentityEvent secondIdentityEvent = createIdentityEvent(ownIdentity, identity);
-               assertThat(identityEvent, is(secondIdentityEvent));
-               assertThat(secondIdentityEvent, is(identityEvent));
-       }
-
-       @Test
-       public void nullDoesNotEqualIdentityEvent() {
-               assertThat(identityEvent, not(is((Object) null)));
-       }
-
-
-}
diff --git a/src/test/java/net/pterodactylus/sone/freenet/wot/event/OwnIdentityEventTest.java b/src/test/java/net/pterodactylus/sone/freenet/wot/event/OwnIdentityEventTest.java
deleted file mode 100644 (file)
index 3d27c34..0000000
+++ /dev/null
@@ -1,46 +0,0 @@
-package net.pterodactylus.sone.freenet.wot.event;
-
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.is;
-import static org.hamcrest.Matchers.not;
-import static org.mockito.Mockito.mock;
-
-import net.pterodactylus.sone.freenet.wot.OwnIdentity;
-
-import org.junit.Test;
-
-/**
- * Unit test for {@link OwnIdentityEvent}.
- */
-public class OwnIdentityEventTest {
-
-       private final OwnIdentity ownIdentity = mock(OwnIdentity.class);
-       private final OwnIdentityEvent ownIdentityEvent = createOwnIdentityEvent(ownIdentity);
-
-       @Test
-       public void eventRetainsOwnIdentity() {
-               assertThat(ownIdentityEvent.ownIdentity(), is(ownIdentity));
-       }
-
-       protected OwnIdentityEvent createOwnIdentityEvent(final OwnIdentity ownIdentity) {
-               return new OwnIdentityEvent(ownIdentity) {
-               };
-       }
-
-       @Test
-       public void twoOwnIdentityEventsWithTheSameIdentityHaveTheSameHashCode() {
-               OwnIdentityEvent secondOwnIdentityEvent = createOwnIdentityEvent(ownIdentity);
-               assertThat(secondOwnIdentityEvent.hashCode(), is(ownIdentityEvent.hashCode()));
-       }
-
-       @Test
-       public void ownIdentityEventDoesNotMatchNull() {
-               assertThat(ownIdentityEvent, not(is((Object) null)));
-       }
-
-       @Test
-       public void ownIdentityEventDoesNotMatchObjectWithADifferentClass() {
-               assertThat(ownIdentityEvent, not(is(new Object())));
-       }
-
-}
index 7a84978..e85c131 100644 (file)
@@ -21,9 +21,7 @@ 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.l10n.BaseL10n;
 import freenet.support.api.HTTPRequest;
 
 import com.google.common.base.Charsets;
@@ -41,7 +39,6 @@ public class DebugLoadersTest {
        @Rule
        public final TemporaryFolder temporaryFolder = new TemporaryFolder();
 
-       private final BaseL10n l10n = mock(BaseL10n.class);
        private final StringWriter stringWriter = new StringWriter();
        private final TemplateContext templateContext = new TemplateContext();
        private Loaders loaders;
@@ -71,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, l10n, sessionManager);
+               FreenetRequest request = new FreenetRequest(uri, method, httpRequest, toadletContext);
                OutputStream outputStream = new ByteArrayOutputStream();
                Response response = new Response(outputStream);
                page.handleRequest(request, response);
index dc51479..5c71b73 100644 (file)
@@ -21,9 +21,7 @@ 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.l10n.BaseL10n;
 import freenet.support.api.HTTPRequest;
 
 import org.junit.Test;
@@ -33,7 +31,6 @@ import org.junit.Test;
  */
 public class DefaultLoadersTest {
 
-       private final BaseL10n l10n = mock(BaseL10n.class);
        private final Loaders loaders = new DefaultLoaders();
        private final StringWriter stringWriter = new StringWriter();
        private final TemplateContext templateContext = new TemplateContext();
@@ -52,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, l10n, sessionManager);
+               FreenetRequest request = new FreenetRequest(uri, method, httpRequest, toadletContext);
                OutputStream outputStream = new ByteArrayOutputStream();
                Response response = new Response(outputStream);
                staticPage.handleRequest(request, response);
diff --git a/src/test/java/net/pterodactylus/sone/notify/ListNotificationTest.java b/src/test/java/net/pterodactylus/sone/notify/ListNotificationTest.java
deleted file mode 100644 (file)
index 5e40f81..0000000
+++ /dev/null
@@ -1,120 +0,0 @@
-package net.pterodactylus.sone.notify;
-
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.emptyIterable;
-import static org.hamcrest.Matchers.is;
-import static org.hamcrest.Matchers.not;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-import static org.mockito.hamcrest.MockitoHamcrest.argThat;
-
-import java.util.Arrays;
-
-import net.pterodactylus.util.notify.NotificationListener;
-import net.pterodactylus.util.template.Template;
-import net.pterodactylus.util.template.TemplateContext;
-
-import org.hamcrest.Matchers;
-import org.junit.Test;
-
-/**
- * Unit test for {@link ListNotification}.
- */
-public class ListNotificationTest {
-
-       private static final String ID = "notification-id";
-       private static final String KEY = "element-key";
-       private static final String OTHER_KEY = "other-key";
-
-       private final Template template = mock(Template.class);
-       private final TemplateContext templateInitialContext = mock(TemplateContext.class);
-       private ListNotification<Object> listNotification;
-
-       public ListNotificationTest() {
-               when(template.getInitialContext()).thenReturn(templateInitialContext);
-               listNotification = new ListNotification<>(ID, KEY, template);
-       }
-
-       @Test
-       public void creatingAListNotificationSetsEmptyIterableOnElementKeyInTemplateContext() {
-               verify(templateInitialContext).set(eq(KEY), argThat(emptyIterable()));
-       }
-
-       @Test
-       public void newListNotificationHasNoElement() {
-               assertThat(listNotification.getElements(), emptyIterable());
-       }
-
-       @Test
-       public void newListNotificationIsEmpty() {
-               assertThat(listNotification.isEmpty(), is(true));
-       }
-
-       @Test
-       public void listNotificationRetainsSetElements() {
-               listNotification.setElements(Arrays.asList("a", "b", "c"));
-               assertThat(listNotification.getElements(), Matchers.<Object>contains("a", "b", "c"));
-       }
-
-       @Test
-       public void listNotificationRetainsAddedElements() {
-               listNotification.add("a");
-               listNotification.add("b");
-               listNotification.add("c");
-               assertThat(listNotification.getElements(), Matchers.<Object>contains("a", "b", "c"));
-       }
-
-       @Test
-       public void listNotificationRemovesCorrectElement() {
-               listNotification.setElements(Arrays.asList("a", "b", "c"));
-               listNotification.remove("b");
-               assertThat(listNotification.getElements(), Matchers.<Object>contains("a", "c"));
-       }
-
-       @Test
-       public void removingTheLastElementDismissesTheNotification() {
-               NotificationListener notificationListener = mock(NotificationListener.class);
-               listNotification.addNotificationListener(notificationListener);
-               listNotification.add("a");
-               listNotification.remove("a");
-               verify(notificationListener).notificationDismissed(listNotification);
-       }
-
-       @Test
-       public void dismissingTheListNotificationRemovesAllElements() {
-               listNotification.setElements(Arrays.asList("a", "b", "c"));
-               listNotification.dismiss();
-               assertThat(listNotification.getElements(), emptyIterable());
-       }
-
-       @Test
-       public void listNotificationWithDifferentElementsIsNotEqual() {
-               ListNotification secondNotification = new ListNotification(ID, KEY, template);
-               listNotification.add("a");
-               secondNotification.add("b");
-               assertThat(listNotification, not(is(secondNotification)));
-       }
-
-       @Test
-       public void listNotificationWithDifferentKeyIsNotEqual() {
-               ListNotification secondNotification = new ListNotification(ID, OTHER_KEY, template);
-               assertThat(listNotification, not(is(secondNotification)));
-       }
-
-       @Test
-       public void copiedNotificationsHaveTheSameHashCode() {
-               ListNotification secondNotification = new ListNotification(listNotification);
-               listNotification.add("a");
-               secondNotification.add("a");
-               listNotification.setLastUpdateTime(secondNotification.getLastUpdatedTime());
-               assertThat(listNotification.hashCode(), is(secondNotification.hashCode()));
-       }
-
-       @Test
-       public void listNotificationIsNotEqualToOtherObjects() {
-           assertThat(listNotification, not(is(new Object())));
-       }
-
-}
diff --git a/src/test/java/net/pterodactylus/sone/template/FilesystemTemplateTest.java b/src/test/java/net/pterodactylus/sone/template/FilesystemTemplateTest.java
deleted file mode 100644 (file)
index 9259ef5..0000000
+++ /dev/null
@@ -1,125 +0,0 @@
-package net.pterodactylus.sone.template;
-
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.is;
-import static org.hamcrest.Matchers.notNullValue;
-
-import java.io.File;
-import java.io.IOException;
-import java.io.StringWriter;
-import java.io.Writer;
-import java.util.Arrays;
-import java.util.concurrent.atomic.AtomicReference;
-
-import net.pterodactylus.util.template.Part;
-import net.pterodactylus.util.template.TemplateContext;
-import net.pterodactylus.util.template.TemplateException;
-
-import com.google.common.base.Charsets;
-import com.google.common.io.Files;
-import org.junit.Before;
-import org.junit.Test;
-
-/**
- * Unit test for {@link FilesystemTemplate}.
- */
-public class FilesystemTemplateTest {
-
-       private final File tempFile;
-       private final FilesystemTemplate filesystemTemplate;
-       private final AtomicReference<StringWriter> stringWriter = new AtomicReference<>(new StringWriter());
-       private final TemplateContext templateContext = new TemplateContext();
-
-       public FilesystemTemplateTest() throws IOException {
-               tempFile = File.createTempFile("template-", ".dat");
-               writeTemplate("Text");
-               filesystemTemplate = new FilesystemTemplate(tempFile.getAbsolutePath());
-       }
-
-       private void writeTemplate(String text) throws IOException {
-               Files.write(text + ".<%foreach values value><% value><%/foreach>", tempFile, Charsets.UTF_8);
-       }
-
-       @Before
-       public void setupTemplateContext() {
-               templateContext.set("values", Arrays.asList("a", 1));
-       }
-
-       @Test(expected = FilesystemTemplate.TemplateFileNotFoundException.class)
-       public void loadingTemplateFromNonExistingFileThrowsException() throws IOException {
-               FilesystemTemplate filesystemTemplate = new FilesystemTemplate("/a/b/c.dat");
-               filesystemTemplate.getInitialContext();
-       }
-
-       @Test
-       public void templateCanBeLoadedFromTheFilesystem() {
-               filesystemTemplate.render(templateContext, stringWriter.get());
-               assertThat(getRenderedString(), is("Text.a1"));
-       }
-
-       @Test
-       public void templateCanBeReloaded() throws IOException, InterruptedException {
-               filesystemTemplate.render(templateContext, stringWriter.get());
-               assertThat(getRenderedString(), is("Text.a1"));
-               Thread.sleep(1000);
-               writeTemplate("New");
-               filesystemTemplate.render(templateContext, stringWriter.get());
-               assertThat(getRenderedString(), is("New.a1"));
-       }
-
-       @Test
-       public void templateIsNotReloadedIfNotChanged() {
-               filesystemTemplate.render(templateContext, stringWriter.get());
-               assertThat(getRenderedString(), is("Text.a1"));
-               filesystemTemplate.render(templateContext, stringWriter.get());
-               assertThat(getRenderedString(), is("Text.a1"));
-       }
-
-       private String getRenderedString() {
-               String renderedString = stringWriter.get().toString();
-               stringWriter.set(new StringWriter());
-               return renderedString;
-       }
-
-       @Test
-       public void initialContextIsCopiedToReloadedTemplates() throws IOException, InterruptedException {
-               filesystemTemplate.getInitialContext().set("values", "test");
-               Thread.sleep(1000);
-               writeTemplate("New");
-               assertThat(filesystemTemplate.getInitialContext().get("values"), is((Object) "test"));
-       }
-
-       @Test
-       public void partsAreCopiedToReloadedTemplates() throws InterruptedException, IOException {
-               filesystemTemplate.add(new Part() {
-                       @Override
-                       public void render(TemplateContext templateContext, Writer writer) throws TemplateException {
-                               try {
-                                       writer.write(".Test");
-                               } catch (IOException e) {
-                                       throw new TemplateException(e);
-                               }
-                       }
-               });
-               Thread.sleep(1000);
-               writeTemplate("New");
-               filesystemTemplate.render(templateContext, stringWriter.get());
-               assertThat(getRenderedString(), is("New.a1.Test"));
-       }
-
-       @Test
-       public void columnOfReturnedTemplateIsReturnedAsZero() {
-               assertThat(filesystemTemplate.getColumn(), is(0));
-       }
-
-       @Test
-       public void lineOfReturnedTemplateIsReturnedAsZero() {
-               assertThat(filesystemTemplate.getLine(), is(0));
-       }
-
-       @Test
-       public void templateCanBeIteratedOver() {
-           assertThat(filesystemTemplate.iterator(), notNullValue());
-       }
-
-}
diff --git a/src/test/java/net/pterodactylus/sone/template/PostAccessorTest.java b/src/test/java/net/pterodactylus/sone/template/PostAccessorTest.java
deleted file mode 100644 (file)
index e60dce9..0000000
+++ /dev/null
@@ -1,118 +0,0 @@
-package net.pterodactylus.sone.template;
-
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.contains;
-import static org.hamcrest.Matchers.is;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
-
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.List;
-import java.util.Set;
-
-import net.pterodactylus.sone.core.Core;
-import net.pterodactylus.sone.data.Post;
-import net.pterodactylus.sone.data.PostReply;
-import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.util.template.TemplateContext;
-
-import org.junit.Before;
-import org.junit.Test;
-
-/**
- * Unit test for {@link PostAccessor}.
- */
-public class PostAccessorTest {
-
-       private final Core core = mock(Core.class);
-       private final PostAccessor accessor = new PostAccessor(core);
-       private final Post post = mock(Post.class);
-
-       private final long now = System.currentTimeMillis();
-
-       @Before
-       public void setupPost() {
-               when(post.getId()).thenReturn("post-id");
-       }
-
-       @Test
-       @SuppressWarnings("unchecked")
-       public void accessorReturnsTheCorrectReplies() {
-               List<PostReply> replies = new ArrayList<>();
-               replies.add(createPostReply(2000));
-               replies.add(createPostReply(-1000));
-               replies.add(createPostReply(-2000));
-               replies.add(createPostReply(-3000));
-               replies.add(createPostReply(-4000));
-               when(core.getReplies("post-id")).thenReturn(replies);
-               Collection<PostReply> repliesForPost = (Collection<PostReply>) accessor.get(null, post, "replies");
-               assertThat(repliesForPost, contains(
-                               replies.get(1),
-                               replies.get(2),
-                               replies.get(3),
-                               replies.get(4)
-               ));
-       }
-
-       private PostReply createPostReply(long timeOffset) {
-               PostReply postReply = mock(PostReply.class);
-               when(postReply.getTime()).thenReturn(now + timeOffset);
-               return postReply;
-       }
-
-       @Test
-       @SuppressWarnings("unchecked")
-       public void accessorReturnsTheLikingSones() {
-               Set<Sone> sones = mock(Set.class);
-               when(core.getLikes(post)).thenReturn(sones);
-               Set<Sone> likingSones = (Set<Sone>) accessor.get(null, post, "likes");
-               assertThat(likingSones, is(sones));
-       }
-
-       @Test
-       public void accessorReturnsWhetherTheCurrentSoneLikedAPost() {
-               Sone sone = mock(Sone.class);
-               when(sone.isLikedPostId("post-id")).thenReturn(true);
-               TemplateContext templateContext = new TemplateContext();
-               templateContext.set("currentSone", sone);
-               assertThat(accessor.get(templateContext, post, "liked"), is((Object) true));
-       }
-
-       @Test
-       public void accessorReturnsFalseIfPostIsNotLiked() {
-               Sone sone = mock(Sone.class);
-               TemplateContext templateContext = new TemplateContext();
-               templateContext.set("currentSone", sone);
-               assertThat(accessor.get(templateContext, post, "liked"), is((Object) false));
-       }
-
-       @Test
-       public void accessorReturnsFalseIfThereIsNoCurrentSone() {
-               TemplateContext templateContext = new TemplateContext();
-               assertThat(accessor.get(templateContext, post, "liked"), is((Object) false));
-       }
-
-       @Test
-       public void accessorReturnsThatNotKnownPostIsNew() {
-               assertThat(accessor.get(null, post, "new"), is((Object) true));
-       }
-
-       @Test
-       public void accessorReturnsThatKnownPostIsNotNew() {
-               when(post.isKnown()).thenReturn(true);
-               assertThat(accessor.get(null, post, "new"), is((Object) false));
-       }
-
-       @Test
-       public void accessorReturnsIfPostIsBookmarked() {
-               when(core.isBookmarked(post)).thenReturn(true);
-               assertThat(accessor.get(null, post, "bookmarked"), is((Object) true));
-       }
-
-       @Test
-       public void accessorReturnsOtherProperties() {
-               assertThat(accessor.get(null, post, "hashCode"), is((Object) post.hashCode()));
-       }
-
-}
index d04da0f..bd571e7 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - Matchers.java - Copyright © 2013–2019 David Roden
+ * Sone - Matchers.java - Copyright © 2013–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -22,12 +22,13 @@ import static java.util.regex.Pattern.compile;
 import java.io.IOException;
 import java.io.InputStream;
 
+import javax.annotation.*;
+
 import net.pterodactylus.sone.data.Album;
 import net.pterodactylus.sone.data.Image;
 import net.pterodactylus.sone.data.Post;
 import net.pterodactylus.sone.data.PostReply;
 
-import com.google.common.base.Optional;
 import org.hamcrest.Description;
 import org.hamcrest.Matcher;
 import org.hamcrest.TypeSafeDiagnosingMatcher;
@@ -91,8 +92,7 @@ public class Matchers {
                };
        }
 
-       public static Matcher<Post> isPost(String postId, long time,
-                       String text, Optional<String> recipient) {
+       public static Matcher<Post> isPost(String postId, long time, String text, @Nullable String recipient) {
                return new PostMatcher(postId, time, text, recipient);
        }
 
@@ -229,10 +229,10 @@ public class Matchers {
                private final String postId;
                private final long time;
                private final String text;
-               private final Optional<String> recipient;
+               @Nullable
+               private final String recipient;
 
-               private PostMatcher(String postId, long time, String text,
-                               Optional<String> recipient) {
+               private PostMatcher(String postId, long time, String text, @Nullable String recipient) {
                        this.postId = postId;
                        this.time = time;
                        this.text = text;
@@ -257,15 +257,15 @@ public class Matchers {
                                                .appendValue(text);
                                return false;
                        }
-                       if (recipient.isPresent()) {
+                       if (recipient != null) {
                                if (!post.getRecipientId().isPresent()) {
                                        mismatchDescription.appendText(
                                                        "Recipient not present");
                                        return false;
                                }
-                               if (!post.getRecipientId().get().equals(recipient.get())) {
+                               if (!post.getRecipientId().get().equals(recipient)) {
                                        mismatchDescription.appendText("Recipient is not ")
-                                                       .appendValue(recipient.get());
+                                                       .appendValue(recipient);
                                        return false;
                                }
                        } else {
@@ -283,9 +283,9 @@ public class Matchers {
                                        .appendValue(postId);
                        description.appendText(", created at @").appendValue(time);
                        description.appendText(", text ").appendValue(text);
-                       if (recipient.isPresent()) {
+                       if (recipient != null) {
                                description.appendText(", directed at ")
-                                               .appendValue(recipient.get());
+                                               .appendValue(recipient);
                        }
                }
 
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));
-       }
-
-}
diff --git a/src/test/kotlin/net/pterodactylus/sone/core/ConfigurationSoneParserTest.kt b/src/test/kotlin/net/pterodactylus/sone/core/ConfigurationSoneParserTest.kt
new file mode 100644 (file)
index 0000000..29ade5f
--- /dev/null
@@ -0,0 +1,435 @@
+package net.pterodactylus.sone.core
+
+import net.pterodactylus.sone.core.ConfigurationSoneParser.*
+import net.pterodactylus.sone.data.*
+import net.pterodactylus.sone.database.*
+import net.pterodactylus.sone.test.*
+import net.pterodactylus.sone.test.Matchers.*
+import net.pterodactylus.util.config.*
+import org.hamcrest.MatcherAssert.*
+import org.hamcrest.Matchers.*
+import org.junit.*
+import org.junit.rules.*
+import org.mockito.ArgumentMatchers.*
+import org.mockito.ArgumentMatchers.eq
+
+/**
+ * Unit test for [ConfigurationSoneParser].
+ */
+class ConfigurationSoneParserTest {
+
+       @Rule
+       @JvmField
+       val expectedException = ExpectedException.none()!!
+
+       private val configuration = mock<Configuration>()
+       private val sone = mock<Sone>().apply {
+               whenever(this.id).thenReturn("1")
+       }
+       private val configurationSoneParser = ConfigurationSoneParser(configuration, sone)
+
+       @Test
+       fun emptyProfileIsLoadedCorrectly() {
+               setupEmptyProfile()
+               val profile = configurationSoneParser.parseProfile()
+               assertThat(profile, notNullValue())
+               assertThat(profile.firstName, nullValue())
+               assertThat(profile.middleName, nullValue())
+               assertThat(profile.lastName, nullValue())
+               assertThat(profile.birthDay, nullValue())
+               assertThat(profile.birthMonth, nullValue())
+               assertThat(profile.birthYear, nullValue())
+               assertThat(profile.fields, emptyIterable())
+       }
+
+       private fun setupEmptyProfile() {
+               whenever(configuration.getStringValue(anyString())).thenReturn(TestValue.from(null))
+               whenever(configuration.getIntValue(anyString())).thenReturn(TestValue.from(null))
+       }
+
+       @Test
+       fun filledProfileWithFieldsIsParsedCorrectly() {
+               setupFilledProfile()
+               val profile = configurationSoneParser.parseProfile()
+               assertThat(profile, notNullValue())
+               assertThat(profile.firstName, equalTo("First"))
+               assertThat(profile.middleName, equalTo("M."))
+               assertThat(profile.lastName, equalTo("Last"))
+               assertThat(profile.birthDay, equalTo(18))
+               assertThat(profile.birthMonth, equalTo(12))
+               assertThat(profile.birthYear, equalTo(1976))
+               val fields = profile.fields
+               assertThat(fields, hasSize<Any>(2))
+               assertThat(fields[0].name, equalTo("Field1"))
+               assertThat(fields[0].value, equalTo("Value1"))
+               assertThat(fields[1].name, equalTo("Field2"))
+               assertThat(fields[1].value, equalTo("Value2"))
+       }
+
+       private fun setupFilledProfile() {
+               setupString("Sone/1/Profile/FirstName", "First")
+               setupString("Sone/1/Profile/MiddleName", "M.")
+               setupString("Sone/1/Profile/LastName", "Last")
+               setupInteger("Sone/1/Profile/BirthDay", 18)
+               setupInteger("Sone/1/Profile/BirthMonth", 12)
+               setupInteger("Sone/1/Profile/BirthYear", 1976)
+               setupString("Sone/1/Profile/Fields/0/Name", "Field1")
+               setupString("Sone/1/Profile/Fields/0/Value", "Value1")
+               setupString("Sone/1/Profile/Fields/1/Name", "Field2")
+               setupString("Sone/1/Profile/Fields/1/Value", "Value2")
+               setupString("Sone/1/Profile/Fields/2/Name")
+       }
+
+       private fun setupString(nodeName: String, value: String? = null) {
+               whenever(configuration.getStringValue(eq(nodeName))).thenReturn(TestValue.from(value))
+       }
+
+       private fun setupInteger(nodeName: String, value: Int?) {
+               whenever(configuration.getIntValue(eq(nodeName))).thenReturn(TestValue.from(value))
+       }
+
+       @Test
+       fun postsAreParsedCorrectly() {
+               setupCompletePosts()
+               val postBuilderFactory = createPostBuilderFactory()
+               val posts = configurationSoneParser.parsePosts(postBuilderFactory)
+               assertThat(posts, containsInAnyOrder(
+                               isPost("P0", 1000L, "T0", null),
+                               isPost("P1", 1001L, "T1", "1234567890123456789012345678901234567890123")
+               ))
+       }
+
+       private fun createPostBuilderFactory(): PostBuilderFactory {
+               val postBuilderFactory = mock<PostBuilderFactory>()
+               whenever(postBuilderFactory.newPostBuilder()).thenAnswer { TestPostBuilder() }
+               return postBuilderFactory
+       }
+
+       private fun setupCompletePosts() {
+               setupPost("0", "P0", 1000L, "T0")
+               setupPost("1", "P1", 1001L, "T1", "1234567890123456789012345678901234567890123")
+               setupPost("2")
+       }
+
+       private fun setupPost(postNumber: String, postId: String? = null, time: Long = 0, text: String? = null, recipientId: String? = null) {
+               setupString("Sone/1/Posts/$postNumber/ID", postId)
+               setupLong("Sone/1/Posts/$postNumber/Time", time)
+               setupString("Sone/1/Posts/$postNumber/Text", text)
+               setupString("Sone/1/Posts/$postNumber/Recipient", recipientId)
+       }
+
+       private fun setupLong(nodeName: String, value: Long?) {
+               whenever(configuration.getLongValue(eq(nodeName))).thenReturn(TestValue.from(value))
+       }
+
+       @Test
+       fun postWithoutTimeIsRecognized() {
+               setupPostWithoutTime()
+               expectedException.expect<InvalidPostFound>()
+               configurationSoneParser.parsePosts(createPostBuilderFactory())
+       }
+
+       private fun setupPostWithoutTime() {
+               setupPost("0", "P0", 0L, "T0")
+       }
+
+       @Test
+       fun postWithoutTextIsRecognized() {
+               setupPostWithoutText()
+               expectedException.expect<InvalidPostFound>()
+               configurationSoneParser.parsePosts(createPostBuilderFactory())
+       }
+
+       private fun setupPostWithoutText() {
+               setupPost("0", "P0", 1000L)
+       }
+
+       @Test
+       fun postWithInvalidRecipientIdIsRecognized() {
+               setupPostWithInvalidRecipientId()
+               val posts = configurationSoneParser.parsePosts(createPostBuilderFactory())
+               assertThat(posts, contains(isPost("P0", 1000L, "T0", null)))
+       }
+
+       private fun setupPostWithInvalidRecipientId() {
+               setupPost("0", "P0", 1000L, "T0", "123")
+               setupPost("1")
+       }
+
+       @Test
+       fun postRepliesAreParsedCorrectly() {
+               setupPostReplies()
+               val postReplyBuilderFactory = object : PostReplyBuilderFactory {
+                       override fun newPostReplyBuilder(): PostReplyBuilder {
+                               return TestPostReplyBuilder()
+                       }
+               }
+               val postReplies = configurationSoneParser.parsePostReplies(postReplyBuilderFactory)
+               assertThat(postReplies, hasSize(2))
+               assertThat(postReplies, containsInAnyOrder(
+                               isPostReply("R0", "P0", 1000L, "T0"),
+                               isPostReply("R1", "P1", 1001L, "T1")
+               ))
+       }
+
+       private fun setupPostReplies() {
+               setupPostReply("0", "R0", "P0", 1000L, "T0")
+               setupPostReply("1", "R1", "P1", 1001L, "T1")
+               setupPostReply("2")
+       }
+
+       private fun setupPostReply(postReplyNumber: String, postReplyId: String? = null, postId: String? = null, time: Long = 0, text: String? = null) {
+               setupString("Sone/1/Replies/$postReplyNumber/ID", postReplyId)
+               setupString("Sone/1/Replies/$postReplyNumber/Post/ID", postId)
+               setupLong("Sone/1/Replies/$postReplyNumber/Time", time)
+               setupString("Sone/1/Replies/$postReplyNumber/Text", text)
+       }
+
+       @Test
+       fun missingPostIdIsRecognized() {
+               setupPostReplyWithMissingPostId()
+               expectedException.expect<InvalidPostReplyFound>()
+               configurationSoneParser.parsePostReplies(null)
+       }
+
+       private fun setupPostReplyWithMissingPostId() {
+               setupPostReply("0", "R0", null, 1000L, "T0")
+       }
+
+       @Test
+       fun missingPostReplyTimeIsRecognized() {
+               setupPostReplyWithMissingPostReplyTime()
+               expectedException.expect<InvalidPostReplyFound>()
+               configurationSoneParser.parsePostReplies(null)
+       }
+
+       private fun setupPostReplyWithMissingPostReplyTime() {
+               setupPostReply("0", "R0", "P0", 0L, "T0")
+       }
+
+       @Test
+       fun missingPostReplyTextIsRecognized() {
+               setupPostReplyWithMissingPostReplyText()
+               expectedException.expect<InvalidPostReplyFound>()
+               configurationSoneParser.parsePostReplies(null)
+       }
+
+       private fun setupPostReplyWithMissingPostReplyText() {
+               setupPostReply("0", "R0", "P0", 1000L)
+       }
+
+       @Test
+       fun likedPostIdsParsedCorrectly() {
+               setupLikedPostIds()
+               val likedPostIds = configurationSoneParser.parseLikedPostIds()
+               assertThat(likedPostIds, containsInAnyOrder("P1", "P2", "P3"))
+       }
+
+       private fun setupLikedPostIds() {
+               setupString("Sone/1/Likes/Post/0/ID", "P1")
+               setupString("Sone/1/Likes/Post/1/ID", "P2")
+               setupString("Sone/1/Likes/Post/2/ID", "P3")
+               setupString("Sone/1/Likes/Post/3/ID")
+       }
+
+       @Test
+       fun likedPostReplyIdsAreParsedCorrectly() {
+               setupLikedPostReplyIds()
+               val likedPostReplyIds = configurationSoneParser.parseLikedPostReplyIds()
+               assertThat(likedPostReplyIds, containsInAnyOrder("R1", "R2", "R3"))
+       }
+
+       private fun setupLikedPostReplyIds() {
+               setupString("Sone/1/Likes/Reply/0/ID", "R1")
+               setupString("Sone/1/Likes/Reply/1/ID", "R2")
+               setupString("Sone/1/Likes/Reply/2/ID", "R3")
+               setupString("Sone/1/Likes/Reply/3/ID")
+       }
+
+       @Test
+       fun friendsAreParsedCorrectly() {
+               setupFriends()
+               val friends = configurationSoneParser.parseFriends()
+               assertThat(friends, containsInAnyOrder("F1", "F2", "F3"))
+       }
+
+       private fun setupFriends() {
+               setupString("Sone/1/Friends/0/ID", "F1")
+               setupString("Sone/1/Friends/1/ID", "F2")
+               setupString("Sone/1/Friends/2/ID", "F3")
+               setupString("Sone/1/Friends/3/ID")
+       }
+
+       @Test
+       fun topLevelAlbumsAreParsedCorrectly() {
+               setupTopLevelAlbums()
+               val albumBuilderFactory = createAlbumBuilderFactory()
+               val topLevelAlbums = configurationSoneParser.parseTopLevelAlbums(albumBuilderFactory)
+               assertThat(topLevelAlbums, hasSize<Any>(2))
+               val firstAlbum = topLevelAlbums[0]
+               assertThat(firstAlbum, isAlbum("A1", null, "T1", "D1"))
+               assertThat(firstAlbum.albums, emptyIterable<Any>())
+               assertThat<List<Image>>(firstAlbum.images, emptyIterable<Any>())
+               val secondAlbum = topLevelAlbums[1]
+               assertThat(secondAlbum, isAlbum("A2", null, "T2", "D2"))
+               assertThat(secondAlbum.albums, hasSize<Any>(1))
+               assertThat<List<Image>>(secondAlbum.images, emptyIterable<Any>())
+               val thirdAlbum = secondAlbum.albums[0]
+               assertThat(thirdAlbum, isAlbum("A3", "A2", "T3", "D3"))
+               assertThat(thirdAlbum.albums, emptyIterable<Any>())
+               assertThat<List<Image>>(thirdAlbum.images, emptyIterable<Any>())
+       }
+
+       private fun setupTopLevelAlbums() {
+               setupAlbum(0, "A1", null, "T1", "D1", "I1")
+               setupAlbum(1, "A2", null, "T2", "D2")
+               setupAlbum(2, "A3", "A2", "T3", "D3", "I3")
+               setupAlbum(3)
+       }
+
+       private fun setupAlbum(albumNumber: Int, albumId: String? = null, parentAlbumId: String? = null, title: String? = null, description: String? = null, imageId: String? =null) {
+               val albumPrefix = "Sone/1/Albums/$albumNumber"
+               setupString("$albumPrefix/ID", albumId)
+               setupString("$albumPrefix/Title", title)
+               setupString("$albumPrefix/Description", description)
+               setupString("$albumPrefix/Parent", parentAlbumId)
+               setupString("$albumPrefix/AlbumImage", imageId)
+       }
+
+       private fun createAlbumBuilderFactory(): AlbumBuilderFactory {
+               val albumBuilderFactory = mock<AlbumBuilderFactory>()
+               whenever(albumBuilderFactory.newAlbumBuilder()).thenAnswer { TestAlbumBuilder() }
+               return albumBuilderFactory
+       }
+
+       @Test
+       fun albumWithInvalidTitleIsRecognized() {
+               setupAlbum(0, "A1", null, null, "D1", "I1")
+               expectedException.expect<InvalidAlbumFound>()
+               configurationSoneParser.parseTopLevelAlbums(createAlbumBuilderFactory())
+       }
+
+       @Test
+       fun albumWithInvalidDescriptionIsRecognized() {
+               setupAlbum(0, "A1", null, "T1", null, "I1")
+               expectedException.expect<InvalidAlbumFound>()
+               configurationSoneParser.parseTopLevelAlbums(createAlbumBuilderFactory())
+       }
+
+       @Test
+       fun albumWithInvalidParentIsRecognized() {
+               setupAlbum(0, "A1", "A0", "T1", "D1", "I1")
+               expectedException.expect<InvalidParentAlbumFound>()
+               configurationSoneParser.parseTopLevelAlbums(createAlbumBuilderFactory())
+       }
+
+       @Test
+       fun imagesAreParsedCorrectly() {
+               setupTopLevelAlbums()
+               configurationSoneParser.parseTopLevelAlbums(createAlbumBuilderFactory())
+               setupImages()
+               configurationSoneParser.parseImages(createImageBuilderFactory())
+               val albums = configurationSoneParser.albums
+               assertThat<List<Image>>(albums["A1"]!!.images, contains<Image>(isImage("I1", 1000L, "K1", "T1", "D1", 16, 9)))
+               assertThat<List<Image>>(albums["A2"]!!.images, contains<Image>(isImage("I2", 2000L, "K2", "T2", "D2", 16 * 2, 9 * 2)))
+               assertThat<List<Image>>(albums["A3"]!!.images, contains<Image>(isImage("I3", 3000L, "K3", "T3", "D3", 16 * 3, 9 * 3)))
+       }
+
+       private fun setupImages() {
+               setupImage(0, "I1", "A1", 1000L, "K1", "T1", "D1", 16, 9)
+               setupImage(1, "I2", "A2", 2000L, "K2", "T2", "D2", 16 * 2, 9 * 2)
+               setupImage(2, "I3", "A3", 3000L, "K3", "T3", "D3", 16 * 3, 9 * 3)
+               setupImage(3, null, null, 0L, null, null, null, 0, 0)
+       }
+
+       private fun setupImage(imageNumber: Int, id: String?, parentAlbumId: String?, creationTime: Long?, key: String?, title: String?, description: String?, width: Int?, height: Int?) {
+               val imagePrefix = "Sone/1/Images/$imageNumber"
+               setupString("$imagePrefix/ID", id)
+               setupString("$imagePrefix/Album", parentAlbumId)
+               setupLong("$imagePrefix/CreationTime", creationTime)
+               setupString("$imagePrefix/Key", key)
+               setupString("$imagePrefix/Title", title)
+               setupString("$imagePrefix/Description", description)
+               setupInteger("$imagePrefix/Width", width)
+               setupInteger("$imagePrefix/Height", height)
+       }
+
+       private fun createImageBuilderFactory(): ImageBuilderFactory {
+               val imageBuilderFactory = mock<ImageBuilderFactory>()
+               whenever(imageBuilderFactory.newImageBuilder()).thenAnswer { TestImageBuilder() }
+               return imageBuilderFactory
+       }
+
+       @Test
+       fun missingAlbumIdIsRecognized() {
+               setupTopLevelAlbums()
+               configurationSoneParser.parseTopLevelAlbums(createAlbumBuilderFactory())
+               setupImage(0, "I1", null, 1000L, "K1", "T1", "D1", 16, 9)
+               expectedException.expect<InvalidImageFound>()
+               configurationSoneParser.parseImages(createImageBuilderFactory())
+       }
+
+       @Test
+       fun invalidAlbumIdIsRecognized() {
+               setupTopLevelAlbums()
+               configurationSoneParser.parseTopLevelAlbums(createAlbumBuilderFactory())
+               setupImage(0, "I1", "A4", 1000L, "K1", "T1", "D1", 16, 9)
+               expectedException.expect<InvalidParentAlbumFound>()
+               configurationSoneParser.parseImages(createImageBuilderFactory())
+       }
+
+       @Test
+       fun missingCreationTimeIsRecognized() {
+               setupTopLevelAlbums()
+               configurationSoneParser.parseTopLevelAlbums(createAlbumBuilderFactory())
+               setupImage(0, "I1", "A1", null, "K1", "T1", "D1", 16, 9)
+               expectedException.expect<InvalidImageFound>()
+               configurationSoneParser.parseImages(createImageBuilderFactory())
+       }
+
+       @Test
+       fun missingKeyIsRecognized() {
+               setupTopLevelAlbums()
+               configurationSoneParser.parseTopLevelAlbums(createAlbumBuilderFactory())
+               setupImage(0, "I1", "A1", 1000L, null, "T1", "D1", 16, 9)
+               expectedException.expect<InvalidImageFound>()
+               configurationSoneParser.parseImages(createImageBuilderFactory())
+       }
+
+       @Test
+       fun missingTitleIsRecognized() {
+               setupTopLevelAlbums()
+               configurationSoneParser.parseTopLevelAlbums(createAlbumBuilderFactory())
+               setupImage(0, "I1", "A1", 1000L, "K1", null, "D1", 16, 9)
+               expectedException.expect<InvalidImageFound>()
+               configurationSoneParser.parseImages(createImageBuilderFactory())
+       }
+
+       @Test
+       fun missingDescriptionIsRecognized() {
+               setupTopLevelAlbums()
+               configurationSoneParser.parseTopLevelAlbums(createAlbumBuilderFactory())
+               setupImage(0, "I1", "A1", 1000L, "K1", "T1", null, 16, 9)
+               expectedException.expect<InvalidImageFound>()
+               configurationSoneParser.parseImages(createImageBuilderFactory())
+       }
+
+       @Test
+       fun missingWidthIsRecognized() {
+               setupTopLevelAlbums()
+               configurationSoneParser.parseTopLevelAlbums(createAlbumBuilderFactory())
+               setupImage(0, "I1", "A1", 1000L, "K1", "T1", "D1", null, 9)
+               expectedException.expect<InvalidImageFound>()
+               configurationSoneParser.parseImages(createImageBuilderFactory())
+       }
+
+       @Test
+       fun missingHeightIsRecognized() {
+               setupTopLevelAlbums()
+               configurationSoneParser.parseTopLevelAlbums(createAlbumBuilderFactory())
+               setupImage(0, "I1", "A1", 1000L, "K1", "T1", "D1", 16, null)
+               expectedException.expect<InvalidImageFound>()
+               configurationSoneParser.parseImages(createImageBuilderFactory())
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/core/CoreTest.kt b/src/test/kotlin/net/pterodactylus/sone/core/CoreTest.kt
new file mode 100644 (file)
index 0000000..39273d0
--- /dev/null
@@ -0,0 +1,177 @@
+package net.pterodactylus.sone.core
+
+import com.codahale.metrics.*
+import com.google.common.collect.*
+import com.google.common.eventbus.*
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.sone.data.*
+import net.pterodactylus.sone.database.*
+import net.pterodactylus.sone.freenet.wot.*
+import net.pterodactylus.sone.freenet.wot.event.*
+import net.pterodactylus.sone.test.*
+import net.pterodactylus.util.config.*
+import org.hamcrest.*
+import org.hamcrest.MatcherAssert.*
+import org.hamcrest.Matchers.*
+import org.mockito.Mockito.inOrder
+import org.mockito.Mockito.verify
+import org.mockito.hamcrest.MockitoHamcrest.*
+import kotlin.test.*
+
+/**
+ * Unit test for [Core] and its subclasses.
+ */
+class CoreTest {
+
+       @Test
+       fun `mark post known marks post as known`() {
+               val core = mock<Core>()
+               val post = mock<Post>()
+               core.MarkPostKnown(post).run()
+               verify(core).markPostKnown(eq(post))
+       }
+
+       @Test
+       fun `mark reply known marks reply as known`() {
+               val core = mock<Core>()
+               val postReply = mock<PostReply>()
+               core.MarkReplyKnown(postReply).run()
+               verify(core).markReplyKnown(eq(postReply))
+       }
+
+       @Test
+       fun `removing an identity sends removal events for all sone elements`() {
+               // given
+               val configuration = mock<Configuration>()
+               val freenetInterface = mock<FreenetInterface>()
+               val identityManager = mock<IdentityManager>()
+               val soneDownloader = mock<SoneDownloader>()
+               val imageInserter = mock<ImageInserter>()
+               val updateChecker = mock<UpdateChecker>()
+               val webOfTrustUpdater = mock<WebOfTrustUpdater>()
+               val eventBus = mock<EventBus>()
+               val database = mock<Database>()
+               val metricRegistry = 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")
+               val sone = mock<Sone>()
+               whenever(database.getSone("sone-id")).thenReturn(sone)
+               val postReply1 = mock<PostReply>()
+               val postReply2 = mock<PostReply>()
+               whenever(sone.replies).thenReturn(ImmutableSet.of(postReply1, postReply2))
+               val post1 = mock<Post>()
+               val post2 = mock<Post>()
+               whenever(sone.posts).thenReturn(ImmutableList.of(post1, post2))
+
+               // when
+               core.identityRemoved(IdentityRemovedEvent(ownIdentity, identity))
+
+               // then
+               val inOrder = inOrder(eventBus, database)
+               inOrder.verify(eventBus).post(argThat(isPostReplyRemoved(postReply1)))
+               inOrder.verify(eventBus).post(argThat(isPostReplyRemoved(postReply2)))
+               inOrder.verify(eventBus).post(argThat(isPostRemoved(post1)))
+               inOrder.verify(eventBus).post(argThat(isPostRemoved(post2)))
+               inOrder.verify(eventBus).post(argThat(isSoneRemoved(sone)))
+               inOrder.verify(database).removeSone(sone)
+       }
+
+       private fun isPostRemoved(post: Post): Matcher<Any> {
+               return object : TypeSafeDiagnosingMatcher<Any>() {
+                       override fun matchesSafely(item: Any, mismatchDescription: Description): Boolean {
+                               if (item !is PostRemovedEvent) {
+                                       mismatchDescription.appendText("is not PostRemovedEvent")
+                                       return false
+                               }
+                               if (item.post !== post) {
+                                       mismatchDescription.appendText("post is ").appendValue(item.post)
+                                       return false
+                               }
+                               return true
+                       }
+
+                       override fun describeTo(description: Description) {
+                               description.appendText("is PostRemovedEvent and post is ").appendValue(post)
+                       }
+               }
+       }
+
+       private fun isPostReplyRemoved(postReply: PostReply): Matcher<Any> {
+               return object : TypeSafeDiagnosingMatcher<Any>() {
+                       override fun matchesSafely(item: Any, mismatchDescription: Description): Boolean {
+                               if (item !is PostReplyRemovedEvent) {
+                                       mismatchDescription.appendText("is not PostReplyRemovedEvent")
+                                       return false
+                               }
+                               if (item.postReply !== postReply) {
+                                       mismatchDescription.appendText("post reply is ").appendValue(item.postReply)
+                                       return false
+                               }
+                               return true
+                       }
+
+                       override fun describeTo(description: Description) {
+                               description.appendText("is PostReplyRemovedEvent and post is ").appendValue(postReply)
+                       }
+               }
+       }
+
+       private fun isSoneRemoved(sone: Sone): Matcher<Any> {
+               return object : TypeSafeDiagnosingMatcher<Any>() {
+                       override fun matchesSafely(item: Any, mismatchDescription: Description): Boolean {
+                               if (item !is SoneRemovedEvent) {
+                                       mismatchDescription.appendText("is not SoneRemovedEvent")
+                                       return false
+                               }
+                               if (item.sone !== sone) {
+                                       mismatchDescription.appendText("sone is ").appendValue(item.sone)
+                                       return false
+                               }
+                               return true
+                       }
+
+                       override fun describeTo(description: Description) {
+                               description.appendText("is SoneRemovedEvent and sone is ").appendValue(sone)
+                       }
+               }
+       }
+
+       @Test
+       fun `core starts with debug set to false`() {
+               val core = createCore()
+               assertThat(core.debug, equalTo(false))
+       }
+
+       @Test
+       fun `debug flag can be set`() {
+               val core = createCore()
+               core.setDebug()
+               assertThat(core.debug, equalTo(true))
+       }
+
+       @Test
+       fun `setting debug flag posts event to event bus`() {
+               val eventBus = mock<EventBus>()
+               val core = createCore(eventBus)
+               core.setDebug()
+               verify(eventBus).post(argThat(instanceOf(DebugActivatedEvent::class.java)))
+       }
+
+       private fun createCore(eventBus: EventBus = mock()): Core {
+               val configuration = mock<Configuration>()
+               val freenetInterface = mock<FreenetInterface>()
+               val identityManager = mock<IdentityManager>()
+               val soneDownloader = mock<SoneDownloader>()
+               val imageInserter = mock<ImageInserter>()
+               val updateChecker = mock<UpdateChecker>()
+               val webOfTrustUpdater = mock<WebOfTrustUpdater>()
+               val database = mock<Database>()
+               val metricRegistry = MetricRegistry()
+               val soneUriCreator = SoneUriCreator()
+               return Core(configuration, freenetInterface, identityManager, soneDownloader, imageInserter, updateChecker, webOfTrustUpdater, eventBus, database, metricRegistry, soneUriCreator)
+       }
+
+}
index 776d0c9..c66e6ff 100644 (file)
@@ -4,15 +4,12 @@ import com.google.common.base.Ticker
 import com.google.common.io.ByteStreams
 import freenet.keys.FreenetURI
 import net.pterodactylus.sone.core.FreenetInterface.BackgroundFetchCallback
-import net.pterodactylus.sone.test.capture
-import net.pterodactylus.sone.test.mock
+import net.pterodactylus.sone.test.*
 import org.hamcrest.MatcherAssert.assertThat
-import org.hamcrest.Matchers.`is`
 import org.hamcrest.Matchers.equalTo
 import org.junit.Test
 import org.mockito.ArgumentMatchers.any
 import org.mockito.ArgumentMatchers.eq
-import org.mockito.Mockito.`when`
 import org.mockito.Mockito.times
 import org.mockito.Mockito.verify
 import java.io.ByteArrayOutputStream
@@ -23,16 +20,6 @@ import java.util.concurrent.TimeUnit
  */
 class DefaultElementLoaderTest {
 
-       companion object {
-               private const val IMAGE_ID = "KSK@gpl.png"
-               private val freenetURI = FreenetURI(IMAGE_ID)
-               private const val decomposedKey = "CHK@DCiVgTWW9nnWHJc9EVwtFJ6jAfBSVyy~rgiPvhUKbS4,mNY85V0x7dYcv7SnEYo1PCC6y2wNWMDNt-y9UWQx9fI,AAMC--8/fru%CC%88hstu%CC%88ck.jpg"
-               private const val normalizedKey = "CHK@DCiVgTWW9nnWHJc9EVwtFJ6jAfBSVyy~rgiPvhUKbS4,mNY85V0x7dYcv7SnEYo1PCC6y2wNWMDNt-y9UWQx9fI,AAMC--8/frühstück.jpg"
-               private const val textKey = "KSK@gpl.html"
-               private val sizeOkay = 2097152L
-               private val sizeNotOkay = sizeOkay + 1
-       }
-
        private val freenetInterface = mock<FreenetInterface>()
        private val ticker = mock<Ticker>()
        private val elementLoader = DefaultElementLoader(freenetInterface, ticker)
@@ -53,49 +40,49 @@ class DefaultElementLoaderTest {
 
        @Test
        fun `element loader returns loading element on first call`() {
-               assertThat(elementLoader.loadElement(IMAGE_ID).loading, `is`(true))
+               assertThat(elementLoader.loadElement(IMAGE_ID).loading, equalTo(true))
        }
 
        @Test
        fun `element loader does not cancel on image mime type with 2 mib size`() {
                elementLoader.loadElement(IMAGE_ID)
                verify(freenetInterface).startFetch(eq(freenetURI), callback.capture())
-               assertThat(callback.value.shouldCancel(freenetURI, "image/png", sizeOkay), `is`(false))
+               assertThat(callback.value.shouldCancel(freenetURI, "image/png", sizeOkay), equalTo(false))
        }
 
        @Test
        fun `element loader does cancel on image mime type with more than 2 mib size`() {
                elementLoader.loadElement(IMAGE_ID)
                verify(freenetInterface).startFetch(eq(freenetURI), callback.capture())
-               assertThat(callback.value.shouldCancel(freenetURI, "image/png", sizeNotOkay), `is`(true))
+               assertThat(callback.value.shouldCancel(freenetURI, "image/png", sizeNotOkay), equalTo(true))
        }
 
        @Test
        fun `element loader does cancel on audio mime type`() {
                elementLoader.loadElement(IMAGE_ID)
                verify(freenetInterface).startFetch(eq(freenetURI), callback.capture())
-               assertThat(callback.value.shouldCancel(freenetURI, "audio/mpeg", sizeOkay), `is`(true))
+               assertThat(callback.value.shouldCancel(freenetURI, "audio/mpeg", sizeOkay), equalTo(true))
        }
 
        @Test
        fun `element loader does cancel on video mime type`() {
                elementLoader.loadElement(IMAGE_ID)
                verify(freenetInterface).startFetch(eq(freenetURI), callback.capture())
-               assertThat(callback.value.shouldCancel(freenetURI, "video/mkv", sizeOkay), `is`(true))
+               assertThat(callback.value.shouldCancel(freenetURI, "video/mkv", sizeOkay), equalTo(true))
        }
 
        @Test
        fun `element loader does cancel on text mime type`() {
                elementLoader.loadElement(IMAGE_ID)
                verify(freenetInterface).startFetch(eq(freenetURI), callback.capture())
-               assertThat(callback.value.shouldCancel(freenetURI, "text/plain", sizeOkay), `is`(true))
+               assertThat(callback.value.shouldCancel(freenetURI, "text/plain", sizeOkay), equalTo(true))
        }
 
        @Test
        fun `element loader does not cancel on text html mime type`() {
                elementLoader.loadElement(IMAGE_ID)
                verify(freenetInterface).startFetch(eq(freenetURI), callback.capture())
-               assertThat(callback.value.shouldCancel(freenetURI, "text/html", sizeOkay), `is`(false))
+               assertThat(callback.value.shouldCancel(freenetURI, "text/html", sizeOkay), equalTo(false))
        }
 
        @Test
@@ -104,14 +91,14 @@ class DefaultElementLoaderTest {
                verify(freenetInterface).startFetch(eq(FreenetURI(decomposedKey)), callback.capture())
                callback.value.loaded(FreenetURI(normalizedKey), "image/png", read("/static/images/unknown-image-0.png"))
                val linkedElement = elementLoader.loadElement(decomposedKey)
-               assertThat(linkedElement, `is`(LinkedElement(normalizedKey, properties = mapOf(
+               assertThat(linkedElement, equalTo(LinkedElement(normalizedKey, properties = mapOf(
                                "type" to "image", "size" to 2451, "sizeHuman" to "2 KiB"
                ))))
        }
 
        @Test
        fun `element loader can extract description from description header`() {
-           elementLoader.loadElement(textKey)
+               elementLoader.loadElement(textKey)
                verify(freenetInterface).startFetch(eq(FreenetURI(textKey)), callback.capture())
                callback.value.loaded(FreenetURI(textKey), "text/html; charset=UTF-8", read("element-loader.html"))
                val linkedElement = elementLoader.loadElement(textKey)
@@ -126,7 +113,7 @@ class DefaultElementLoaderTest {
 
        @Test
        fun `element loader can extract description from first non-heading paragraph`() {
-           elementLoader.loadElement(textKey)
+               elementLoader.loadElement(textKey)
                verify(freenetInterface).startFetch(eq(FreenetURI(textKey)), callback.capture())
                callback.value.loaded(FreenetURI(textKey), "text/html; charset=UTF-8", read("element-loader2.html"))
                val linkedElement = elementLoader.loadElement(textKey)
@@ -141,7 +128,7 @@ class DefaultElementLoaderTest {
 
        @Test
        fun `element loader can not extract description if html is more complicated`() {
-           elementLoader.loadElement(textKey)
+               elementLoader.loadElement(textKey)
                verify(freenetInterface).startFetch(eq(FreenetURI(textKey)), callback.capture())
                callback.value.loaded(FreenetURI(textKey), "text/html; charset=UTF-8", read("element-loader3.html"))
                val linkedElement = elementLoader.loadElement(textKey)
@@ -156,7 +143,7 @@ class DefaultElementLoaderTest {
 
        @Test
        fun `element loader can not extract title if it is missing`() {
-           elementLoader.loadElement(textKey)
+               elementLoader.loadElement(textKey)
                verify(freenetInterface).startFetch(eq(FreenetURI(textKey)), callback.capture())
                callback.value.loaded(FreenetURI(textKey), "text/html; charset=UTF-8", read("element-loader4.html"))
                val linkedElement = elementLoader.loadElement(textKey)
@@ -174,7 +161,7 @@ class DefaultElementLoaderTest {
                elementLoader.loadElement(IMAGE_ID)
                verify(freenetInterface).startFetch(eq(freenetURI), callback.capture())
                callback.value.failed(freenetURI)
-               assertThat(elementLoader.loadElement(IMAGE_ID).failed, `is`(true))
+               assertThat(elementLoader.loadElement(IMAGE_ID).failed, equalTo(true))
                verify(freenetInterface).startFetch(eq(freenetURI), callback.capture())
        }
 
@@ -183,10 +170,10 @@ class DefaultElementLoaderTest {
                elementLoader.loadElement(IMAGE_ID)
                verify(freenetInterface).startFetch(eq(freenetURI), callback.capture())
                callback.value.failed(freenetURI)
-               `when`(ticker.read()).thenReturn(TimeUnit.MINUTES.toNanos(31))
+               whenever(ticker.read()).thenReturn(TimeUnit.MINUTES.toNanos(31))
                val linkedElement = elementLoader.loadElement(IMAGE_ID)
-               assertThat(linkedElement.failed, `is`(false))
-               assertThat(linkedElement.loading, `is`(true))
+               assertThat(linkedElement.failed, equalTo(false))
+               assertThat(linkedElement.loading, equalTo(true))
                verify(freenetInterface, times(2)).startFetch(eq(freenetURI), callback.capture())
        }
 
@@ -199,3 +186,11 @@ class DefaultElementLoaderTest {
                        } ?: ByteArray(0)
 
 }
+
+private const val IMAGE_ID = "KSK@gpl.png"
+private val freenetURI = FreenetURI(IMAGE_ID)
+private const val decomposedKey = "CHK@DCiVgTWW9nnWHJc9EVwtFJ6jAfBSVyy~rgiPvhUKbS4,mNY85V0x7dYcv7SnEYo1PCC6y2wNWMDNt-y9UWQx9fI,AAMC--8/fru%CC%88hstu%CC%88ck.jpg"
+private const val normalizedKey = "CHK@DCiVgTWW9nnWHJc9EVwtFJ6jAfBSVyy~rgiPvhUKbS4,mNY85V0x7dYcv7SnEYo1PCC6y2wNWMDNt-y9UWQx9fI,AAMC--8/frühstück.jpg"
+private const val textKey = "KSK@gpl.html"
+private const val sizeOkay = 2097152L
+private const val sizeNotOkay = sizeOkay + 1
diff --git a/src/test/kotlin/net/pterodactylus/sone/core/FreenetInterfaceTest.kt b/src/test/kotlin/net/pterodactylus/sone/core/FreenetInterfaceTest.kt
new file mode 100644 (file)
index 0000000..9cbaff0
--- /dev/null
@@ -0,0 +1,475 @@
+package net.pterodactylus.sone.core
+
+import com.google.common.eventbus.*
+import freenet.client.*
+import freenet.client.FetchException.FetchExceptionMode.*
+import freenet.client.InsertException.*
+import freenet.client.async.*
+import freenet.crypt.*
+import freenet.keys.*
+import freenet.keys.InsertableClientSSK.*
+import freenet.node.*
+import freenet.node.RequestStarter.*
+import freenet.support.api.*
+import freenet.support.io.*
+import net.pterodactylus.sone.core.FreenetInterface.*
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.sone.data.*
+import net.pterodactylus.sone.data.impl.*
+import net.pterodactylus.sone.freenet.wot.DefaultIdentity
+import net.pterodactylus.sone.test.*
+import net.pterodactylus.sone.test.Matchers.*
+import net.pterodactylus.sone.test.TestUtil.*
+import net.pterodactylus.sone.utils.*
+import org.hamcrest.MatcherAssert.*
+import org.hamcrest.Matchers.equalTo
+import org.hamcrest.Matchers.notNullValue
+import org.hamcrest.Matchers.nullValue
+import org.junit.*
+import org.junit.rules.*
+import org.mockito.*
+import org.mockito.ArgumentCaptor.*
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mockito.*
+import java.io.*
+import java.util.*
+import kotlin.test.Test
+
+/**
+ * Unit test for [FreenetInterface].
+ */
+class FreenetInterfaceTest {
+
+       @Rule
+       @JvmField
+       val expectionException: ExpectedException = ExpectedException.none()
+
+       @Rule
+       @JvmField
+       val silencedLogging = silencedLogging()
+
+       @Suppress("UnstableApiUsage")
+       private val eventBus = mock<EventBus>()
+       private val node = mock<Node>()
+       private val nodeClientCore = mock<NodeClientCore>()
+       private val highLevelSimpleClient: HighLevelSimpleClient = mock(HighLevelSimpleClient::class.java, withSettings().extraInterfaces(RequestClient::class.java))
+       private val randomSource = DummyRandomSource()
+       private val uskManager = mock<USKManager>()
+       private val sone = mock<Sone>()
+       private val callbackCaptor: ArgumentCaptor<USKCallback> = forClass(USKCallback::class.java)
+       private val image: Image = ImageImpl()
+       private val insertToken: InsertToken
+       private val bucket = mock<Bucket>()
+       private val clientGetCallback: ArgumentCaptor<ClientGetCallback> = forClass(ClientGetCallback::class.java)
+       private val uri = FreenetURI("KSK@pgl.png")
+       private val fetchResult = mock<FetchResult>()
+       private val backgroundFetchCallback = mock<BackgroundFetchCallback>()
+       private val clientGetter = mock<ClientGetter>()
+       private val soneUriCreator = SoneUriCreator()
+       private val freenetInterface: FreenetInterface
+
+       init {
+               whenever(nodeClientCore.makeClient(anyShort(), anyBoolean(), anyBoolean())).thenReturn(highLevelSimpleClient)
+               setField(node, "clientCore", nodeClientCore)
+               setField(node, "random", randomSource)
+               setField(nodeClientCore, "uskManager", uskManager)
+               setField(nodeClientCore, "clientContext", mock<ClientContext>())
+               freenetInterface = FreenetInterface(eventBus, node, soneUriCreator)
+               insertToken = freenetInterface.InsertToken(image)
+               insertToken.setBucket(bucket)
+       }
+
+       @Before
+       fun setupHighLevelSimpleClient() {
+               whenever(highLevelSimpleClient.fetchContext).thenReturn(mock())
+               whenever(highLevelSimpleClient.fetch(eq(uri), anyLong(), any(ClientGetCallback::class.java), any(FetchContext::class.java), anyShort())).thenReturn(clientGetter)
+       }
+
+       @Before
+       fun setupSone() {
+               val insertSsk = createRandom(randomSource, "test-0")
+               whenever(sone.id).thenReturn(insertSsk.uri.routingKey.asFreenetBase64)
+               whenever(sone.identity).thenReturn(DefaultIdentity("id", "name", insertSsk.uri.toString()))
+       }
+
+       @Before
+       fun setupCallbackCaptorAndUskManager() {
+               doNothing().whenever(uskManager).subscribe(any(USK::class.java), callbackCaptor.capture(), anyBoolean(), any(RequestClient::class.java))
+       }
+
+       @Test
+       fun `can fetch uri`() {
+               val freenetUri = FreenetURI("KSK@GPLv3.txt")
+               val fetchResult = createFetchResult()
+               whenever(highLevelSimpleClient.fetch(freenetUri)).thenReturn(fetchResult)
+               val fetched = freenetInterface.fetchUri(freenetUri)
+               assertThat(fetched, notNullValue())
+               assertThat(fetched!!.fetchResult, equalTo(fetchResult))
+               assertThat(fetched.freenetUri, equalTo(freenetUri))
+       }
+
+       @Test
+       fun `fetch follows redirect`() {
+               val freenetUri = FreenetURI("KSK@GPLv2.txt")
+               val newFreenetUri = FreenetURI("KSK@GPLv3.txt")
+               val fetchResult = createFetchResult()
+               val fetchException = FetchException(PERMANENT_REDIRECT, newFreenetUri)
+               whenever(highLevelSimpleClient.fetch(freenetUri)).thenThrow(fetchException)
+               whenever(highLevelSimpleClient.fetch(newFreenetUri)).thenReturn(fetchResult)
+               val fetched = freenetInterface.fetchUri(freenetUri)
+               assertThat(fetched!!.fetchResult, equalTo(fetchResult))
+               assertThat(fetched.freenetUri, equalTo(newFreenetUri))
+       }
+
+       @Test
+       fun `fetch returns null on fetch exceptions`() {
+               val freenetUri = FreenetURI("KSK@GPLv2.txt")
+               val fetchException = FetchException(ALL_DATA_NOT_FOUND)
+               whenever(highLevelSimpleClient.fetch(freenetUri)).thenThrow(fetchException)
+               val fetched = freenetInterface.fetchUri(freenetUri)
+               assertThat(fetched, nullValue())
+       }
+
+       private fun createFetchResult(): FetchResult {
+               val clientMetadata = ClientMetadata("text/plain")
+               val bucket = ArrayBucket("Some Data.".toByteArray())
+               return FetchResult(clientMetadata, bucket)
+       }
+
+       @Test
+       fun `inserting an image`() {
+               val temporaryImage = TemporaryImage("image-id")
+               temporaryImage.mimeType = "image/png"
+               val imageData = byteArrayOf(1, 2, 3, 4)
+               temporaryImage.imageData = imageData
+               val image = ImageImpl("image-id")
+               val insertToken = freenetInterface.InsertToken(image)
+               val insertContext = mock<InsertContext>()
+               whenever(highLevelSimpleClient.getInsertContext(anyBoolean())).thenReturn(insertContext)
+               val clientPutter = mock<ClientPutter>()
+               val insertBlockCaptor = forClass(InsertBlock::class.java)
+               whenever(highLevelSimpleClient.insert(insertBlockCaptor.capture(), eq(null as String?), eq(false), eq(insertContext), eq(insertToken), anyShort())).thenReturn(clientPutter)
+               freenetInterface.insertImage(temporaryImage, image, insertToken)
+               assertThat(insertBlockCaptor.value.data.inputStream, delivers(byteArrayOf(1, 2, 3, 4)))
+               assertThat(getPrivateField(insertToken, "clientPutter"), equalTo(clientPutter))
+               verify(eventBus).post(any(ImageInsertStartedEvent::class.java))
+       }
+
+       @Test
+       fun `insert exception causes a sone exception`() {
+               val temporaryImage = TemporaryImage("image-id")
+               temporaryImage.mimeType = "image/png"
+               val imageData = byteArrayOf(1, 2, 3, 4)
+               temporaryImage.imageData = imageData
+               val image = ImageImpl("image-id")
+               val insertToken = freenetInterface.InsertToken(image)
+               val insertContext = mock<InsertContext>()
+               whenever(highLevelSimpleClient.getInsertContext(anyBoolean())).thenReturn(insertContext)
+               val insertBlockCaptor = forClass(InsertBlock::class.java)
+               whenever(highLevelSimpleClient.insert(insertBlockCaptor.capture(), eq(null as String?), eq(false), eq(insertContext), eq(insertToken), anyShort())).thenThrow(InsertException::class.java)
+               expectionException.expect(SoneInsertException::class.java)
+               freenetInterface.insertImage(temporaryImage, image, insertToken)
+       }
+
+       @Test
+       fun `inserting a directory`() {
+               val freenetUri = mock<FreenetURI>()
+               val manifestEntries = HashMap<String, Any>()
+               val defaultFile = "index.html"
+               val resultingUri = mock<FreenetURI>()
+               whenever(highLevelSimpleClient.insertManifest(eq(freenetUri), eq(manifestEntries), eq(defaultFile))).thenReturn(resultingUri)
+               assertThat(freenetInterface.insertDirectory(freenetUri, manifestEntries, defaultFile), equalTo(resultingUri))
+       }
+
+       @Test
+       fun `insert exception is forwarded as sone exception`() {
+               whenever(highLevelSimpleClient.insertManifest(any(), any(), any())).thenThrow(InsertException::class.java)
+               expectionException.expect(SoneException::class.java)
+               freenetInterface.insertDirectory(null, null, null)
+       }
+
+       @Test
+       fun `sone with wrong request uri will not be subscribed`() {
+               freenetInterface.registerUsk(FreenetURI("KSK@GPLv3.txt"), null)
+               verify(uskManager, never()).subscribe(any(USK::class.java), any(USKCallback::class.java), anyBoolean(), any(RequestClient::class.java))
+       }
+
+       @Test
+       fun `registering a usk`() {
+               val freenetUri = createRandom(randomSource, "test-0").uri.uskForSSK()
+               val callback = mock<Callback>()
+               freenetInterface.registerUsk(freenetUri, callback)
+               verify(uskManager).subscribe(any(USK::class.java), any(USKCallback::class.java), anyBoolean(), any(RequestClient::class.java))
+       }
+
+       @Test
+       fun `registering a non-usk key will not be subscribed`() {
+               val freenetUri = FreenetURI("KSK@GPLv3.txt")
+               val callback = mock<Callback>()
+               freenetInterface.registerUsk(freenetUri, callback)
+               verify(uskManager, never()).subscribe(any(USK::class.java), any(USKCallback::class.java), anyBoolean(), any(RequestClient::class.java))
+       }
+
+       @Test
+       fun `registering an active usk will subscribe to it correctly`() {
+               val freenetUri = createRandom(randomSource, "test-0").uri.uskForSSK()
+               val uskCallback = mock<USKCallback>()
+               freenetInterface.registerActiveUsk(freenetUri, uskCallback)
+               verify(uskManager).subscribe(any(USK::class.java), eq(uskCallback), eq(true), any(RequestClient::class.java))
+       }
+
+       @Test
+       fun `registering an inactive usk will subscribe to it correctly`() {
+               val freenetUri = createRandom(randomSource, "test-0").uri.uskForSSK()
+               val uskCallback = mock<USKCallback>()
+               freenetInterface.registerPassiveUsk(freenetUri, uskCallback)
+               verify(uskManager).subscribe(any(USK::class.java), eq(uskCallback), eq(false), any(RequestClient::class.java))
+       }
+
+       @Test
+       fun `registering an active non-usk will not subscribe to a usk`() {
+               val freenetUri = createRandom(randomSource, "test-0").uri
+               freenetInterface.registerActiveUsk(freenetUri, null)
+               verify(uskManager, never()).subscribe(any(USK::class.java), any(USKCallback::class.java), anyBoolean(), any(RequestClient::class.java))
+       }
+
+       @Test
+       fun `registering an inactive non-usk will not subscribe to a usk`() {
+               val freenetUri = createRandom(randomSource, "test-0").uri
+               freenetInterface.registerPassiveUsk(freenetUri, null)
+               verify(uskManager, never()).subscribe(any(USK::class.java), any(USKCallback::class.java), anyBoolean(), any(RequestClient::class.java))
+       }
+
+       @Test
+       fun `unregistering a not registered usk does nothing`() {
+               val freenetURI = createRandom(randomSource, "test-0").uri.uskForSSK()
+               freenetInterface.unregisterUsk(freenetURI)
+               verify(uskManager, never()).unsubscribe(any(USK::class.java), any(USKCallback::class.java))
+       }
+
+       @Test
+       fun `unregistering a registered usk`() {
+               val freenetURI = createRandom(randomSource, "test-0").uri.uskForSSK()
+               val callback = mock<Callback>()
+               freenetInterface.registerUsk(freenetURI, callback)
+               freenetInterface.unregisterUsk(freenetURI)
+               verify(uskManager).unsubscribe(any(USK::class.java), any(USKCallback::class.java))
+       }
+
+       @Test
+       fun `unregistering a not registered sone does nothing`() {
+               freenetInterface.unregisterUsk(sone)
+               verify(uskManager, never()).unsubscribe(any(USK::class.java), any(USKCallback::class.java))
+       }
+
+       @Test
+       fun `unregistering 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 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))
+       }
+
+       @Test
+       fun `callback for normal usk uses different priorities`() {
+               val callback = mock<Callback>()
+               val soneUri = createRandom(randomSource, "test-0").uri.uskForSSK()
+               freenetInterface.registerUsk(soneUri, callback)
+               assertThat(callbackCaptor.value.pollingPriorityNormal, equalTo(PREFETCH_PRIORITY_CLASS))
+               assertThat(callbackCaptor.value.pollingPriorityProgress, equalTo(INTERACTIVE_PRIORITY_CLASS))
+       }
+
+       @Test
+       fun `callback for normal usk forwards important parameters`() {
+               val callback = mock<Callback>()
+               val uri = createRandom(randomSource, "test-0").uri.uskForSSK()
+               freenetInterface.registerUsk(uri, callback)
+               val key = mock<USK>()
+               whenever(key.uri).thenReturn(uri)
+               callbackCaptor.value.onFoundEdition(3, key, null, false, 0.toShort(), null, true, true)
+               verify(callback).editionFound(eq(uri), eq(3L), eq(true), eq(true))
+       }
+
+       @Test
+       fun `fetched retains uri and fetch result`() {
+               val freenetUri = mock<FreenetURI>()
+               val fetchResult = mock<FetchResult>()
+               val (freenetUri1, fetchResult1) = Fetched(freenetUri, fetchResult)
+               assertThat(freenetUri1, equalTo(freenetUri))
+               assertThat(fetchResult1, equalTo(fetchResult))
+       }
+
+       @Test
+       fun `cancelling an insert will fire image insert aborted event`() {
+               val clientPutter = mock<ClientPutter>()
+               insertToken.setClientPutter(clientPutter)
+               val imageInsertStartedEvent = forClass(ImageInsertStartedEvent::class.java)
+               verify(eventBus).post(imageInsertStartedEvent.capture())
+               assertThat(imageInsertStartedEvent.value.image, equalTo(image))
+               insertToken.cancel()
+               val imageInsertAbortedEvent = forClass(ImageInsertAbortedEvent::class.java)
+               verify(eventBus, times(2)).post(imageInsertAbortedEvent.capture())
+               verify(bucket).free()
+               assertThat(imageInsertAbortedEvent.value.image, equalTo(image))
+       }
+
+       @Test
+       fun `failure without exception sends failed event`() {
+               val insertException = InsertException(mock<InsertException>())
+               insertToken.onFailure(insertException, null)
+               val imageInsertFailedEvent = forClass(ImageInsertFailedEvent::class.java)
+               verify(eventBus).post(imageInsertFailedEvent.capture())
+               verify(bucket).free()
+               assertThat(imageInsertFailedEvent.value.image, equalTo(image))
+               assertThat(imageInsertFailedEvent.value.cause, equalTo<Throwable>(insertException))
+       }
+
+       @Test
+       fun `failure sends failed event with exception`() {
+               val insertException = InsertException(InsertExceptionMode.INTERNAL_ERROR, "Internal error", null)
+               insertToken.onFailure(insertException, null)
+               val imageInsertFailedEvent = forClass(ImageInsertFailedEvent::class.java)
+               verify(eventBus).post(imageInsertFailedEvent.capture())
+               verify(bucket).free()
+               assertThat(imageInsertFailedEvent.value.image, equalTo(image))
+               assertThat(imageInsertFailedEvent.value.cause, equalTo(insertException as Throwable))
+       }
+
+       @Test
+       fun `failure because cancelled by user sends aborted event`() {
+               val insertException = InsertException(InsertExceptionMode.CANCELLED, null)
+               insertToken.onFailure(insertException, null)
+               val imageInsertAbortedEvent = forClass(ImageInsertAbortedEvent::class.java)
+               verify(eventBus).post(imageInsertAbortedEvent.capture())
+               verify(bucket).free()
+               assertThat(imageInsertAbortedEvent.value.image, equalTo(image))
+       }
+
+       @Test
+       fun `ignored methods do not throw exceptions`() {
+               insertToken.onResume(null)
+               insertToken.onFetchable(null)
+               insertToken.onGeneratedMetadata(null, null)
+       }
+
+       @Test
+       fun `generated uri is posted on success`() {
+               val generatedUri = mock<FreenetURI>()
+               insertToken.onGeneratedURI(generatedUri, null)
+               insertToken.onSuccess(null)
+               val imageInsertFinishedEvent = forClass(ImageInsertFinishedEvent::class.java)
+               verify(eventBus).post(imageInsertFinishedEvent.capture())
+               verify(bucket).free()
+               assertThat(imageInsertFinishedEvent.value.image, equalTo(image))
+               assertThat(imageInsertFinishedEvent.value.resultingUri, equalTo(generatedUri))
+       }
+
+       @Test
+       fun `insert token supplier supplies insert tokens`() {
+               val insertTokenSupplier = InsertTokenSupplier(freenetInterface)
+               assertThat(insertTokenSupplier.apply(image), notNullValue())
+       }
+
+       @Test
+       fun `background fetch can be started`() {
+               freenetInterface.startFetch(uri, backgroundFetchCallback)
+               verify(highLevelSimpleClient).fetch(eq(uri), anyLong(), any(ClientGetCallback::class.java), any(FetchContext::class.java), anyShort())
+       }
+
+       @Test
+       fun `background fetch registers snoop and restarts the request`() {
+               freenetInterface.startFetch(uri, backgroundFetchCallback)
+               verify(clientGetter).metaSnoop = any(SnoopMetadata::class.java)
+               verify(clientGetter).restart(eq(uri), anyBoolean(), any(ClientContext::class.java))
+       }
+
+       @Test
+       fun `request is not cancelled for image mime type`() {
+               verifySnoopCancelsRequestForMimeType("image/png", false)
+               verify(backgroundFetchCallback, never()).failed(uri)
+       }
+
+       @Test
+       fun `request is cancelled for null mime type`() {
+               verifySnoopCancelsRequestForMimeType(null, true)
+               verify(backgroundFetchCallback, never()).shouldCancel(eq(uri), any(), anyLong())
+               verify(backgroundFetchCallback).failed(uri)
+       }
+
+       @Test
+       fun `request is cancelled for video mime type`() {
+               verifySnoopCancelsRequestForMimeType("video/mkv", true)
+               verify(backgroundFetchCallback).failed(uri)
+       }
+
+       @Test
+       fun `request is cancelled for audio mime type`() {
+               verifySnoopCancelsRequestForMimeType("audio/mpeg", true)
+               verify(backgroundFetchCallback).failed(uri)
+       }
+
+       @Test
+       fun `request is cancelled for text mime type`() {
+               verifySnoopCancelsRequestForMimeType("text/plain", true)
+               verify(backgroundFetchCallback).failed(uri)
+       }
+
+       private fun verifySnoopCancelsRequestForMimeType(mimeType: String?, cancel: Boolean) {
+               whenever(backgroundFetchCallback.shouldCancel(eq(uri), if (mimeType != null) eq(mimeType) else isNull(), anyLong())).thenReturn(cancel)
+               freenetInterface.startFetch(uri, backgroundFetchCallback)
+               val snoopMetadata = forClass(SnoopMetadata::class.java)
+               verify(clientGetter).metaSnoop = snoopMetadata.capture()
+               val metadata = mock<Metadata>()
+               whenever(metadata.mimeType).thenReturn(mimeType)
+               assertThat(snoopMetadata.value.snoopMetadata(metadata, mock()), equalTo(cancel))
+       }
+
+       @Test
+       fun `callback of background fetch is notified on success`() {
+               freenetInterface.startFetch(uri, backgroundFetchCallback)
+               verify(highLevelSimpleClient).fetch(eq(uri), anyLong(), clientGetCallback.capture(), any(FetchContext::class.java), anyShort())
+               whenever(fetchResult.mimeType).thenReturn("image/png")
+               whenever(fetchResult.asByteArray()).thenReturn(byteArrayOf(1, 2, 3, 4, 5))
+               clientGetCallback.value.onSuccess(fetchResult, mock())
+               verify(backgroundFetchCallback).loaded(uri, "image/png", byteArrayOf(1, 2, 3, 4, 5))
+               verifyNoMoreInteractions(backgroundFetchCallback)
+       }
+
+       @Test
+       fun `callback of background fetch is notified on failure`() {
+               freenetInterface.startFetch(uri, backgroundFetchCallback)
+               verify(highLevelSimpleClient).fetch(eq(uri), anyLong(), clientGetCallback.capture(), any(FetchContext::class.java), anyShort())
+               whenever(fetchResult.mimeType).thenReturn("image/png")
+               whenever(fetchResult.asByteArray()).thenReturn(byteArrayOf(1, 2, 3, 4, 5))
+               clientGetCallback.value.onFailure(FetchException(ALL_DATA_NOT_FOUND), mock())
+               verify(backgroundFetchCallback).failed(uri)
+               verifyNoMoreInteractions(backgroundFetchCallback)
+       }
+
+       @Test
+       fun `callback of background fetch is notified as failure if bucket can not be loaded`() {
+               freenetInterface.startFetch(uri, backgroundFetchCallback)
+               verify(highLevelSimpleClient).fetch(eq(uri), anyLong(), clientGetCallback.capture(), any(FetchContext::class.java), anyShort())
+               whenever(fetchResult.mimeType).thenReturn("image/png")
+               whenever(fetchResult.asByteArray()).thenThrow(IOException::class.java)
+               clientGetCallback.value.onSuccess(fetchResult, mock())
+               verify(backgroundFetchCallback).failed(uri)
+               verifyNoMoreInteractions(backgroundFetchCallback)
+       }
+
+       @Test
+       fun `unregistering a registered USK with different edition unregisters USK`() {
+               val callback = mock<Callback>()
+               val uri = createRandom(randomSource, "test-123").uri.uskForSSK()
+               freenetInterface.registerUsk(uri, callback)
+               freenetInterface.unregisterUsk(uri.setSuggestedEdition(234))
+               verify(uskManager).unsubscribe(any<USK>(), any<USKCallback>())
+       }
+
+}
index 39a11dc..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) }
@@ -37,7 +37,7 @@ class ImageInserterTest {
 
        @Test
        fun `exception when inserting image is ignored`() {
-               doThrow(SoneException::class.java).`when`(freenetInterface).insertImage(eq(temporaryImage), eq(image), any(InsertToken::class.java))
+               doThrow(SoneException::class.java).whenever(freenetInterface).insertImage(eq(temporaryImage), eq(image), any(InsertToken::class.java))
                imageInserter.insertImage(temporaryImage, image)
                verify(freenetInterface).insertImage(eq(temporaryImage), eq(image), any(InsertToken::class.java))
        }
diff --git a/src/test/kotlin/net/pterodactylus/sone/core/PreferencesLoaderTest.kt b/src/test/kotlin/net/pterodactylus/sone/core/PreferencesLoaderTest.kt
new file mode 100644 (file)
index 0000000..cb8df38
--- /dev/null
@@ -0,0 +1,63 @@
+package net.pterodactylus.sone.core
+
+import com.google.common.eventbus.*
+import net.pterodactylus.sone.fcp.FcpInterface.*
+import net.pterodactylus.util.config.*
+import org.hamcrest.MatcherAssert.*
+import org.hamcrest.Matchers.*
+import org.junit.*
+
+/**
+ * Unit test for [PreferencesLoader].
+ */
+class PreferencesLoaderTest {
+
+       @Suppress("UnstableApiUsage")
+       private val eventBus = EventBus()
+       private val preferences = Preferences(eventBus)
+       private val configuration = Configuration(MapConfigurationBackend())
+       private val preferencesLoader = PreferencesLoader(preferences)
+
+       @Before
+       fun setupConfiguration() {
+               setupIntValue("InsertionDelay", 15)
+               setupIntValue("PostsPerPage", 25)
+               setupIntValue("ImagesPerPage", 12)
+               setupIntValue("CharactersPerPost", 150)
+               setupIntValue("PostCutOffLength", 300)
+               setupBooleanValue("RequireFullAccess", true)
+               setupBooleanValue("ActivateFcpInterface", true)
+               setupIntValue("FcpFullAccessRequired", 1)
+               setupBooleanValue("StrictFiltering", true)
+       }
+
+       private fun setupIntValue(optionName: String, value: Int) {
+               configuration.getIntValue("Option/$optionName").value = value
+       }
+
+       private fun setupBooleanValue(optionName: String, value: Boolean) {
+               configuration.getBooleanValue("Option/$optionName").value = value
+       }
+
+       @Test
+       fun `configuration is loaded correctly`() {
+               preferencesLoader.loadFrom(configuration)
+               assertThat(preferences.insertionDelay, equalTo(15))
+               assertThat(preferences.postsPerPage, equalTo(25))
+               assertThat(preferences.imagesPerPage, equalTo(12))
+               assertThat(preferences.charactersPerPost, equalTo(150))
+               assertThat(preferences.postCutOffLength, equalTo(300))
+               assertThat(preferences.requireFullAccess, equalTo(true))
+               assertThat(preferences.fcpInterfaceActive, equalTo(true))
+               assertThat(preferences.fcpFullAccessRequired, equalTo(FullAccessRequired.WRITING))
+               assertThat(preferences.strictFiltering, equalTo(true))
+       }
+
+       @Test
+       fun `configuration is loaded correctly with cut off length minus one`() {
+               setupIntValue("PostCutOffLength", -1)
+               preferencesLoader.loadFrom(configuration)
+               assertThat(preferences.postCutOffLength, not(equalTo(-1)))
+       }
+
+}
index 199604b..dafe782 100644 (file)
@@ -1,27 +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 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`() {
@@ -31,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)
@@ -43,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
@@ -64,6 +83,11 @@ class PreferencesTest {
        }
 
        @Test
+       fun `preferences saves null for default insertion delay setting`() {
+               verifySavedOption(nullValue()) { it.getIntValue("Option/InsertionDelay").getValue(null) }
+       }
+
+       @Test
        fun `preferences retain posts per page`() {
                preferences.newPostsPerPage = 15
                assertThat(preferences.postsPerPage, equalTo(15))
@@ -180,149 +204,168 @@ class PreferencesTest {
        }
 
        @Test
-       fun `preferences retain positive trust`() {
-               preferences.newPositiveTrust = 15
-               assertThat(preferences.positiveTrust, equalTo(15))
-       }
-
-       @Test(expected = IllegalArgumentException::class)
-       fun `invalid positive trust is rejected`() {
-               preferences.newPositiveTrust = -15
+       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))
+               assertThat(events, hasItem<FcpInterfaceActivatedEvent>(instanceOf(FcpInterfaceActivatedEvent::class.java)))
        }
 
        @Test
-       fun `preferences return default value when positive trust is set to null`() {
-               preferences.newPositiveTrust = null
-               assertThat(preferences.positiveTrust, equalTo(75))
+       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))
+               assertThat(events, hasItem<FcpInterfaceDeactivatedEvent>(instanceOf(FcpInterfaceDeactivatedEvent::class.java)))
        }
 
        @Test
-       fun `preferences start with positive trust default value`() {
-               assertThat(preferences.positiveTrust, equalTo(75))
+       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))
+               assertThat(events, hasItem<FcpInterfaceDeactivatedEvent>(instanceOf(FcpInterfaceDeactivatedEvent::class.java)))
        }
 
        @Test
-       fun `preferences retain negative trust`() {
-               preferences.newNegativeTrust = -15
-               assertThat(preferences.negativeTrust, equalTo(-15))
-       }
-
-       @Test(expected = IllegalArgumentException::class)
-       fun `invalid negative trust is rejected`() {
-               preferences.newNegativeTrust = 150
+       fun `preferences start with fcp interface active default value`() {
+               assertThat(preferences.fcpInterfaceActive, equalTo(false))
        }
 
        @Test
-       fun `preferences return default value when negative trust is set to null`() {
-               preferences.newNegativeTrust = null
-               assertThat(preferences.negativeTrust, equalTo(-25))
+       fun `preferences retain fcp full access required of no`() {
+               verifyFullAccessRequiredChangedEvent(NO)
        }
 
-       @Test
-       fun `preferences start with negative trust default value`() {
-               assertThat(preferences.negativeTrust, equalTo(-25))
+       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 trust comment`() {
-               preferences.newTrustComment = "Trust"
-               assertThat(preferences.trustComment, equalTo("Trust"))
+       fun `preferences retain fcp full access required of writing`() {
+               verifyFullAccessRequiredChangedEvent(WRITING)
        }
 
        @Test
-       fun `preferences return default value when trust comment is set to null`() {
-               preferences.newTrustComment = null
-               assertThat(preferences.trustComment,
-                               equalTo("Set from Sone Web Interface"))
+       fun `preferences retain fcp full access required of always`() {
+               verifyFullAccessRequiredChangedEvent(ALWAYS)
        }
 
        @Test
-       fun `preferences start with trust comment default value`() {
-               assertThat(preferences.trustComment,
-                               equalTo("Set from Sone Web Interface"))
+       fun `preferences return default value when fcp full access required is set to null`() {
+               verifyFullAccessRequiredChangedEvent(null, ALWAYS)
        }
 
        @Test
-       fun `preferences retain fcp interface active of true`() {
-               preferences.newFcpInterfaceActive = true
-               assertThat(preferences.fcpInterfaceActive, equalTo(true))
-               verify(eventBus).post(any(FcpInterfaceActivatedEvent::class.java))
+       fun `preferences start with fcp full access required default value`() {
+               assertThat(preferences.fcpFullAccessRequired, equalTo(ALWAYS))
        }
 
        @Test
-       fun `preferences retain fcp interface active of false`() {
-               preferences.newFcpInterfaceActive = false
-               assertThat(preferences.fcpInterfaceActive, equalTo(false))
-               verify(eventBus).post(any(FcpInterfaceDeactivatedEvent::class.java))
+       fun `setting insertion delay to valid value sends change event`() {
+               testPreferencesChangedEvent("InsertionDelay", { preferences.newInsertionDelay = it }, 30)
        }
 
        @Test
-       fun `preferences return default value when fcp interface active is set to null`() {
-               preferences.newFcpInterfaceActive = null
-               assertThat(preferences.fcpInterfaceActive, equalTo(false))
-               verify(eventBus).post(any(FcpInterfaceDeactivatedEvent::class.java))
+       fun `setting posts per page to valid value sends change event`() {
+               testPreferencesChangedEvent("PostsPerPage", { preferences.newPostsPerPage = it }, 31)
        }
 
        @Test
-       fun `preferences start with fcp interface active default value`() {
-               assertThat(preferences.fcpInterfaceActive, equalTo(false))
+       fun `default strict filtering is false`() {
+               assertThat(preferences.strictFiltering, equalTo(false))
        }
 
        @Test
-       fun `preferences retain fcp full access required of no`() {
-               preferences.newFcpFullAccessRequired = NO
-               assertThat(preferences.fcpFullAccessRequired, equalTo(NO))
-               verifyFullAccessRequiredChangedEvent(NO)
+       fun `strict filtering can be set`() {
+               preferences.newStrictFiltering = true
+               assertThat(preferences.strictFiltering, equalTo(true))
        }
 
-       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))
+       @Test
+       fun `strict filtering returns to default on null`() {
+               preferences.newStrictFiltering = true
+               preferences.newStrictFiltering = null
+               assertThat(preferences.strictFiltering, equalTo(false))
        }
 
        @Test
-       fun `preferences retain fcp full access required of writing`() {
-               preferences.newFcpFullAccessRequired = WRITING
-               assertThat(preferences.fcpFullAccessRequired, equalTo(WRITING))
-               verifyFullAccessRequiredChangedEvent(WRITING)
+       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 `preferences retain fcp full access required of always`() {
-               preferences.newFcpFullAccessRequired = ALWAYS
-               assertThat(preferences.fcpFullAccessRequired, equalTo(ALWAYS))
-               verifyFullAccessRequiredChangedEvent(ALWAYS)
+       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 `preferences return default value when fcp full access required is set to null`() {
-               preferences.newFcpFullAccessRequired = null
-               assertThat(preferences.fcpFullAccessRequired, equalTo(ALWAYS))
-               verifyFullAccessRequiredChangedEvent(ALWAYS)
+       fun `default strict filtering is saved as null`() {
+               verifySavedOption(nullValue()) { it.getBooleanValue("Option/StrictFiltering").value }
        }
 
        @Test
-       fun `preferences start with fcp full access required default value`() {
-               assertThat(preferences.fcpFullAccessRequired, equalTo(ALWAYS))
+       fun `activated strict filtering is saved as true`() {
+               preferences.newStrictFiltering = true
+               verifySavedOption(equalTo(true)) { it.getBooleanValue("Option/StrictFiltering").value }
        }
 
        @Test
-       fun `setting insertion delay to valid value sends change event`() {
-               testPreferencesChangedEvent("InsertionDelay", { preferences.newInsertionDelay = it }, 30)
+       fun `deactivated strict filtering is saved as false`() {
+               preferences.newStrictFiltering = false
+               verifySavedOption(equalTo(false)) { it.getBooleanValue("Option/StrictFiltering").value }
        }
 
-       @Test
-       fun `setting posts per page to valid value sends change event`() {
-               testPreferencesChangedEvent("PostsPerPage", { preferences.newPostsPerPage = it }, 31)
+       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)))
        }
 
 }
diff --git a/src/test/kotlin/net/pterodactylus/sone/core/SoneInserterTest.kt b/src/test/kotlin/net/pterodactylus/sone/core/SoneInserterTest.kt
new file mode 100644 (file)
index 0000000..3da9e88
--- /dev/null
@@ -0,0 +1,279 @@
+package net.pterodactylus.sone.core
+
+import com.codahale.metrics.*
+import com.google.common.base.*
+import com.google.common.base.Optional
+import com.google.common.eventbus.*
+import com.google.common.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.*
+import org.hamcrest.Matchers.*
+import org.junit.*
+import org.mockito.*
+import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.anyString
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mockito.doAnswer
+import org.mockito.Mockito.never
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.hamcrest.MockitoHamcrest.*
+import org.mockito.stubbing.*
+import java.lang.System.*
+import java.util.*
+import kotlin.test.Test
+
+/**
+ * Unit test for [SoneInserter] and its subclasses.
+ */
+class SoneInserterTest {
+
+       private val metricRegistry = MetricRegistry()
+       private val core = mock<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() {
+               val updateChecker = mock<UpdateChecker>()
+               whenever(core.updateChecker).thenReturn(updateChecker)
+               whenever(core.getSone(anyString())).thenReturn(null)
+       }
+
+       @Test
+       fun `insertion delay is forwarded to sone inserter`() {
+               val eventBus = AsyncEventBus(directExecutor())
+               eventBus.register(SoneInserter(core, eventBus, freenetInterface, metricRegistry, 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.identity).thenReturn(ownIdentity)
+               whenever(sone.fingerprint).thenReturn(fingerprint)
+               whenever(sone.rootAlbum).thenReturn(mock())
+               whenever(core.getSone(anyString())).thenReturn(sone)
+               return sone
+       }
+
+       @Test
+       fun `isModified is true if modification detector says so`() {
+               val soneModificationDetector = mock<SoneModificationDetector>()
+               whenever(soneModificationDetector.isModified).thenReturn(true)
+               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, soneUriCreator, "SoneId", soneModificationDetector, 1)
+               assertThat(soneInserter.isModified, equalTo(false))
+       }
+
+       @Test
+       fun `last fingerprint is stored correctly`() {
+               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, soneUriCreator, "SoneId")
+               soneInserter.stop()
+               soneInserter.serviceRun()
+       }
+
+       @Test
+       fun `sone inserter inserts a sone if it is eligible`() {
+               val finalUri = mock<FreenetURI>()
+               val sone = createSone(insertUri)
+               val soneModificationDetector = mock<SoneModificationDetector>()
+               whenever(soneModificationDetector.isEligibleForInsert).thenReturn(true)
+               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(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))
+               assertThat(soneEvents.allValues[1], instanceOf(SoneInsertedEvent::class.java))
+               assertThat(soneEvents.allValues[1].sone, equalTo(sone))
+       }
+
+       @Test
+       fun `sone inserter bails out if it is stopped while inserting`() {
+               val finalUri = mock<FreenetURI>()
+               val sone = createSone(insertUri)
+               val soneModificationDetector = mock<SoneModificationDetector>()
+               whenever(soneModificationDetector.isEligibleForInsert).thenReturn(true)
+               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(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))
+               assertThat(soneEvents.allValues[1], instanceOf(SoneInsertedEvent::class.java))
+               assertThat(soneEvents.allValues[1].sone, equalTo(sone))
+               verify(core, never()).touchConfiguration()
+       }
+
+       @Test
+       fun `sone inserter does not insert sone if it is not eligible`() {
+               createSone(insertUri)
+               val soneModificationDetector = mock<SoneModificationDetector>()
+               val soneInserter = SoneInserter(core, eventBus, freenetInterface, metricRegistry, soneUriCreator, "SoneId", soneModificationDetector, 1)
+               Thread(Runnable {
+                       try {
+                               Thread.sleep(500)
+                       } catch (ie1: InterruptedException) {
+                               throw RuntimeException(ie1)
+                       }
+
+                       soneInserter.stop()
+               }).start()
+               soneInserter.serviceRun()
+               verify(freenetInterface, never()).insertDirectory(eq(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 sone = createSone(insertUri)
+               val soneModificationDetector = mock<SoneModificationDetector>()
+               whenever(soneModificationDetector.isEligibleForInsert).thenReturn(true)
+               val soneInserter = SoneInserter(core, eventBus, freenetInterface, metricRegistry, soneUriCreator, "SoneId", soneModificationDetector, 1)
+               val soneException = SoneException(Exception())
+               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(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))
+               assertThat(soneEvents.allValues[1], instanceOf(SoneInsertAbortedEvent::class.java))
+               assertThat(soneEvents.allValues[1].sone, equalTo(sone))
+               verify(core, never()).touchConfiguration()
+       }
+
+       @Test
+       fun `sone inserter exits if sone is unknown`() {
+               val soneModificationDetector = mock<SoneModificationDetector>()
+               val soneInserter = SoneInserter(core, eventBus, freenetInterface, metricRegistry, soneUriCreator, "SoneId", soneModificationDetector, 1)
+               whenever(soneModificationDetector.isEligibleForInsert).thenReturn(true)
+               whenever(core.getSone("SoneId")).thenReturn(null)
+               soneInserter.serviceRun()
+       }
+
+       @Test
+       fun `sone inserter catches exception and continues`() {
+               val soneModificationDetector = mock<SoneModificationDetector>()
+               val soneInserter = SoneInserter(core, eventBus, freenetInterface, metricRegistry, soneUriCreator, "SoneId", soneModificationDetector, 1)
+               val stopInserterAndThrowException = Answer<Optional<Sone>> {
+                       soneInserter.stop()
+                       throw NullPointerException()
+               }
+               whenever(soneModificationDetector.isEligibleForInsert).thenAnswer(stopInserterAndThrowException)
+               soneInserter.serviceRun()
+       }
+
+       @Test
+       fun `template is rendered correctly for manifest element`() {
+               val soneProperties = HashMap<String, Any>()
+               soneProperties["id"] = "SoneId"
+               val manifestCreator = ManifestCreator(core, soneProperties)
+               val now = currentTimeMillis()
+               whenever(core.startupTime).thenReturn(now)
+               val manifestElement = manifestCreator.createManifestElement("test.txt", "plain/text; charset=utf-8", "sone-inserter-manifest.txt")
+               assertThat(manifestElement!!.name, equalTo("test.txt"))
+               assertThat(manifestElement.mimeTypeOverride, equalTo("plain/text; charset=utf-8"))
+               val templateContent = String(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 a null manifest element`() {
+               val soneProperties = HashMap<String, Any>()
+               val manifestCreator = ManifestCreator(core, soneProperties)
+               assertThat(manifestCreator.createManifestElement("test.txt",
+                               "plain/text; charset=utf-8",
+                               "sone-inserter-invalid-manifest.txt"),
+                               nullValue())
+       }
+
+       @Test
+       fun `error while rendering template returns a null manifest element`() {
+               val soneProperties = HashMap<String, Any>()
+               val manifestCreator = ManifestCreator(core, soneProperties)
+               whenever(core.toString()).thenThrow(NullPointerException::class.java)
+               assertThat(manifestCreator.createManifestElement("test.txt",
+                               "plain/text; charset=utf-8",
+                               "sone-inserter-faulty-manifest.txt"),
+                               nullValue())
+       }
+
+       @Test
+       fun `successful insert updates metrics`() {
+               val finalUri = mock<FreenetURI>()
+               createSone(insertUri)
+               val soneModificationDetector = mock<SoneModificationDetector>()
+               whenever(soneModificationDetector.isEligibleForInsert).thenReturn(true)
+               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 histogram = metricRegistry.histogram("sone.insert.duration")
+               assertThat(histogram.count, equalTo(1L))
+       }
+
+       @Test
+       fun `unsuccessful insert does not update histogram but records error`() {
+               createSone(insertUri)
+               val soneModificationDetector = mock<SoneModificationDetector>()
+               whenever(soneModificationDetector.isEligibleForInsert).thenReturn(true)
+               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())
+               }
+               soneInserter.serviceRun()
+               val histogram = metricRegistry.histogram("sone.insert.duration")
+               assertThat(histogram.count, equalTo(0L))
+               val meter = metricRegistry.meter("sone.insert.errors")
+               assertThat(meter.count, equalTo(1L))
+       }
+
+}
+
+val insertUri = createInsertUri
+val expectedInsertUri = createInsertUri
diff --git a/src/test/kotlin/net/pterodactylus/sone/core/SoneParserTest.kt b/src/test/kotlin/net/pterodactylus/sone/core/SoneParserTest.kt
new file mode 100644 (file)
index 0000000..38419a2
--- /dev/null
@@ -0,0 +1,418 @@
+package net.pterodactylus.sone.core
+
+import com.codahale.metrics.*
+import com.google.common.base.Optional.*
+import freenet.crypt.*
+import freenet.keys.InsertableClientSSK.*
+import net.pterodactylus.sone.data.*
+import net.pterodactylus.sone.data.impl.AlbumImpl
+import net.pterodactylus.sone.database.memory.*
+import net.pterodactylus.sone.freenet.wot.*
+import net.pterodactylus.sone.test.*
+import net.pterodactylus.util.config.*
+import org.hamcrest.MatcherAssert.*
+import org.hamcrest.Matchers.*
+import org.mockito.Mockito.*
+import java.lang.System.*
+import java.util.concurrent.TimeUnit.*
+import kotlin.test.*
+
+/**
+ * Unit test for [SoneParser].
+ */
+class SoneParserTest {
+
+       private val database = MemoryDatabase(Configuration(MapConfigurationBackend()))
+       private val metricRegistry = MetricRegistry()
+       private val soneParser = SoneParser(database, metricRegistry)
+       private val sone = mock<Sone>()
+
+       @BeforeTest
+       fun setupSone() {
+               setupSone(this.sone, Identity::class.java)
+               database.storeSone(sone)
+       }
+
+       private fun setupSone(sone: Sone, identityClass: Class<out Identity>) {
+               val identity = mock(identityClass)
+               val clientSSK = createRandom(DummyRandomSource(), "WoT")
+               whenever(identity.requestUri).thenReturn(clientSSK.uri.toString())
+               whenever(identity.id).thenReturn("identity")
+               whenever(sone.id).thenReturn("identity")
+               whenever(sone.identity).thenReturn(identity)
+               whenever(sone.requestUri).thenAnswer { clientSSK.uri.setKeyType("USK").setDocName("Sone") }
+               whenever(sone.time).thenReturn(currentTimeMillis() - DAYS.toMillis(1))
+               whenever(sone.rootAlbum).thenReturn(AlbumImpl(sone))
+       }
+
+       @Test
+       fun `parsing a sone fails when document is not xml`() {
+               val inputStream = javaClass.getResourceAsStream("sone-parser-not-xml.xml")
+               assertThat(soneParser.parseSone(sone, inputStream), nullValue())
+       }
+
+       @Test
+       fun `parsing a sone fails when document has negative protocol version`() {
+               val inputStream = javaClass.getResourceAsStream("sone-parser-negative-protocol-version.xml")
+               assertThat(soneParser.parseSone(sone, inputStream), nullValue())
+       }
+
+       @Test
+       fun `parsing a sone fails when protocol version is too large`() {
+               val inputStream = javaClass.getResourceAsStream("sone-parser-too-large-protocol-version.xml")
+               assertThat(soneParser.parseSone(sone, inputStream), nullValue())
+       }
+
+       @Test
+       fun `parsing a sone fails when there is no time`() {
+               val inputStream = javaClass.getResourceAsStream("sone-parser-no-time.xml")
+               assertThat(soneParser.parseSone(sone, inputStream), nullValue())
+       }
+
+       @Test
+       fun `parsing a sone fails when time is not numeric`() {
+               val inputStream = javaClass.getResourceAsStream("sone-parser-time-not-numeric.xml")
+               assertThat(soneParser.parseSone(sone, inputStream), nullValue())
+       }
+
+       @Test
+       fun `parsing a sone fails when profile is missing`() {
+               val inputStream = javaClass.getResourceAsStream("sone-parser-no-profile.xml")
+               assertThat(soneParser.parseSone(sone, inputStream), nullValue())
+       }
+
+       @Test
+       fun `parsing a sone fails when profile field is missing afield name`() {
+               val inputStream = javaClass.getResourceAsStream("sone-parser-profile-missing-field-name.xml")
+               assertThat(soneParser.parseSone(sone, inputStream), nullValue())
+       }
+
+       @Test
+       fun `parsing a sone fails when profile field name is empty`() {
+               val inputStream = javaClass.getResourceAsStream("sone-parser-profile-empty-field-name.xml")
+               assertThat(soneParser.parseSone(sone, inputStream), nullValue())
+       }
+
+       @Test
+       fun `parsing a sone fails when profile field name is not unique`() {
+               val inputStream = javaClass.getResourceAsStream("sone-parser-profile-duplicate-field-name.xml")
+               assertThat(soneParser.parseSone(sone, inputStream), nullValue())
+       }
+
+       @Test
+       fun `parsing a sone succeeds without payload`() {
+               val inputStream = javaClass.getResourceAsStream("sone-parser-no-payload.xml")
+               assertThat(soneParser.parseSone(sone, inputStream)!!.time, equalTo(1407197508000L))
+       }
+
+       @Test
+       fun `parsing a local sone succeeds without payload`() {
+               val inputStream = javaClass.getResourceAsStream("sone-parser-no-payload.xml")
+               val localSone = mock<Sone>()
+               setupSone(localSone, OwnIdentity::class.java)
+               whenever(localSone.isLocal).thenReturn(true)
+               val parsedSone = soneParser.parseSone(localSone, inputStream)
+               assertThat(parsedSone!!.time, equalTo(1407197508000L))
+               assertThat(parsedSone.isLocal, equalTo(true))
+       }
+
+       @Test
+       fun `parsing a sone succeeds without protocol version`() {
+               val inputStream = javaClass.getResourceAsStream("sone-parser-missing-protocol-version.xml")
+               assertThat(soneParser.parseSone(sone, inputStream), notNullValue())
+       }
+
+       @Test
+       fun `parsing a sone fails with missing client name`() {
+               val inputStream = javaClass.getResourceAsStream("sone-parser-missing-client-name.xml")
+               assertThat(soneParser.parseSone(sone, inputStream), nullValue())
+       }
+
+       @Test
+       fun `parsing a sone fails with missing client version`() {
+               val inputStream = javaClass.getResourceAsStream("sone-parser-missing-client-version.xml")
+               assertThat(soneParser.parseSone(sone, inputStream), nullValue())
+       }
+
+       @Test
+       fun `parsing a sone succeeds with client info`() {
+               val inputStream = javaClass.getResourceAsStream("sone-parser-with-client-info.xml")
+               assertThat(soneParser.parseSone(sone, inputStream)!!.client, equalTo(Client("some-client", "some-version")))
+       }
+
+       @Test
+       fun `parsing a sone succeeds with profile`() {
+               val inputStream = javaClass.getResourceAsStream("sone-parser-with-profile.xml")
+               val profile = soneParser.parseSone(sone, inputStream)!!.profile
+               assertThat(profile.firstName, equalTo("first"))
+               assertThat(profile.middleName, equalTo("middle"))
+               assertThat(profile.lastName, equalTo("last"))
+               assertThat(profile.birthDay, equalTo(18))
+               assertThat(profile.birthMonth, equalTo(12))
+               assertThat(profile.birthYear, equalTo(1976))
+       }
+
+       @Test
+       fun `parsing a sone succeeds without profile fields`() {
+               val inputStream = javaClass.getResourceAsStream("sone-parser-without-fields.xml")
+               assertThat(soneParser.parseSone(sone, inputStream), notNullValue())
+       }
+
+       @Test
+       fun `parsing a sone fails without post id`() {
+               val inputStream = javaClass.getResourceAsStream("sone-parser-without-post-id.xml")
+               assertThat(soneParser.parseSone(sone, inputStream), nullValue())
+       }
+
+       @Test
+       fun `parsing a sone fails without post time`() {
+               val inputStream = javaClass.getResourceAsStream("sone-parser-without-post-time.xml")
+               assertThat(soneParser.parseSone(sone, inputStream), nullValue())
+       }
+
+       @Test
+       fun `parsing a sone fails without post text`() {
+               val inputStream = javaClass.getResourceAsStream("sone-parser-without-post-text.xml")
+               assertThat(soneParser.parseSone(sone, inputStream), nullValue())
+       }
+
+       @Test
+       fun `parsing a sone fails with invalid post time`() {
+               val inputStream = javaClass.getResourceAsStream("sone-parser-with-invalid-post-time.xml")
+               assertThat(soneParser.parseSone(sone, inputStream), nullValue())
+       }
+
+       @Test
+       fun `parsing a sone succeeds with valid post time`() {
+               val inputStream = javaClass.getResourceAsStream("sone-parser-with-valid-post-time.xml")
+               val posts = soneParser.parseSone(sone, inputStream)!!.posts
+               assertThat(posts, hasSize(1))
+               assertThat(posts[0].sone.id, equalTo(sone.id))
+               assertThat(posts[0].id, equalTo("3de12680-afef-11e9-a124-e713cf8912fe"))
+               assertThat(posts[0].time, equalTo(1407197508000L))
+               assertThat(posts[0].recipientId, equalTo(absent()))
+               assertThat(posts[0].text, equalTo("text"))
+       }
+
+       @Test
+       fun `parsing a sone succeeds with recipient`() {
+               val inputStream = javaClass.getResourceAsStream("sone-parser-with-recipient.xml")
+               val posts = soneParser.parseSone(sone, inputStream)!!.posts
+               assertThat(posts, hasSize(1))
+               assertThat(posts[0].sone.id, equalTo(sone.id))
+               assertThat(posts[0].id, equalTo("3de12680-afef-11e9-a124-e713cf8912fe"))
+               assertThat(posts[0].time, equalTo(1407197508000L))
+               assertThat(posts[0].recipientId, equalTo(of("1234567890123456789012345678901234567890123")))
+               assertThat(posts[0].text, equalTo("text"))
+       }
+
+       @Test
+       fun `parsing a sone succeeds with invalid recipient`() {
+               val inputStream = javaClass.getResourceAsStream("sone-parser-with-invalid-recipient.xml")
+               val posts = soneParser.parseSone(sone, inputStream)!!.posts
+               assertThat(posts, hasSize(1))
+               assertThat(posts[0].sone.id, equalTo(sone.id))
+               assertThat(posts[0].id, equalTo("3de12680-afef-11e9-a124-e713cf8912fe"))
+               assertThat(posts[0].time, equalTo(1407197508000L))
+               assertThat(posts[0].recipientId, equalTo(absent()))
+               assertThat(posts[0].text, equalTo("text"))
+       }
+
+       @Test
+       fun `parsing a sone fails without post reply id`() {
+               val inputStream = javaClass.getResourceAsStream("sone-parser-without-post-reply-id.xml")
+               assertThat(soneParser.parseSone(sone, inputStream), nullValue())
+       }
+
+       @Test
+       fun `parsing a sone fails without post reply post id`() {
+               val inputStream = javaClass.getResourceAsStream("sone-parser-without-post-reply-post-id.xml")
+               assertThat(soneParser.parseSone(sone, inputStream), nullValue())
+       }
+
+       @Test
+       fun `parsing a sone fails without post reply time`() {
+               val inputStream = javaClass.getResourceAsStream("sone-parser-without-post-reply-time.xml")
+               assertThat(soneParser.parseSone(sone, inputStream), nullValue())
+       }
+
+       @Test
+       fun `parsing a sone fails without post reply text`() {
+               val inputStream = javaClass.getResourceAsStream("sone-parser-without-post-reply-text.xml")
+               assertThat(soneParser.parseSone(sone, inputStream), nullValue())
+       }
+
+       @Test
+       fun `parsing a sone fails with invalid post reply time`() {
+               val inputStream = javaClass.getResourceAsStream("sone-parser-with-invalid-post-reply-time.xml")
+               assertThat(soneParser.parseSone(sone, inputStream), nullValue())
+       }
+
+       @Test
+       fun `parsing a sone succeeds with valid post reply time`() {
+               val inputStream = javaClass.getResourceAsStream("sone-parser-with-valid-post-reply-time.xml")
+               val postReplies = soneParser.parseSone(sone, inputStream)!!.replies
+               assertThat(postReplies, hasSize(1))
+               val postReply = postReplies.first()
+               assertThat(postReply.id, equalTo("5ccba7f4-aff0-11e9-b176-a7b9db60ce98"))
+               assertThat(postReply.postId, equalTo("3de12680-afef-11e9-a124-e713cf8912fe"))
+               assertThat(postReply.sone.id, equalTo("identity"))
+               assertThat(postReply.time, equalTo(1407197508000L))
+               assertThat(postReply.text, equalTo("reply-text"))
+       }
+
+       @Test
+       fun `parsing a sone succeeds without liked post ids`() {
+               val inputStream = javaClass.getResourceAsStream("sone-parser-without-liked-post-ids.xml")
+               assertThat(soneParser.parseSone(sone, inputStream), notNullValue())
+       }
+
+       @Test
+       fun `parsing a sone succeeds with liked post ids`() {
+               val inputStream = javaClass.getResourceAsStream("sone-parser-with-liked-post-ids.xml")
+               assertThat(soneParser.parseSone(sone, inputStream)!!.likedPostIds, equalTo(setOf("liked-post-id")))
+       }
+
+       @Test
+       fun `parsing a sone succeeds without liked post reply ids`() {
+               val inputStream = javaClass.getResourceAsStream("sone-parser-without-liked-post-reply-ids.xml")
+               assertThat(soneParser.parseSone(sone, inputStream), notNullValue())
+       }
+
+       @Test
+       fun `parsing a sone succeeds with liked post reply ids`() {
+               val inputStream = javaClass.getResourceAsStream("sone-parser-with-liked-post-reply-ids.xml")
+               assertThat(soneParser.parseSone(sone, inputStream)!!.likedReplyIds, equalTo(setOf("liked-post-reply-id")))
+       }
+
+       @Test
+       fun `parsing a sone succeeds without albums`() {
+               val inputStream = javaClass.getResourceAsStream("sone-parser-without-albums.xml")
+               assertThat(soneParser.parseSone(sone, inputStream), notNullValue())
+       }
+
+       @Test
+       fun `parsing a sone fails without album id`() {
+               val inputStream = javaClass.getResourceAsStream("sone-parser-without-album-id.xml")
+               assertThat(soneParser.parseSone(sone, inputStream), nullValue())
+       }
+
+       @Test
+       fun `parsing a sone fails without album title`() {
+               val inputStream = javaClass.getResourceAsStream("sone-parser-without-album-title.xml")
+               assertThat(soneParser.parseSone(sone, inputStream), nullValue())
+       }
+
+       @Test
+       fun `parsing a sone succeeds with nested albums`() {
+               val inputStream = javaClass.getResourceAsStream("sone-parser-with-multiple-albums.xml")
+               val parsedSone = soneParser.parseSone(sone, inputStream)
+               assertThat(parsedSone, notNullValue())
+               assertThat(parsedSone!!.rootAlbum.albums, hasSize(1))
+               val album = parsedSone.rootAlbum.albums[0]
+               assertThat(album.id, equalTo("album-id-1"))
+               assertThat(album.title, equalTo("album-title"))
+               assertThat(album.description, equalTo("album-description"))
+               assertThat(album.albums, hasSize(1))
+               val nestedAlbum = album.albums[0]
+               assertThat(nestedAlbum.id, equalTo("album-id-2"))
+               assertThat(nestedAlbum.title, equalTo("album-title-2"))
+               assertThat(nestedAlbum.description, equalTo("album-description-2"))
+               assertThat(nestedAlbum.albums, hasSize(0))
+       }
+
+       @Test
+       fun `parsing a sone fails with invalid parent album id`() {
+               val inputStream = javaClass.getResourceAsStream("sone-parser-with-invalid-parent-album-id.xml")
+               assertThat(soneParser.parseSone(sone, inputStream), nullValue())
+       }
+
+       @Test
+       fun `parsing a sone succeeds without images`() {
+               val inputStream = javaClass.getResourceAsStream("sone-parser-without-images.xml")
+               assertThat(soneParser.parseSone(sone, inputStream), notNullValue())
+       }
+
+       @Test
+       fun `parsing a sone fails without image id`() {
+               val inputStream = javaClass.getResourceAsStream("sone-parser-without-image-id.xml")
+               assertThat(soneParser.parseSone(sone, inputStream), nullValue())
+       }
+
+       @Test
+       fun `parsing a sone fails without image time`() {
+               val inputStream = javaClass.getResourceAsStream("sone-parser-without-image-time.xml")
+               assertThat(soneParser.parseSone(sone, inputStream), nullValue())
+       }
+
+       @Test
+       fun `parsing a sone fails without image key`() {
+               val inputStream = javaClass.getResourceAsStream("sone-parser-without-image-key.xml")
+               assertThat(soneParser.parseSone(sone, inputStream), nullValue())
+       }
+
+       @Test
+       fun `parsing a sone fails without image title`() {
+               val inputStream = javaClass.getResourceAsStream("sone-parser-without-image-title.xml")
+               assertThat(soneParser.parseSone(sone, inputStream), nullValue())
+       }
+
+       @Test
+       fun `parsing a sone fails without image width`() {
+               val inputStream = javaClass.getResourceAsStream("sone-parser-without-image-width.xml")
+               assertThat(soneParser.parseSone(sone, inputStream), nullValue())
+       }
+
+       @Test
+       fun `parsing a sone fails without image height`() {
+               val inputStream = javaClass.getResourceAsStream("sone-parser-without-image-height.xml")
+               assertThat(soneParser.parseSone(sone, inputStream), nullValue())
+       }
+
+       @Test
+       fun `parsing a sone fails with invalid image width`() {
+               val inputStream = javaClass.getResourceAsStream("sone-parser-with-invalid-image-width.xml")
+               assertThat(soneParser.parseSone(sone, inputStream), nullValue())
+       }
+
+       @Test
+       fun `parsing a sone fails with invalid image height`() {
+               val inputStream = javaClass.getResourceAsStream("sone-parser-with-invalid-image-height.xml")
+               assertThat(soneParser.parseSone(sone, inputStream), nullValue())
+       }
+
+       @Test
+       fun `parsing a sone succeeds with image`() {
+               val inputStream = javaClass.getResourceAsStream("sone-parser-with-image.xml")
+               val sone = soneParser.parseSone(this.sone, inputStream)
+               assertThat(sone, notNullValue())
+               assertThat(sone!!.rootAlbum.albums, hasSize(1))
+               assertThat(sone.rootAlbum.albums[0].images, hasSize(1))
+               val image = sone.rootAlbum.albums[0].images[0]
+               assertThat(image.id, equalTo("image-id"))
+               assertThat(image.creationTime, equalTo(1407197508000L))
+               assertThat(image.key, equalTo("KSK@GPLv3.txt"))
+               assertThat(image.title, equalTo("image-title"))
+               assertThat(image.description, equalTo("image-description"))
+               assertThat(image.width, equalTo(1920))
+               assertThat(image.height, equalTo(1080))
+               assertThat(sone.profile.avatar, equalTo("image-id"))
+       }
+
+       @Test
+       fun `unsuccessful parsing does not add a histogram entry`() {
+               val inputStream = javaClass.getResourceAsStream("sone-parser-with-invalid-image-height.xml")
+               assertThat(soneParser.parseSone(sone, inputStream), nullValue())
+               val histogram = metricRegistry.histogram("sone.parse.duration")
+               assertThat(histogram.count, equalTo(0L))
+       }
+
+       @Test
+       fun `successful parsing adds histogram entry`() {
+               val inputStream = javaClass.getResourceAsStream("sone-parser-without-images.xml")
+               assertThat(soneParser.parseSone(sone, inputStream), notNullValue())
+               val histogram = metricRegistry.histogram("sone.parse.duration")
+               assertThat(histogram.count, equalTo(1L))
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/core/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
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/core/UpdateCheckerTest.kt b/src/test/kotlin/net/pterodactylus/sone/core/UpdateCheckerTest.kt
new file mode 100644 (file)
index 0000000..3bc8a9e
--- /dev/null
@@ -0,0 +1,237 @@
+package net.pterodactylus.sone.core
+
+import com.google.common.eventbus.*
+import freenet.client.*
+import freenet.keys.*
+import freenet.support.io.*
+import net.pterodactylus.sone.core.FreenetInterface.*
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.sone.main.*
+import net.pterodactylus.sone.test.*
+import net.pterodactylus.util.version.Version
+import org.hamcrest.MatcherAssert.*
+import org.hamcrest.Matchers.equalTo
+import org.hamcrest.Matchers.instanceOf
+import org.junit.*
+import org.mockito.ArgumentCaptor.*
+import org.mockito.ArgumentMatchers.any
+import org.mockito.Mockito.never
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.hamcrest.MockitoHamcrest.*
+import java.io.*
+import kotlin.Long.Companion.MAX_VALUE
+
+/**
+ * Unit test for [UpdateChecker].
+ */
+class UpdateCheckerTest {
+
+       private val eventBus = mock<EventBus>()
+       private val freenetInterface = mock<FreenetInterface>()
+       private val currentVersion = Version(1, 0, 0)
+       private val pluginHomepage = PluginHomepage("KSK@homepage")
+       private val updateChecker = UpdateChecker(eventBus, freenetInterface, currentVersion, pluginHomepage)
+
+       @Before
+       fun startUpdateChecker() {
+               updateChecker.start()
+       }
+
+       @Test
+       fun `new update checker does not have a latest version`() {
+               assertThat(updateChecker.hasLatestVersion(), equalTo(false))
+               assertThat(updateChecker.latestVersion, equalTo(currentVersion))
+       }
+
+       @Test
+       fun `starting an update checker register a usk`() {
+               verify(freenetInterface).registerUsk(any(FreenetURI::class.java), any(Callback::class.java))
+       }
+
+       @Test
+       fun `stopping an update checker unregisters a usk`() {
+               updateChecker.stop()
+               verify(freenetInterface).unregisterUsk(any(FreenetURI::class.java))
+       }
+
+       @Test
+       fun `callback does not download if new edition is not found`() {
+               setupCallbackWithEdition(MAX_VALUE, false)
+               verify(freenetInterface, never()).fetchUri(any(FreenetURI::class.java))
+               verify(eventBus, never()).post(argThat(instanceOf(UpdateFoundEvent::class.java)))
+       }
+
+       private fun setupCallbackWithEdition(edition: Long, newKnownGood: Boolean, newSlot: Boolean = false) {
+               val uri = forClass(FreenetURI::class.java)
+               val callback = forClass(Callback::class.java)
+               verify(freenetInterface).registerUsk(uri.capture(), callback.capture())
+               callback.value.editionFound(uri.value, edition, newKnownGood, newSlot)
+       }
+
+       @Test
+       fun `callback starts if new edition is found`() {
+               setupFetchResult(createFutureFetchResult())
+               setupCallbackWithEdition(MAX_VALUE, true)
+               verifyAFreenetUriIsFetched()
+               verifyEventIsFired(Version(99, 0, 0), 11865368297000L, false)
+               verifyThatUpdateCheckerKnowsLatestVersion(Version(99, 0, 0), 11865368297000L)
+       }
+
+       private fun createFutureFetchResult(): FetchResult {
+               val clientMetadata = ClientMetadata("application/xml")
+               val fetched = ArrayBucket(("# MapConfigurationBackendVersion=1\n" +
+                               "CurrentVersion/Version: 99.0.0\n" +
+                               "CurrentVersion/ReleaseTime: 11865368297000\n" +
+                               "DisruptiveVersion/0.1.2: true").toByteArray())
+               return FetchResult(clientMetadata, fetched)
+       }
+
+       private fun verifyEventIsFired(version: Version, releaseTime: Long, disruptive: Boolean) {
+               val updateFoundEvent = forClass(UpdateFoundEvent::class.java)
+               verify(eventBus, times(1)).post(updateFoundEvent.capture())
+               assertThat(updateFoundEvent.value.version, equalTo(version))
+               assertThat(updateFoundEvent.value.releaseTime, equalTo(releaseTime))
+               assertThat(updateFoundEvent.value.isDisruptive, equalTo(disruptive))
+       }
+
+       private fun verifyThatUpdateCheckerKnowsLatestVersion(version: Version, releaseTime: Long) {
+               assertThat(updateChecker.latestVersion, equalTo(version))
+               assertThat(updateChecker.latestVersionDate, equalTo(releaseTime))
+               assertThat(updateChecker.hasLatestVersion(), equalTo(true))
+       }
+
+       @Test
+       fun `callback does not start if no new edition is found`() {
+               setupFetchResult(createPastFetchResult())
+               setupCallbackWithEdition(updateChecker.latestEdition, true)
+               verifyAFreenetUriIsFetched()
+               verifyNoUpdateFoundEventIsFired()
+       }
+
+       private fun setupFetchResult(pastFetchResult: FetchResult) {
+               whenever(freenetInterface.fetchUri(any(FreenetURI::class.java))).thenAnswer { invocation ->
+                       val freenetUri = invocation.arguments[0] as FreenetURI
+                       Fetched(freenetUri, pastFetchResult)
+               }
+       }
+
+       private fun createPastFetchResult(): FetchResult {
+               val clientMetadata = ClientMetadata("application/xml")
+               val fetched = ArrayBucket(("# MapConfigurationBackendVersion=1\n" +
+                               "CurrentVersion/Version: 0.2\n" +
+                               "CurrentVersion/ReleaseTime: 1289417883000").toByteArray())
+               return FetchResult(clientMetadata, fetched)
+       }
+
+       @Test
+       fun `invalid update file does not start callback`() {
+               setupFetchResult(createInvalidFetchResult())
+               setupCallbackWithEdition(MAX_VALUE, true)
+               verifyAFreenetUriIsFetched()
+               verifyNoUpdateFoundEventIsFired()
+       }
+
+       private fun createInvalidFetchResult(): FetchResult {
+               val clientMetadata = ClientMetadata("text/plain")
+               val fetched = ArrayBucket("Some other data.".toByteArray())
+               return FetchResult(clientMetadata, fetched)
+       }
+
+       @Test
+       fun `non existing properties will not cause update to be found`() {
+               setupCallbackWithEdition(MAX_VALUE, true)
+               verifyAFreenetUriIsFetched()
+               verifyNoUpdateFoundEventIsFired()
+       }
+
+       private fun verifyNoUpdateFoundEventIsFired() {
+               verify(eventBus, never()).post(any(UpdateFoundEvent::class.java))
+       }
+
+       private fun verifyAFreenetUriIsFetched() {
+               verify(freenetInterface).fetchUri(any(FreenetURI::class.java))
+       }
+
+       @Test
+       fun `broken bucket does not cause update to be found`() {
+               setupFetchResult(createBrokenBucketFetchResult())
+               setupCallbackWithEdition(MAX_VALUE, true)
+               verifyAFreenetUriIsFetched()
+               verifyNoUpdateFoundEventIsFired()
+       }
+
+       private fun createBrokenBucketFetchResult(): FetchResult {
+               val clientMetadata = ClientMetadata("text/plain")
+               val fetched = object : ArrayBucket("Some other data.".toByteArray()) {
+                       override fun getInputStream() =
+                                       whenever(mock<InputStream>().read()).thenThrow(IOException()).getMock<InputStream>()
+               }
+               return FetchResult(clientMetadata, fetched)
+       }
+
+       @Test
+       fun `invalid time does not cause an update to be found`() {
+               setupFetchResult(createInvalidTimeFetchResult())
+               setupCallbackWithEdition(MAX_VALUE, true)
+               verifyAFreenetUriIsFetched()
+               verifyNoUpdateFoundEventIsFired()
+       }
+
+       private fun createInvalidTimeFetchResult(): FetchResult {
+               val clientMetadata = ClientMetadata("application/xml")
+               val fetched = ArrayBucket(("# MapConfigurationBackendVersion=1\n" +
+                               "CurrentVersion/Version: 0.2\n" +
+                               "CurrentVersion/ReleaseTime: invalid").toByteArray())
+               return FetchResult(clientMetadata, fetched)
+       }
+
+       @Test
+       fun `invalid properties does not cause an update to be found`() {
+               setupFetchResult(createMissingTimeFetchResult())
+               setupCallbackWithEdition(MAX_VALUE, true)
+               verifyAFreenetUriIsFetched()
+               verifyNoUpdateFoundEventIsFired()
+       }
+
+       private fun createMissingTimeFetchResult(): FetchResult {
+               val clientMetadata = ClientMetadata("application/xml")
+               val fetched = ArrayBucket(("# MapConfigurationBackendVersion=1\nCurrentVersion/Version: 0.2\n").toByteArray())
+               return FetchResult(clientMetadata, fetched)
+       }
+
+       @Test
+       fun `invalid version does not cause an update to be found`() {
+               setupFetchResult(createInvalidVersionFetchResult())
+               setupCallbackWithEdition(MAX_VALUE, true)
+               verifyAFreenetUriIsFetched()
+               verifyNoUpdateFoundEventIsFired()
+       }
+
+       private fun createInvalidVersionFetchResult(): FetchResult {
+               val clientMetadata = ClientMetadata("application/xml")
+               val fetched = ArrayBucket(("# MapConfigurationBackendVersion=1\n" +
+                               "CurrentVersion/Version: foo\n" +
+                               "CurrentVersion/ReleaseTime: 1289417883000").toByteArray())
+               return FetchResult(clientMetadata, fetched)
+       }
+
+       @Test
+       fun `disruptive version gets notification`() {
+               setupFetchResult(createDisruptiveVersionFetchResult())
+               setupCallbackWithEdition(MAX_VALUE, true)
+               verifyAFreenetUriIsFetched()
+               verifyEventIsFired(Version(1, 2, 3), 1289417883000L, true)
+               verifyThatUpdateCheckerKnowsLatestVersion(Version(1, 2, 3), 1289417883000L)
+       }
+
+       private fun createDisruptiveVersionFetchResult(): FetchResult {
+               val clientMetadata = ClientMetadata("application/xml")
+               val fetched = ArrayBucket(("# MapConfigurationBackendVersion=1\n" +
+                               "CurrentVersion/Version: 1.2.3\n" +
+                               "CurrentVersion/ReleaseTime: 1289417883000\n" +
+                               "DisruptiveVersion/1.2.3: true").toByteArray())
+               return FetchResult(clientMetadata, fetched)
+       }
+
+}
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/ClientTest.kt b/src/test/kotlin/net/pterodactylus/sone/data/ClientTest.kt
new file mode 100644 (file)
index 0000000..bad8ff8
--- /dev/null
@@ -0,0 +1,15 @@
+package net.pterodactylus.sone.data
+
+import org.hamcrest.MatcherAssert.*
+import org.hamcrest.Matchers.*
+import kotlin.test.*
+
+class ClientTest {
+
+       @Test
+       fun `toString() formats client name and version`() {
+               val client = Client("Test Client", "v123")
+               assertThat(client.toString(), equalTo("Test Client v123"))
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/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 1536da4..973047f 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - MemoryDatabaseTest.kt - Copyright © 2013–2019 David Roden
+ * Sone - MemoryDatabaseTest.kt - Copyright © 2013–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -38,7 +38,7 @@ import kotlin.test.*
  */
 class MemoryDatabaseTest {
 
-       private val configuration = mock<Configuration>()
+       private val configuration = deepMock<Configuration>()
        private val memoryDatabase = MemoryDatabase(configuration)
        private val sone = mock<Sone>()
 
@@ -50,15 +50,15 @@ class MemoryDatabaseTest {
        @Test
        fun `stored sone is made available`() {
                storeSone()
-               assertThat(memoryDatabase.getPost("post1"), isPost("post1", 1000L, "post1", absent()))
-               assertThat(memoryDatabase.getPost("post2"), isPost("post2", 2000L, "post2", of(RECIPIENT_ID)))
+               assertThat(memoryDatabase.getPost("post1"), isPost("post1", 1000L, "post1", null))
+               assertThat(memoryDatabase.getPost("post2"), isPost("post2", 2000L, "post2", RECIPIENT_ID))
                assertThat(memoryDatabase.getPost("post3"), nullValue())
                assertThat(memoryDatabase.getPostReply("reply1"), isPostReply("reply1", "post1", 3000L, "reply1"))
                assertThat(memoryDatabase.getPostReply("reply2"), isPostReply("reply2", "post2", 4000L, "reply2"))
                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,11 +405,41 @@ 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))
        }
 
+       @Test
+       @Dirty("the rate limiter should be mocked")
+       fun `saving the database twice in a row only saves it once`() {
+               memoryDatabase.save()
+               memoryDatabase.save()
+               verify(configuration.getStringValue("KnownPosts/0/ID"), times(1)).value = null
+       }
+
+       @Test
+       @Dirty("the rate limiter should be mocked")
+       fun `setting posts as knows twice in a row only saves the database once`() {
+               prepareConfigurationValues()
+               val post = mock<Post>()
+               whenever(post.id).thenReturn("post-id")
+               memoryDatabase.setPostKnown(post, true)
+               memoryDatabase.setPostKnown(post, true)
+               verify(configuration, times(1)).getStringValue("KnownPosts/1/ID")
+       }
+
+       @Test
+       @Dirty("the rate limiter should be mocked")
+       fun `setting post replies as knows twice in a row only saves the database once`() {
+               prepareConfigurationValues()
+               val postReply = mock<PostReply>()
+               whenever(postReply.id).thenReturn("post-reply-id")
+               memoryDatabase.setPostReplyKnown(postReply)
+               memoryDatabase.setPostReplyKnown(postReply)
+               verify(configuration, times(1)).getStringValue("KnownReplies/1/ID")
+       }
+
 }
 
 private const val SONE_ID = "sone"
index 29db41c..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
@@ -56,7 +54,7 @@ class CreatePostCommandTest : SoneCommandTest() {
                parameters += "Text" to "Test"
                whenever(core.getSone("LocalSoneId")).thenReturn(localSone)
                val post = mock<Post>().apply { whenever(id).thenReturn("PostId") }
-               whenever(core.createPost(localSone, absent(), "Test")).thenReturn(post)
+               whenever(core.createPost(localSone, null, "Test")).thenReturn(post)
                val response = command.execute(parameters)
                assertThat(response.replyParameters.get("Message"), equalTo("PostCreated"))
                assertThat(response.replyParameters.get("Post"), equalTo("PostId"))
@@ -90,7 +88,7 @@ class CreatePostCommandTest : SoneCommandTest() {
                whenever(core.getSone("LocalSoneId")).thenReturn(localSone)
                whenever(core.getSone("RemoteSoneId")).thenReturn(remoteSone)
                val post = mock<Post>().apply { whenever(id).thenReturn("PostId") }
-               whenever(core.createPost(localSone, of(remoteSone), "Test")).thenReturn(post)
+               whenever(core.createPost(localSone, remoteSone, "Test")).thenReturn(post)
                val response = command.execute(parameters)
                assertThat(response.replyParameters.get("Message"), equalTo("PostCreated"))
                assertThat(response.replyParameters.get("Post"), equalTo("PostId"))
diff --git a/src/test/kotlin/net/pterodactylus/sone/freenet/AsyncFreenetInterfaceTest.kt b/src/test/kotlin/net/pterodactylus/sone/freenet/AsyncFreenetInterfaceTest.kt
new file mode 100644 (file)
index 0000000..472fd94
--- /dev/null
@@ -0,0 +1,80 @@
+/**
+ * Sone - AsyncFreenetInterfaceTest.kt - Copyright © 2019–2020 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.freenet
+
+import freenet.client.*
+import freenet.keys.*
+import freenet.support.io.*
+import kotlinx.coroutines.*
+import net.pterodactylus.sone.core.*
+import org.hamcrest.MatcherAssert.*
+import org.hamcrest.Matchers.*
+import java.util.concurrent.atomic.*
+import kotlin.test.*
+
+class AsyncFreenetInterfaceTest {
+
+       @Test
+       fun `returned deferred is completed by success`() = runBlocking {
+               val result = FetchResult(ClientMetadata(), NullBucket())
+               val freenetClient = object : FreenetClient {
+                       override fun fetch(freenetKey: FreenetURI) = result
+               }
+               val freenetInterface = AsyncFreenetInterface(freenetClient)
+               val fetched = async { freenetInterface.fetchUri(FreenetURI("KSK@GPL.txt")) }
+
+               withTimeout(1000) {
+                       assertThat(fetched.await(), equalTo(Fetched(FreenetURI("KSK@GPL.txt"), result)))
+               }
+       }
+
+       @Test
+       fun `permanent redircts are being followed`() = runBlocking {
+               val result = FetchResult(ClientMetadata(), NullBucket())
+               val freenetClient = object : FreenetClient {
+                       val redirected = AtomicBoolean(false)
+                       override fun fetch(freenetKey: FreenetURI) =
+                                       if (redirected.compareAndSet(false, true))
+                                               throw FetchException(FetchException.FetchExceptionMode.PERMANENT_REDIRECT, FreenetURI("KSK@GPLv3.txt"))
+                                       else result
+               }
+               val freenetInterface = AsyncFreenetInterface(freenetClient)
+               val fetched = async { freenetInterface.fetchUri(FreenetURI("KSK@GPL.txt")) }
+
+               withTimeout(1000) {
+                       assertThat(fetched.await(), equalTo(Fetched(FreenetURI("KSK@GPLv3.txt"), result)))
+               }
+       }
+
+       @Test
+       fun `fetch errors are being re-thrown`() = runBlocking<Unit> {
+               val freenetClient = object : FreenetClient {
+                       override fun fetch(freenetKey: FreenetURI) =
+                                       throw FetchException(FetchException.FetchExceptionMode.ALL_DATA_NOT_FOUND)
+               }
+               val freenetInterface = AsyncFreenetInterface(freenetClient)
+               val fetched = supervisorScope { async { freenetInterface.fetchUri(FreenetURI("KSK@GPL.txt")) } }
+
+               withTimeout(1000) {
+                       assertFailsWith(FetchException::class) {
+                               fetched.await()
+                       }
+               }
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/freenet/BaseL10nTranslationTest.kt b/src/test/kotlin/net/pterodactylus/sone/freenet/BaseL10nTranslationTest.kt
new file mode 100644 (file)
index 0000000..a188d47
--- /dev/null
@@ -0,0 +1,30 @@
+package net.pterodactylus.sone.freenet
+
+import freenet.l10n.*
+import net.pterodactylus.sone.test.*
+import org.hamcrest.MatcherAssert.*
+import org.hamcrest.Matchers.*
+import org.junit.*
+import java.util.*
+
+/**
+ * Test for [BaseL10nTranslation].
+ */
+class BaseL10nTranslationTest {
+
+       private val baseL10n = mock<BaseL10n>()
+       private val translation = BaseL10nTranslation(baseL10n)
+
+       @Test
+       fun `translate method is facade for the correct method`() {
+               whenever(baseL10n.getString("test")).thenReturn("answer")
+               assertThat(translation.translate("test"), equalTo("answer"))
+       }
+
+       @Test
+       fun `language exposes correct short code`() {
+               whenever(baseL10n.selectedLanguage).thenReturn(BaseL10n.LANGUAGE.ENGLISH)
+               assertThat(translation.currentLocale, equalTo(Locale.ENGLISH))
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/freenet/FreenetClientTest.kt b/src/test/kotlin/net/pterodactylus/sone/freenet/FreenetClientTest.kt
new file mode 100644 (file)
index 0000000..f6d5883
--- /dev/null
@@ -0,0 +1,23 @@
+package net.pterodactylus.sone.freenet
+
+import freenet.client.*
+import freenet.keys.*
+import freenet.support.io.*
+import net.pterodactylus.sone.test.*
+import org.hamcrest.MatcherAssert.*
+import org.hamcrest.Matchers.*
+import kotlin.test.*
+
+class FreenetClientTest {
+
+       private val highLevelSimpleClient = mock<HighLevelSimpleClient>()
+       private val freenetClient = DefaultFreenetClient(highLevelSimpleClient)
+
+       @Test
+       fun `fetch method calls method on hlsc`() {
+               val fetchResult = FetchResult(ClientMetadata(), NullBucket())
+               whenever(highLevelSimpleClient.fetch(FreenetURI("KSK@GPL.txt"))).thenReturn(fetchResult)
+               assertThat(freenetClient.fetch(FreenetURI("KSK@GPL.txt")), equalTo(fetchResult))
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/freenet/FreenetURIsTest.kt b/src/test/kotlin/net/pterodactylus/sone/freenet/FreenetURIsTest.kt
new file mode 100644 (file)
index 0000000..ca1fc6e
--- /dev/null
@@ -0,0 +1,24 @@
+package net.pterodactylus.sone.freenet
+
+import freenet.keys.*
+import org.hamcrest.MatcherAssert.*
+import org.hamcrest.Matchers.*
+import org.junit.*
+
+/**
+ * Unit test for [Key].
+ */
+class FreenetURIsTest {
+
+       private val uri = FreenetURI("SSK@$routingKey,$cryptoKey,$extra/some-site-12/foo/bar.html")
+
+       @Test
+       fun routingKeyIsExtractCorrectly() {
+               assertThat(uri.routingKeyString, equalTo(routingKey))
+       }
+
+}
+
+private const val routingKey = "NfUYvxDwU9vqb2mh-qdT~DYJ6U0XNbxMGGoLe0aCHJs"
+private const val cryptoKey = "Miglsgix0VR56ZiPl4NgjnUd~UdrnHqIvXJ3KKHmxmI"
+private const val extra = "AQACAAE"
index 66b1ab1..358d106 100644 (file)
@@ -1,60 +1,51 @@
 package net.pterodactylus.sone.freenet
 
-import freenet.l10n.BaseL10n
-import freenet.l10n.BaseL10n.LANGUAGE.ENGLISH
-import net.pterodactylus.sone.test.mock
-import net.pterodactylus.sone.test.whenever
-import net.pterodactylus.util.template.TemplateContext
-import org.hamcrest.MatcherAssert.assertThat
-import org.hamcrest.Matchers.equalTo
-import org.junit.Before
-import org.junit.Test
-import org.mockito.ArgumentMatchers.anyString
+import org.hamcrest.MatcherAssert.*
+import org.hamcrest.Matchers.*
+import org.junit.*
+import java.util.*
+import kotlin.collections.*
 
 /**
  * Unit test for [L10nFilter].
  */
 class L10nFilterTest {
 
-       private val l10n = mock<BaseL10n>()
-       private val filter = L10nFilter(l10n)
-       private val templateContext = mock<TemplateContext>()
        private val translations = mutableMapOf<String, String>()
-
-       @Before
-       fun setupL10n() {
-               whenever(l10n.selectedLanguage).thenReturn(ENGLISH)
-               whenever(l10n.getString(anyString())).then { translations[it.arguments[0]] }
+       private val translation = object : Translation {
+               override val currentLocale = Locale.ENGLISH
+               override fun translate(key: String): String = translations[key] ?: ""
        }
+       private val filter = L10nFilter(translation)
 
        @Test
        fun `translation without parameters returns translated string`() {
                translations["data"] = "translated data"
-               assertThat(filter.format(templateContext, "data", emptyMap()), equalTo("translated data"))
+               assertThat(filter.format(null, "data", emptyMap()), equalTo("translated data"))
        }
 
        @Test
        fun `translation with parameters returned translated string`() {
                translations["data"] = "translated {0,number} {1}"
-               assertThat(filter.format(templateContext, "data", mapOf("0" to 4.5, "1" to "data")), equalTo("translated 4.5 data"))
+               assertThat(filter.format(null, "data", mapOf("0" to 4.5, "1" to "data")), equalTo("translated 4.5 data"))
        }
 
        @Test
        fun `filter processes l10n text without parameters correctly`() {
                translations["data"] = "translated data"
-               assertThat(filter.format(templateContext, L10nText("data"), emptyMap()), equalTo("translated data"))
+               assertThat(filter.format(null, L10nText("data"), emptyMap()), equalTo("translated data"))
        }
 
        @Test
        fun `filter processes l10n text with parameters correctly`() {
                translations["data"] = "translated {0,number} {1}"
-               assertThat(filter.format(templateContext, L10nText("data", listOf(4.5, "data")), emptyMap()), equalTo("translated 4.5 data"))
+               assertThat(filter.format(null, L10nText("data", listOf(4.5, "data")), emptyMap()), equalTo("translated 4.5 data"))
        }
 
        @Test
        fun `filter does not replace values if there are no parameters`() {
                translations["data"] = "{link}"
-               assertThat(filter.format(templateContext, "data", emptyMap()), equalTo("{link}"))
+               assertThat(filter.format(null, "data", emptyMap()), equalTo("{link}"))
        }
 
 }
diff --git a/src/test/kotlin/net/pterodactylus/sone/freenet/plugin/FredPluginConnectorTest.kt b/src/test/kotlin/net/pterodactylus/sone/freenet/plugin/FredPluginConnectorTest.kt
new file mode 100644 (file)
index 0000000..0c68bfe
--- /dev/null
@@ -0,0 +1,77 @@
+/**
+ * Sone - FredPluginConnectorTest.kt - Copyright © 2019–2020 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/* Fred-based plugin stuff is mostly deprecated. ¯\_(ツ)_/¯ */
+@file:Suppress("DEPRECATION")
+
+package net.pterodactylus.sone.freenet.plugin
+
+import freenet.pluginmanager.*
+import freenet.support.*
+import freenet.support.api.*
+import freenet.support.io.*
+import kotlinx.coroutines.*
+import net.pterodactylus.sone.freenet.*
+import org.hamcrest.MatcherAssert.*
+import org.hamcrest.Matchers.*
+import org.junit.*
+import org.junit.rules.*
+import kotlin.concurrent.*
+
+class FredPluginConnectorTest {
+
+       @Rule
+       @JvmField
+       val expectedException = ExpectedException.none()!!
+
+       @Test
+       fun `connector throws exception if plugin can not be found`() = runBlocking {
+               val pluginConnector = FredPluginConnector(pluginRespiratorFacade)
+               expectedException.expect(PluginException::class.java)
+               pluginConnector.sendRequest("wrong.plugin", requestFields, requestData)
+               Unit
+       }
+
+       @Test
+       fun `connector returns correct fields and data`() = runBlocking {
+               val pluginConnector = FredPluginConnector(pluginRespiratorFacade)
+               val reply = pluginConnector.sendRequest("test.plugin", requestFields, requestData)
+               assertThat(reply.fields, equalTo(responseFields))
+               assertThat(reply.data, equalTo(responseData))
+       }
+
+}
+
+private val requestFields = SimpleFieldSetBuilder().put("foo", "bar").get()
+private val requestData: Bucket? = ArrayBucket(byteArrayOf(1, 2))
+private val responseFields = SimpleFieldSetBuilder().put("baz", "quo").get()
+private val responseData: Bucket? = ArrayBucket(byteArrayOf(3, 4))
+
+private val pluginRespiratorFacade = object : PluginRespiratorFacade {
+       override fun getPluginTalker(pluginTalker: FredPluginTalker, pluginName: String, identifier: String) =
+                       if (pluginName == "test.plugin") {
+                               object : PluginTalkerFacade {
+                                       override fun send(pluginParameters: SimpleFieldSet, data: Bucket?) {
+                                               if ((pluginParameters == requestFields) && (data == requestData)) {
+                                                       thread { pluginTalker.onReply(pluginName, identifier, responseFields, responseData) }
+                                               }
+                                       }
+                               }
+                       } else {
+                               throw PluginNotFoundException()
+                       }
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/freenet/plugin/PluginRespiratorFacadeTest.kt b/src/test/kotlin/net/pterodactylus/sone/freenet/plugin/PluginRespiratorFacadeTest.kt
new file mode 100644 (file)
index 0000000..4e44d5d
--- /dev/null
@@ -0,0 +1,46 @@
+package net.pterodactylus.sone.freenet.plugin
+
+import freenet.pluginmanager.*
+import freenet.support.*
+import freenet.support.api.*
+import freenet.support.io.*
+import net.pterodactylus.sone.freenet.*
+import net.pterodactylus.sone.test.*
+import org.hamcrest.MatcherAssert.*
+import org.hamcrest.Matchers.*
+import org.mockito.ArgumentMatchers.*
+import kotlin.test.*
+
+/**
+ * Unit test for [FredPluginRespiratorFacade] and [FredPluginTalkerFacade].
+ */
+@Suppress("DEPRECATION")
+class PluginRespiratorFacadeTest {
+
+       @Test
+       fun `respirator facade creates correct plugin talker facade`() {
+               val pluginTalkerSendParameters = mutableListOf<PluginTalkerSendParameters>()
+               val originalPluginTalker = mock<PluginTalker>().apply {
+                       whenever(send(any(), any())).then { invocation ->
+                               pluginTalkerSendParameters += PluginTalkerSendParameters(invocation.getArgument(0), invocation.getArgument(1))
+                               Unit
+                       }
+               }
+               val fredPluginTalker = FredPluginTalker { _, _, _, _ -> }
+               val pluginRespirator = mock<PluginRespirator>().apply {
+                       whenever(getPluginTalker(fredPluginTalker, "test.plugin", "test-request-1")).thenReturn(originalPluginTalker)
+               }
+               val pluginRespiratorFacade = FredPluginRespiratorFacade(pluginRespirator)
+               val pluginTalker = pluginRespiratorFacade.getPluginTalker(fredPluginTalker, "test.plugin", "test-request-1")
+               pluginTalker.send(fields, data)
+               assertThat(pluginTalkerSendParameters, hasSize(1))
+               assertThat(pluginTalkerSendParameters[0].parameter, equalTo(fields))
+               assertThat(pluginTalkerSendParameters[0].data, equalTo(data))
+       }
+
+}
+
+private val fields = SimpleFieldSetBuilder().put("foo", "bar").get()
+private val data: Bucket? = ArrayBucket(byteArrayOf(1, 2))
+
+private data class PluginTalkerSendParameters(val parameter: SimpleFieldSet, val data: Bucket?)
diff --git a/src/test/kotlin/net/pterodactylus/sone/freenet/wot/DefaultIdentityTest.kt b/src/test/kotlin/net/pterodactylus/sone/freenet/wot/DefaultIdentityTest.kt
new file mode 100644 (file)
index 0000000..4d5c0ec
--- /dev/null
@@ -0,0 +1,142 @@
+/*
+ * Sone - DefaultIdentityTest.kt - Copyright © 2013–2020 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.freenet.wot
+
+import com.google.common.collect.ImmutableMap.*
+import net.pterodactylus.sone.test.*
+import net.pterodactylus.sone.test.Matchers.*
+import org.hamcrest.MatcherAssert.*
+import org.hamcrest.Matchers.containsInAnyOrder
+import org.hamcrest.Matchers.empty
+import org.hamcrest.Matchers.equalTo
+import org.hamcrest.Matchers.hasEntry
+import org.hamcrest.Matchers.not
+import org.hamcrest.Matchers.nullValue
+import org.hamcrest.collection.IsIterableContainingInOrder.contains
+import org.junit.*
+
+/**
+ * Unit test for [DefaultIdentity].
+ */
+open class DefaultIdentityTest {
+
+       protected open val identity = DefaultIdentity("Id", "Nickname", "RequestURI")
+
+       @Test
+       fun `identity can be created`() {
+               assertThat(identity.id, equalTo("Id"))
+               assertThat(identity.nickname, equalTo("Nickname"))
+               assertThat(identity.requestUri, equalTo("RequestURI"))
+               assertThat(identity.contexts, empty())
+               assertThat(identity.properties, equalTo(emptyMap()))
+       }
+
+       @Test
+       fun `contexts are added correctly`() {
+               identity.addContext("Test")
+               assertThat(identity.contexts, contains("Test"))
+               assertThat(identity.hasContext("Test"), equalTo(true))
+       }
+
+       @Test
+       fun `contexts are removed correctly`() {
+               identity.addContext("Test")
+               identity.removeContext("Test")
+               assertThat(identity.contexts, empty())
+               assertThat(identity.hasContext("Test"), equalTo(false))
+       }
+
+       @Test
+       fun `contexts are set correctly in bulk`() {
+               identity.addContext("Test")
+               identity.contexts = setOf("Test1", "Test2")
+               assertThat(identity.contexts, containsInAnyOrder("Test1", "Test2"))
+               assertThat(identity.hasContext("Test"), equalTo(false))
+               assertThat(identity.hasContext("Test1"), equalTo(true))
+               assertThat(identity.hasContext("Test2"), equalTo(true))
+       }
+
+       @Test
+       fun `properties are added correctly`() {
+               identity.setProperty("Key", "Value")
+               assertThat(identity.properties.size, equalTo(1))
+               assertThat(identity.properties, hasEntry("Key", "Value"))
+               assertThat(identity.getProperty("Key"), equalTo("Value"))
+       }
+
+       @Test
+       fun `properties are removed correctly`() {
+               identity.setProperty("Key", "Value")
+               identity.removeProperty("Key")
+               assertThat(identity.properties, equalTo(emptyMap()))
+               assertThat(identity.getProperty("Key"), nullValue())
+       }
+
+       @Test
+       fun `properties are set correctly in bulk`() {
+               identity.setProperty("Key", "Value")
+               identity.properties = of("Key1", "Value1", "Key2", "Value2")
+               assertThat(identity.properties.size, equalTo(2))
+               assertThat(identity.getProperty("Key"), nullValue())
+               assertThat(identity.getProperty("Key1"), equalTo("Value1"))
+               assertThat(identity.getProperty("Key2"), equalTo("Value2"))
+       }
+
+       @Test
+       fun `trust relationships are added correctly`() {
+               val ownIdentity = mock<OwnIdentity>()
+               val trust = mock<Trust>()
+               identity.setTrust(ownIdentity, trust)
+               assertThat(identity.getTrust(ownIdentity), equalTo(trust))
+       }
+
+       @Test
+       fun `trust relationships are removed correctly`() {
+               val ownIdentity = mock<OwnIdentity>()
+               val trust = mock<Trust>()
+               identity.setTrust(ownIdentity, trust)
+               identity.removeTrust(ownIdentity)
+               assertThat(identity.getTrust(ownIdentity), nullValue())
+       }
+
+       @Test
+       fun `identities with the same id are equal`() {
+               val identity2 = DefaultIdentity("Id", "Nickname2", "RequestURI2")
+               assertThat(identity2, equalTo(identity))
+               assertThat(identity, equalTo(identity2))
+       }
+
+       @Test
+       fun `two equal identities have the same hash code`() {
+               val identity2 = DefaultIdentity("Id", "Nickname2", "RequestURI2")
+               assertThat(identity.hashCode(), equalTo(identity2.hashCode()))
+       }
+
+       @Test
+       fun `null does not match an identity`() {
+               assertThat(identity, not(equalTo<Any>(null as Any?)))
+       }
+
+       @Test
+       fun `toString() contains id and nickname`() {
+               val identityString = identity.toString()
+               assertThat(identityString, matchesRegex(".*\\bId\\b.*"))
+               assertThat(identityString, matchesRegex(".*\\bNickname\\b.*"))
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/freenet/wot/DefaultOwnIdentityTest.kt b/src/test/kotlin/net/pterodactylus/sone/freenet/wot/DefaultOwnIdentityTest.kt
new file mode 100644 (file)
index 0000000..2b6fc09
--- /dev/null
@@ -0,0 +1,36 @@
+/*
+ * Sone - DefaultOwnIdentityTest.kt - Copyright © 2013–2020 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.freenet.wot
+
+import org.hamcrest.MatcherAssert.*
+import org.hamcrest.Matchers.*
+import org.junit.*
+
+/**
+ * Unit test for [DefaultOwnIdentity].
+ */
+class DefaultOwnIdentityTest : DefaultIdentityTest() {
+
+       override val identity = DefaultOwnIdentity("Id", "Nickname", "RequestURI", "InsertURI")
+
+       @Test
+       fun `own identity can be created`() {
+               assertThat((identity as OwnIdentity).insertUri, equalTo("InsertURI"))
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/freenet/wot/Identities.kt b/src/test/kotlin/net/pterodactylus/sone/freenet/wot/Identities.kt
new file mode 100644 (file)
index 0000000..d699bb2
--- /dev/null
@@ -0,0 +1,35 @@
+/*
+ * Sone - Identities.kt - Copyright © 2013–2020 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.freenet.wot
+
+fun createOwnIdentity(id: String, contexts: Set<String>, vararg properties: Pair<String, String>): OwnIdentity {
+       val ownIdentity = DefaultOwnIdentity(id, "Nickname$id", "Request$id", "Insert$id")
+       setContextsAndPropertiesOnIdentity(ownIdentity, contexts, mapOf(*properties))
+       return ownIdentity
+}
+
+fun createIdentity(id: String, contexts: Set<String>, vararg properties: Pair<String, String>): Identity {
+       val identity = DefaultIdentity(id, "Nickname$id", "Request$id")
+       setContextsAndPropertiesOnIdentity(identity, contexts, mapOf(*properties))
+       return identity
+}
+
+private fun setContextsAndPropertiesOnIdentity(identity: Identity, contexts: Set<String>, properties: Map<String, String>) {
+       identity.contexts = contexts
+       identity.properties = properties
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/freenet/wot/IdentityChangeDetectorTest.kt b/src/test/kotlin/net/pterodactylus/sone/freenet/wot/IdentityChangeDetectorTest.kt
new file mode 100644 (file)
index 0000000..c02610c
--- /dev/null
@@ -0,0 +1,154 @@
+/*
+ * Sone - IdentityChangeDetectorTest.kt - Copyright © 2013–2020 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.freenet.wot
+
+import org.hamcrest.MatcherAssert.*
+import org.hamcrest.Matchers.*
+import org.junit.*
+
+/**
+ * Unit test for [IdentityChangeDetector].
+ */
+class IdentityChangeDetectorTest {
+
+       private val identityChangeDetector = IdentityChangeDetector(createOldIdentities())
+       private val newIdentities = mutableListOf<Identity>()
+       private val removedIdentities = mutableListOf<Identity>()
+       private val changedIdentities = mutableListOf<Identity>()
+       private val unchangedIdentities = mutableListOf<Identity>()
+
+       @Before
+       fun setup() {
+               identityChangeDetector.onNewIdentity = { identity -> newIdentities.add(identity) }
+               identityChangeDetector.onRemovedIdentity = { identity -> removedIdentities.add(identity) }
+               identityChangeDetector.onChangedIdentity = { identity -> changedIdentities.add(identity) }
+               identityChangeDetector.onUnchangedIdentity = { identity -> unchangedIdentities.add(identity) }
+       }
+
+       @Test
+       fun `no differences are detected when sending the old identities again`() {
+               identityChangeDetector.detectChanges(createOldIdentities())
+               assertThat(newIdentities, empty())
+               assertThat(removedIdentities, empty())
+               assertThat(changedIdentities, empty())
+               assertThat(unchangedIdentities, containsInAnyOrder(createIdentity1(), createIdentity2(), createIdentity3()))
+       }
+
+       @Test
+       fun `detect that an identity was removed`() {
+               identityChangeDetector.detectChanges(listOf(createIdentity1(), createIdentity3()))
+               assertThat(newIdentities, empty())
+               assertThat(removedIdentities, containsInAnyOrder(createIdentity2()))
+               assertThat(changedIdentities, empty())
+               assertThat(unchangedIdentities, containsInAnyOrder(createIdentity1(), createIdentity3()))
+       }
+
+       @Test
+       fun `detect that an identity was added`() {
+               identityChangeDetector.detectChanges(listOf(createIdentity1(), createIdentity2(), createIdentity3(), createIdentity4()))
+               assertThat(newIdentities, containsInAnyOrder(createIdentity4()))
+               assertThat(removedIdentities, empty())
+               assertThat(changedIdentities, empty())
+               assertThat(unchangedIdentities, containsInAnyOrder(createIdentity1(), createIdentity2(), createIdentity3()))
+       }
+
+       @Test
+       fun `detect that a context was removed`() {
+               val identity2 = createIdentity2()
+               identity2.removeContext("Context C")
+               identityChangeDetector.detectChanges(listOf(createIdentity1(), identity2, createIdentity3()))
+               assertThat(newIdentities, empty())
+               assertThat(removedIdentities, empty())
+               assertThat(changedIdentities, containsInAnyOrder(identity2))
+               assertThat(unchangedIdentities, containsInAnyOrder(createIdentity1(), createIdentity3()))
+       }
+
+       @Test
+       fun `detect that a context was added`() {
+               val identity2 = createIdentity2()
+               identity2.addContext("Context C1")
+               identityChangeDetector.detectChanges(listOf(createIdentity1(), identity2, createIdentity3()))
+               assertThat(newIdentities, empty())
+               assertThat(removedIdentities, empty())
+               assertThat(changedIdentities, containsInAnyOrder(identity2))
+               assertThat(unchangedIdentities, containsInAnyOrder(createIdentity1(), createIdentity3()))
+       }
+
+       @Test
+       fun `detect that a property was removed`() {
+               val identity1 = createIdentity1()
+               identity1.removeProperty("Key A")
+               identityChangeDetector.detectChanges(listOf(identity1, createIdentity2(), createIdentity3()))
+               assertThat(newIdentities, empty())
+               assertThat(removedIdentities, empty())
+               assertThat(changedIdentities, containsInAnyOrder(identity1))
+               assertThat(unchangedIdentities, containsInAnyOrder(createIdentity2(), createIdentity3()))
+       }
+
+       @Test
+       fun `detect that a property was added`() {
+               val identity3 = createIdentity3()
+               identity3.setProperty("Key A", "Value A")
+               identityChangeDetector.detectChanges(listOf(createIdentity1(), createIdentity2(), identity3))
+               assertThat(newIdentities, empty())
+               assertThat(removedIdentities, empty())
+               assertThat(changedIdentities, containsInAnyOrder(identity3))
+               assertThat(unchangedIdentities, containsInAnyOrder(createIdentity1(), createIdentity2()))
+       }
+
+       @Test
+       fun `detect that a property was changed`() {
+               val identity3 = createIdentity3()
+               identity3.setProperty("Key E", "Value F")
+               identityChangeDetector.detectChanges(listOf(createIdentity1(), createIdentity2(), identity3))
+               assertThat(newIdentities, empty())
+               assertThat(removedIdentities, empty())
+               assertThat(changedIdentities, containsInAnyOrder(identity3))
+               assertThat(unchangedIdentities, containsInAnyOrder(createIdentity1(), createIdentity2()))
+       }
+
+       @Test
+       fun `no removed identities are detected without an identity processor`() {
+               identityChangeDetector.onRemovedIdentity = null
+               identityChangeDetector.detectChanges(listOf(createIdentity1(), createIdentity3()))
+               assertThat(removedIdentities, empty())
+       }
+
+       @Test
+       fun `no added identities are detected without an identity processor`() {
+               identityChangeDetector.onNewIdentity = null
+               identityChangeDetector.detectChanges(listOf(createIdentity1(), createIdentity2(), createIdentity3(), createIdentity4()))
+               assertThat(newIdentities, empty())
+       }
+
+       private fun createOldIdentities() =
+                       listOf(createIdentity1(), createIdentity2(), createIdentity3())
+
+       private fun createIdentity1() =
+                       createIdentity("Test1", setOf("Context A", "Context B"), "Key A" to "Value A", "Key B" to "Value B")
+
+       private fun createIdentity2() =
+                       createIdentity("Test2", setOf("Context C", "Context D"), "Key C" to "Value C", "Key D" to "Value D")
+
+       private fun createIdentity3() =
+                       createIdentity("Test3", setOf("Context E", "Context F"), "Key E" to "Value E", "Key F" to "Value F")
+
+       private fun createIdentity4() =
+                       createIdentity("Test4", setOf("Context G", "Context H"), "Key G" to "Value G", "Key H" to "Value H")
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/freenet/wot/IdentityChangeEventSenderTest.kt b/src/test/kotlin/net/pterodactylus/sone/freenet/wot/IdentityChangeEventSenderTest.kt
new file mode 100644 (file)
index 0000000..0236a3e
--- /dev/null
@@ -0,0 +1,71 @@
+/*
+ * Sone - IdentityChangeEventSenderTest.kt - Copyright © 2013–2020 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.freenet.wot
+
+import com.google.common.eventbus.*
+import net.pterodactylus.sone.freenet.wot.event.*
+import net.pterodactylus.sone.test.*
+import org.junit.*
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mockito.verify
+
+/**
+ * Unit test for [IdentityChangeEventSender].
+ */
+class IdentityChangeEventSenderTest {
+
+       private val eventBus = mock<EventBus>()
+       private val ownIdentities = listOf(
+                       createOwnIdentity("O1", setOf("Test"), "KeyA" to "ValueA"),
+                       createOwnIdentity("O2", setOf("Test2"), "KeyB" to "ValueB"),
+                       createOwnIdentity("O3", setOf("Test3"), "KeyC" to "ValueC")
+       )
+       private val identities = listOf(
+                       createIdentity("I1", setOf()),
+                       createIdentity("I2", setOf()),
+                       createIdentity("I3", setOf()),
+                       createIdentity("I2", setOf("Test"))
+       )
+       private val identityChangeEventSender = IdentityChangeEventSender(eventBus, createOldIdentities())
+
+       @Test
+       fun addingAnOwnIdentityIsDetectedAndReportedCorrectly() {
+               val newIdentities = createNewIdentities()
+               identityChangeEventSender.detectChanges(newIdentities)
+               verify(eventBus).post(eq(OwnIdentityRemovedEvent(ownIdentities[0])))
+               verify(eventBus).post(eq(IdentityRemovedEvent(ownIdentities[0], identities[0])))
+               verify(eventBus).post(eq(IdentityRemovedEvent(ownIdentities[0], identities[1])))
+               verify(eventBus).post(eq(OwnIdentityAddedEvent(ownIdentities[2])))
+               verify(eventBus).post(eq(IdentityAddedEvent(ownIdentities[2], identities[1])))
+               verify(eventBus).post(eq(IdentityAddedEvent(ownIdentities[2], identities[2])))
+               verify(eventBus).post(eq(IdentityRemovedEvent(ownIdentities[1], identities[0])))
+               verify(eventBus).post(eq(IdentityAddedEvent(ownIdentities[1], identities[2])))
+               verify(eventBus).post(eq(IdentityUpdatedEvent(ownIdentities[1], identities[1])))
+       }
+
+       private fun createNewIdentities() = mapOf(
+                       ownIdentities[1] to listOf(identities[3], identities[2]),
+                       ownIdentities[2] to listOf(identities[1], identities[2])
+       )
+
+       private fun createOldIdentities() = mapOf(
+                       ownIdentities[0] to listOf(identities[0], identities[1]),
+                       ownIdentities[1] to listOf(identities[0], identities[1])
+       )
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/freenet/wot/IdentityLoaderTest.kt b/src/test/kotlin/net/pterodactylus/sone/freenet/wot/IdentityLoaderTest.kt
new file mode 100644 (file)
index 0000000..0e0e336
--- /dev/null
@@ -0,0 +1,116 @@
+/*
+ * Sone - IdentityLoaderTest.kt - Copyright © 2013–2020 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.freenet.wot
+
+import org.hamcrest.MatcherAssert.*
+import org.hamcrest.Matchers.*
+import org.junit.*
+
+/**
+ * Unit test for [IdentityLoader].
+ */
+class IdentityLoaderTest {
+
+       private val ownIdentities = createOwnIdentities()
+       private val webOfTrustConnector = object : TestWebOfTrustConnector() {
+               override fun loadAllOwnIdentities() = ownIdentities.toSet()
+               override fun loadTrustedIdentities(ownIdentity: OwnIdentity, context: String?) =
+                               when (ownIdentity) {
+                                       ownIdentities[0] -> createTrustedIdentitiesForFirstOwnIdentity()
+                                       ownIdentities[1] -> createTrustedIdentitiesForSecondOwnIdentity()
+                                       ownIdentities[2] -> createTrustedIdentitiesForThirdOwnIdentity()
+                                       ownIdentities[3] -> createTrustedIdentitiesForFourthOwnIdentity()
+                                       else -> throw RuntimeException()
+                               }
+       }
+
+       @Test
+       fun loadingIdentities() {
+               val identityLoader = IdentityLoader(webOfTrustConnector, Context("Test"))
+               val identities = identityLoader.loadTrustedIdentities()
+               assertThat(identities.keys, hasSize(4))
+               assertThat(identities.keys, containsInAnyOrder(ownIdentities[0], ownIdentities[1], ownIdentities[2], ownIdentities[3]))
+               verifyIdentitiesForOwnIdentity(identities, ownIdentities[0], createTrustedIdentitiesForFirstOwnIdentity())
+               verifyIdentitiesForOwnIdentity(identities, ownIdentities[1], createTrustedIdentitiesForSecondOwnIdentity())
+               verifyIdentitiesForOwnIdentity(identities, ownIdentities[2], emptySet())
+               verifyIdentitiesForOwnIdentity(identities, ownIdentities[3], createTrustedIdentitiesForFourthOwnIdentity())
+       }
+
+       @Test
+       fun loadingIdentitiesWithoutContext() {
+               val identityLoaderWithoutContext = IdentityLoader(webOfTrustConnector)
+               val identities = identityLoaderWithoutContext.loadTrustedIdentities()
+               assertThat(identities.keys, hasSize(4))
+               assertThat(identities.keys, containsInAnyOrder(ownIdentities[0], ownIdentities[1], ownIdentities[2], ownIdentities[3]))
+               verifyIdentitiesForOwnIdentity(identities, ownIdentities[0], createTrustedIdentitiesForFirstOwnIdentity())
+               verifyIdentitiesForOwnIdentity(identities, ownIdentities[1], createTrustedIdentitiesForSecondOwnIdentity())
+               verifyIdentitiesForOwnIdentity(identities, ownIdentities[2], createTrustedIdentitiesForThirdOwnIdentity())
+               verifyIdentitiesForOwnIdentity(identities, ownIdentities[3], createTrustedIdentitiesForFourthOwnIdentity())
+       }
+
+       private fun verifyIdentitiesForOwnIdentity(identities: Map<OwnIdentity, Collection<Identity>>, ownIdentity: OwnIdentity, trustedIdentities: Set<Identity>) {
+               assertThat(identities[ownIdentity], equalTo<Collection<Identity>>(trustedIdentities))
+       }
+
+}
+
+private fun createOwnIdentities() = listOf(
+               createOwnIdentity("O1", "ON1", "OR1", "OI1", setOf("Test", "Test2"), mapOf("KeyA" to "ValueA", "KeyB" to "ValueB")),
+               createOwnIdentity("O2", "ON2", "OR2", "OI2", setOf("Test"), mapOf("KeyC" to "ValueC")),
+               createOwnIdentity("O3", "ON3", "OR3", "OI3", setOf("Test2"), mapOf("KeyE" to "ValueE", "KeyD" to "ValueD")),
+               createOwnIdentity("O4", "ON4", "OR$", "OI4", setOf("Test"), mapOf("KeyA" to "ValueA", "KeyD" to "ValueD"))
+)
+
+private fun createTrustedIdentitiesForFirstOwnIdentity() = setOf(
+               createIdentity("I11", "IN11", "IR11", setOf("Test"), mapOf("KeyA" to "ValueA"))
+)
+
+private fun createTrustedIdentitiesForSecondOwnIdentity() = setOf(
+               createIdentity("I21", "IN21", "IR21", setOf("Test", "Test2"), mapOf("KeyB" to "ValueB"))
+)
+
+private fun createTrustedIdentitiesForThirdOwnIdentity() = setOf(
+               createIdentity("I31", "IN31", "IR31", setOf("Test", "Test3"), mapOf("KeyC" to "ValueC"))
+)
+
+private fun createTrustedIdentitiesForFourthOwnIdentity(): Set<Identity> = emptySet()
+
+private fun createOwnIdentity(id: String, nickname: String, requestUri: String, insertUri: String, contexts: Set<String>, properties: Map<String, String>): OwnIdentity =
+               DefaultOwnIdentity(id, nickname, requestUri, insertUri).apply {
+                       setContexts(contexts)
+                       this.properties = properties
+               }
+
+private fun createIdentity(id: String, nickname: String, requestUri: String, contexts: Set<String>, properties: Map<String, String>): Identity =
+               DefaultIdentity(id, nickname, requestUri).apply {
+                       setContexts(contexts)
+                       this.properties = properties
+               }
+
+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
+       override fun removeProperty(ownIdentity: OwnIdentity, name: String) = Unit
+       override fun ping() = 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()
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/freenet/wot/PluginWebOfTrustConnectorTest.kt b/src/test/kotlin/net/pterodactylus/sone/freenet/wot/PluginWebOfTrustConnectorTest.kt
new file mode 100644 (file)
index 0000000..a9c0412
--- /dev/null
@@ -0,0 +1,242 @@
+package net.pterodactylus.sone.freenet.wot
+
+import freenet.support.*
+import freenet.support.api.*
+import net.pterodactylus.sone.freenet.*
+import net.pterodactylus.sone.freenet.plugin.*
+import net.pterodactylus.sone.test.*
+import org.hamcrest.*
+import org.hamcrest.MatcherAssert.*
+import org.hamcrest.Matchers.*
+import org.hamcrest.core.*
+import kotlin.test.*
+
+/**
+ * Unit test for [PluginWebOfTrustConnector].
+ */
+class PluginWebOfTrustConnectorTest {
+
+       private val ownIdentity = DefaultOwnIdentity("id", "nickname", "requestUri", "insertUri")
+
+       @Test
+       fun `wot plugin can be pinged`() {
+               createPluginConnector("Ping")
+                               .connect { ping() }
+       }
+
+       @Test
+       fun `own identities are returned correctly`() {
+               val ownIdentities = createPluginConnector("GetOwnIdentities") {
+                       put("Identity0", "id-0")
+                       put("RequestURI0", "request-uri-0")
+                       put("InsertURI0", "insert-uri-0")
+                       put("Nickname0", "nickname-0")
+                       put("Contexts0.Context0", "id-0-context-0")
+                       put("Properties0.Property0.Name", "id-0-property-0-name")
+                       put("Properties0.Property0.Value", "id-0-property-0-value")
+                       put("Identity1", "id-1")
+                       put("RequestURI1", "request-uri-1")
+                       put("InsertURI1", "insert-uri-1")
+                       put("Nickname1", "nickname-1")
+                       put("Contexts1.Context0", "id-1-context-0")
+                       put("Properties1.Property0.Name", "id-1-property-0-name")
+                       put("Properties1.Property0.Value", "id-1-property-0-value")
+               }.connect { loadAllOwnIdentities() }
+               assertThat(ownIdentities, containsInAnyOrder(
+                               isOwnIdentity("id-0", "nickname-0", "request-uri-0", "insert-uri-0", contains("id-0-context-0"), hasEntry("id-0-property-0-name", "id-0-property-0-value")),
+                               isOwnIdentity("id-1", "nickname-1", "request-uri-1", "insert-uri-1", contains("id-1-context-0"), hasEntry("id-1-property-0-name", "id-1-property-0-value"))
+               ))
+       }
+
+       @Test
+       fun `trusted identities are requested with correct own identity`() {
+               createPluginConnector("GetIdentitiesByScore", hasField("Truster", equalTo("id")))
+                               .connect { loadTrustedIdentities(ownIdentity) }
+       }
+
+       @Test
+       fun `trusted identities are requested with correct selection parameter`() {
+               createPluginConnector("GetIdentitiesByScore", hasField("Selection", equalTo("+")))
+                               .connect { loadTrustedIdentities(ownIdentity) }
+       }
+
+       @Test
+       fun `trusted identities are requested with empty context if null context requested`() {
+               createPluginConnector("GetIdentitiesByScore", hasField("Context", equalTo("")))
+                               .connect { loadTrustedIdentities(ownIdentity) }
+       }
+
+       @Test
+       fun `trusted identities are requested with context if context requested`() {
+               createPluginConnector("GetIdentitiesByScore", hasField("Context", equalTo("TestContext")))
+                               .connect { loadTrustedIdentities(ownIdentity, "TestContext") }
+       }
+
+       @Test
+       fun `trusted identities are requested with trust values`() {
+               createPluginConnector("GetIdentitiesByScore", hasField("WantTrustValues", equalTo("true")))
+                               .connect { loadTrustedIdentities(ownIdentity) }
+       }
+
+       @Test
+       fun `empty list of trusted identities is returned correctly`() {
+               val trustedIdentities = createPluginConnector("GetIdentitiesByScore")
+                               .connect { loadTrustedIdentities(ownIdentity) }
+               assertThat(trustedIdentities, empty())
+       }
+
+       @Test
+       fun `trusted identities without context, properties, or trust value are returned correctly`() {
+               val trustedIdentities = createPluginConnector("GetIdentitiesByScore") {
+                       put("Identity0", "id0")
+                       put("Nickname0", "nickname0")
+                       put("RequestURI0", "request-uri0")
+                       put("Identity1", "id1")
+                       put("Nickname1", "nickname1")
+                       put("RequestURI1", "request-uri1")
+               }.connect { loadTrustedIdentities(ownIdentity) }
+               assertThat(trustedIdentities, contains(
+                               allOf(
+                                               isIdentity("id0", "nickname0", "request-uri0", empty<String>(), isEmptyMap()),
+                                               isTrusted(ownIdentity, isTrust(null, null, null))
+                               ),
+                               allOf(
+                                               isIdentity("id1", "nickname1", "request-uri1", empty<String>(), isEmptyMap()),
+                                               isTrusted(ownIdentity, isTrust(null, null, null))
+                               )
+               ))
+       }
+
+       @Test
+       fun `trusted identity without nickname is returned correctly`() {
+               val trustedIdentities = createPluginConnector("GetIdentitiesByScore") {
+                       put("Identity0", "id0")
+                       put("RequestURI0", "request-uri0")
+               }.connect { loadTrustedIdentities(ownIdentity) }
+               assertThat(trustedIdentities, contains(
+                               allOf(
+                                               isIdentity("id0", null, "request-uri0", empty<String>(), isEmptyMap()),
+                                               isTrusted(ownIdentity, isTrust(null, null, null))
+                               )
+               ))
+       }
+
+       @Test
+       fun `trusted identity with contexts is returned correctly`() {
+               val trustedIdentities = createPluginConnector("GetIdentitiesByScore") {
+                       put("Identity0", "id0")
+                       put("Nickname0", "nickname0")
+                       put("RequestURI0", "request-uri0")
+                       put("Contexts0.Context0", "Context0")
+                       put("Contexts0.Context1", "Context1")
+               }.connect { loadTrustedIdentities(ownIdentity) }
+               assertThat(trustedIdentities, contains(
+                               isIdentity("id0", "nickname0", "request-uri0", containsInAnyOrder("Context0", "Context1"), isEmptyMap())
+               ))
+       }
+
+       @Test
+       fun `trusted identity with properties is returned correctly`() {
+               val trustedIdentities = createPluginConnector("GetIdentitiesByScore") {
+                       put("Identity0", "id0")
+                       put("Nickname0", "nickname0")
+                       put("RequestURI0", "request-uri0")
+                       put("Properties0.Property0.Name", "foo")
+                       put("Properties0.Property0.Value", "bar")
+                       put("Properties0.Property1.Name", "baz")
+                       put("Properties0.Property1.Value", "quo")
+               }.connect { loadTrustedIdentities(ownIdentity) }
+               assertThat(trustedIdentities, contains(
+                               isIdentity("id0", "nickname0", "request-uri0", empty(), allOf(hasEntry("foo", "bar"), hasEntry("baz", "quo")))
+               ))
+       }
+
+       @Test
+       fun `trusted identity with trust value is returned correctly`() {
+               val trustedIdentities = createPluginConnector("GetIdentitiesByScore") {
+                       put("Identity0", "id0")
+                       put("Nickname0", "nickname0")
+                       put("RequestURI0", "request-uri0")
+                       put("Trust0", "12")
+                       put("Score0", "34")
+                       put("Rank0", "56")
+               }.connect { loadTrustedIdentities(ownIdentity) }
+               assertThat(trustedIdentities, contains(
+                               allOf(
+                                               isIdentity("id0", "nickname0", "request-uri0", empty(), isEmptyMap()),
+                                               isTrusted(ownIdentity, isTrust(12, 34, 56))
+                               )
+               ))
+       }
+
+       @Test
+       fun `adding a context sends the correct own identity id`() {
+               createPluginConnector("AddContext", hasField("Identity", equalTo(ownIdentity.id)))
+                               .connect { addContext(ownIdentity, "TestContext") }
+       }
+
+       @Test
+       fun `adding a context sends the correct context`() {
+               createPluginConnector("AddContext", hasField("Context", equalTo("TestContext")))
+                               .connect { addContext(ownIdentity, "TestContext") }
+       }
+
+       @Test
+       fun `removing a context sends the correct own identity id`() {
+               createPluginConnector("RemoveContext", hasField("Identity", equalTo(ownIdentity.id)))
+                               .connect { removeContext(ownIdentity, "TestContext") }
+       }
+
+       @Test
+       fun `removing a context sends the correct context`() {
+               createPluginConnector("RemoveContext", hasField("Context", equalTo("TestContext")))
+                               .connect { removeContext(ownIdentity, "TestContext") }
+       }
+
+       @Test
+       fun `setting a property sends the correct identity id`() {
+               createPluginConnector("SetProperty", hasField("Identity", equalTo(ownIdentity.id)))
+                               .connect { setProperty(ownIdentity, "TestProperty", "TestValue") }
+       }
+
+       @Test
+       fun `setting a property sends the correct property name`() {
+               createPluginConnector("SetProperty", hasField("Property", equalTo("TestProperty")))
+                               .connect { setProperty(ownIdentity, "TestProperty", "TestValue") }
+       }
+
+       @Test
+       fun `setting a property sends the correct property value`() {
+               createPluginConnector("SetProperty", hasField("Value", equalTo("TestValue")))
+                               .connect { setProperty(ownIdentity, "TestProperty", "TestValue") }
+       }
+
+       @Test
+       fun `removing a property sends the correct identity id`() {
+               createPluginConnector("RemoveProperty", hasField("Identity", equalTo(ownIdentity.id)))
+                               .connect { removeProperty(ownIdentity, "TestProperty") }
+       }
+
+       @Test
+       fun `removing a property sends the correct property name`() {
+               createPluginConnector("RemoveProperty", hasField("Property", equalTo("TestProperty")))
+                               .connect { removeProperty(ownIdentity, "TestProperty") }
+       }
+
+}
+
+private fun <R> PluginConnector.connect(block: PluginWebOfTrustConnector.() -> R) =
+               PluginWebOfTrustConnector(this).let(block)
+
+fun createPluginConnector(message: String, fieldsMatcher: Matcher<SimpleFieldSet> = IsAnything<SimpleFieldSet>(), build: SimpleFieldSetBuilder.() -> Unit = {}) =
+               object : PluginConnector {
+                       override suspend fun sendRequest(pluginName: String, fields: SimpleFieldSet, data: Bucket?) =
+                                       if ((pluginName != wotPluginName) || (fields.get("Message") != message)) {
+                                               throw PluginException()
+                                       } else {
+                                               assertThat(fields, fieldsMatcher)
+                                               PluginReply(SimpleFieldSetBuilder().apply(build).get(), null)
+                                       }
+               }
+
+private const val wotPluginName = "plugins.WebOfTrust.WebOfTrust"
diff --git a/src/test/kotlin/net/pterodactylus/sone/freenet/wot/WebOfTrustPingerTest.kt b/src/test/kotlin/net/pterodactylus/sone/freenet/wot/WebOfTrustPingerTest.kt
new file mode 100644 (file)
index 0000000..9cc1084
--- /dev/null
@@ -0,0 +1,118 @@
+/**
+ * Sone - WebOfTrustPingerTest.kt - Copyright © 2019–2020 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.freenet.wot
+
+import com.google.common.eventbus.*
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.sone.freenet.plugin.*
+import net.pterodactylus.sone.utils.*
+import org.hamcrest.MatcherAssert.*
+import org.hamcrest.Matchers.*
+import java.util.concurrent.atomic.*
+import java.util.function.*
+import kotlin.test.*
+
+/**
+ * Unit test for [WebOfTrustPinger].
+ */
+class WebOfTrustPingerTest {
+
+       private val eventBus = EventBus()
+       private val webOfTrustReachable = AtomicBoolean()
+       private val webOfTrustReacher = Runnable { webOfTrustReachable.get().onFalse { throw PluginException() } }
+       private val rescheduled = AtomicBoolean()
+       private val reschedule: Consumer<Runnable> = Consumer { if (it == pinger) rescheduled.set(true) }
+       private val pinger = WebOfTrustPinger(eventBus, webOfTrustReacher, reschedule)
+
+       @Test
+       fun `pinger sends wot appeared event when run first and wot is reachable`() {
+               webOfTrustReachable.set(true)
+               val appearedReceived = AtomicBoolean()
+               eventBus.register(WebOfTrustAppearedCatcher { appearedReceived.set(true) })
+               pinger()
+               assertThat(appearedReceived.get(), equalTo(true))
+       }
+
+       @Test
+       fun `pinger reschedules when wot is reachable`() {
+               webOfTrustReachable.set(true)
+               pinger()
+               assertThat(rescheduled.get(), equalTo(true))
+       }
+
+       @Test
+       fun `pinger sends wot disappeared event when run first and wot is not reachable`() {
+               val appearedReceived = AtomicBoolean()
+               eventBus.register(WebOfTrustAppearedCatcher { appearedReceived.set(true) })
+               pinger()
+               assertThat(appearedReceived.get(), equalTo(false))
+       }
+
+       @Test
+       fun `pinger reschedules when wot is not reachable`() {
+               pinger()
+               assertThat(rescheduled.get(), equalTo(true))
+       }
+
+       @Test
+       fun `pinger sends wot disappeared event when run twice and wot is not reachable on second call`() {
+               val disappearedReceived = AtomicBoolean()
+               eventBus.register(WebOfTrustDisappearedCatcher { disappearedReceived.set(true) })
+               webOfTrustReachable.set(true)
+               pinger()
+               webOfTrustReachable.set(false)
+               pinger()
+               assertThat(disappearedReceived.get(), equalTo(true))
+       }
+
+       @Test
+       fun `pinger sends wot appeared event only once`() {
+               webOfTrustReachable.set(true)
+               val appearedReceived = AtomicBoolean()
+               eventBus.register(WebOfTrustAppearedCatcher { appearedReceived.set(true) })
+               pinger()
+               appearedReceived.set(false)
+               pinger()
+               assertThat(appearedReceived.get(), equalTo(false))
+       }
+
+       @Test
+       fun `pinger sends wot disappeared event only once`() {
+               val disappearedReceived = AtomicBoolean()
+               eventBus.register(WebOfTrustDisappearedCatcher { disappearedReceived.set(true) })
+               pinger()
+               disappearedReceived.set(false)
+               pinger()
+               assertThat(disappearedReceived.get(), equalTo(false))
+       }
+
+}
+
+private class WebOfTrustAppearedCatcher(private val received: () -> Unit) {
+       @Subscribe
+       fun webOfTrustAppeared(@Suppress("UNUSED_PARAMETER") webOfTrustAppeared: WebOfTrustAppeared) {
+               received()
+       }
+}
+
+private class WebOfTrustDisappearedCatcher(private val received: () -> Unit) {
+       @Subscribe
+       fun webOfTrustDisappeared(@Suppress("UNUSED_PARAMETER") webOfTrustDisappeared: WebOfTrustDisappeared) {
+               received()
+       }
+}
index 2f55d52..c6aed88 100644 (file)
@@ -5,10 +5,13 @@ import freenet.client.*
 import freenet.clients.http.*
 import freenet.node.*
 import freenet.pluginmanager.*
+import net.pterodactylus.sone.freenet.plugin.*
 import net.pterodactylus.sone.test.*
 import org.hamcrest.MatcherAssert.*
 import org.hamcrest.Matchers.*
 import org.junit.*
+import org.junit.rules.*
+import org.mockito.*
 import org.mockito.Mockito.*
 
 /**
@@ -16,6 +19,10 @@ import org.mockito.Mockito.*
  */
 class FreenetModuleTest {
 
+       @Rule
+       @JvmField
+       val expectedException = ExpectedException.none()!!
+
        private val sessionManager = mock<SessionManager>()
        private val pluginRespirator = deepMock<PluginRespirator>().apply {
                whenever(getSessionManager("Sone")).thenReturn(sessionManager)
@@ -27,20 +34,10 @@ class FreenetModuleTest {
        private val module = FreenetModule(pluginRespirator)
        private val injector = Guice.createInjector(module)
 
-       private inline fun <reified T : Any> verifySingletonInstance() {
-               val firstInstance = injector.getInstance<T>()
-               val secondInstance = injector.getInstance<T>()
-               assertThat(firstInstance, sameInstance(secondInstance))
-       }
-
        @Test
-       fun `plugin respirator is returned correctly`() {
-               assertThat(injector.getInstance(), sameInstance(pluginRespirator))
-       }
-
-       @Test
-       fun `plugin respirator is returned as singleton`() {
-               verifySingletonInstance<PluginRespirator>()
+       fun `plugin respirator is not bound`() {
+               expectedException.expect(Exception::class.java)
+               injector.getInstance<PluginRespirator>()
        }
 
        @Test
@@ -50,7 +47,7 @@ class FreenetModuleTest {
 
        @Test
        fun `node is returned as singleton`() {
-               verifySingletonInstance<Node>()
+               injector.verifySingletonInstance<Node>()
        }
 
        @Test
@@ -60,7 +57,7 @@ class FreenetModuleTest {
 
        @Test
        fun `high level simply client is returned as singleton`() {
-               verifySingletonInstance<HighLevelSimpleClient>()
+               injector.verifySingletonInstance<HighLevelSimpleClient>()
        }
 
        @Test
@@ -70,7 +67,7 @@ class FreenetModuleTest {
 
        @Test
        fun `session manager is returned as singleton`() {
-               verifySingletonInstance<SessionManager>()
+               injector.verifySingletonInstance<SessionManager>()
                verify(pluginRespirator).getSessionManager("Sone")
        }
 
@@ -81,7 +78,7 @@ class FreenetModuleTest {
 
        @Test
        fun `toadlet container is returned as singleten`() {
-               verifySingletonInstance<ToadletContainer>()
+               injector.verifySingletonInstance<ToadletContainer>()
        }
 
        @Test
@@ -90,8 +87,30 @@ class FreenetModuleTest {
        }
 
        @Test
-       fun `page maker is returned as singleten`() {
-               verifySingletonInstance<PageMaker>()
+       fun `page maker is returned as singleton`() {
+               injector.verifySingletonInstance<PageMaker>()
+       }
+
+       @Test
+       fun `plugin respirator facade is returned correctly`() {
+               val pluginRespiratorFacade = injector.getInstance<PluginRespiratorFacade>()
+               pluginRespiratorFacade.getPluginTalker(mock(), "test.plugin", "test-request-1")
+               verify(pluginRespirator).getPluginTalker(any(), ArgumentMatchers.eq("test.plugin"), ArgumentMatchers.eq("test-request-1"))
+       }
+
+       @Test
+       fun `plugin respirator facade is returned as singleton`() {
+               injector.verifySingletonInstance<PluginRespiratorFacade>()
+       }
+
+       @Test
+       fun `plugin connector is returned correctly`() {
+               assertThat(injector.getInstance<PluginConnector>(), notNullValue())
+       }
+
+       @Test
+       fun `plugin connector facade is returned as singleton`() {
+               injector.verifySingletonInstance<PluginConnector>()
        }
 
 }
diff --git a/src/test/kotlin/net/pterodactylus/sone/main/SoneModuleCreatorTest.kt b/src/test/kotlin/net/pterodactylus/sone/main/SoneModuleCreatorTest.kt
deleted file mode 100644 (file)
index 0286032..0000000
+++ /dev/null
@@ -1,176 +0,0 @@
-package net.pterodactylus.sone.main
-
-import com.google.common.base.*
-import com.google.common.eventbus.*
-import com.google.inject.*
-import com.google.inject.name.Names.*
-import net.pterodactylus.sone.database.*
-import net.pterodactylus.sone.database.memory.*
-import net.pterodactylus.sone.freenet.wot.*
-import net.pterodactylus.sone.test.*
-import net.pterodactylus.util.config.*
-import net.pterodactylus.util.version.Version
-import org.hamcrest.MatcherAssert.*
-import org.hamcrest.Matchers.*
-import org.junit.*
-import java.io.*
-import java.util.concurrent.atomic.*
-
-class SoneModuleCreatorTest {
-
-       private val currentDir: File = File(".")
-       private val pluginVersion = Version("", 0, 1, 2)
-       private val pluginYear = 2019
-       private val pluginHomepage = "home://page"
-       private val sonePlugin = mock<SonePlugin>().apply {
-               whenever(version).thenReturn(pluginVersion.toString())
-               whenever(year).thenReturn(pluginYear)
-               whenever(homepage).thenReturn(pluginHomepage)
-       }
-
-       @After
-       fun removePropertiesFromCurrentDirectory() {
-               File(currentDir, "sone.properties").delete()
-       }
-
-       @Test
-       fun `creator binds configuration when no file is present`() {
-               File(currentDir, "sone.properties").delete()
-               assertThat(getInstance<Configuration>(), notNullValue())
-       }
-
-       @Test
-       fun `creator binds first start to true when no file is present`() {
-               File(currentDir, "sone.properties").delete()
-               assertThat(getInstance(named("FirstStart")), equalTo(true))
-       }
-
-       @Test
-       fun `config file is created in current directory if not present`() {
-               File(currentDir, "sone.properties").delete()
-               val configuration = getInstance<Configuration>()
-               configuration.save()
-               assertThat(File(currentDir, "sone.properties").exists(), equalTo(true))
-       }
-
-       @Test
-       fun `creator binds configuration when file is present`() {
-               File(currentDir, "sone.properties").writeText("Option=old")
-               assertThat(getInstance<Configuration>().getStringValue("Option").value, equalTo("old"))
-       }
-
-       @Test
-       fun `creator binds first start to false when file is present`() {
-               File(currentDir, "sone.properties").writeText("Option=old")
-               assertThat(getInstance(named("FirstStart")), equalTo(false))
-       }
-
-       @Test
-       fun `invalid config file leads to new config being created`() {
-               File(currentDir, "sone.properties").writeText("Option=old\nbroken")
-               val configuration = getInstance<Configuration>()
-               assertThat(configuration.getStringValue("Option").getValue(null), nullValue())
-       }
-
-       @Test
-       fun `invalid config file leads to new config being set to true`() {
-               File(currentDir, "sone.properties").writeText("Option=old\nbroken")
-               assertThat(getInstance(named("NewConfig")), equalTo(true))
-       }
-
-       @Test
-       fun `valid config file leads to new config being set to false`() {
-               File(currentDir, "sone.properties").writeText("Option=old")
-               assertThat(getInstance(named("NewConfig")), equalTo(false))
-       }
-
-       @Test
-       fun `event bus is bound`() {
-               assertThat(getInstance<EventBus>(), notNullValue())
-       }
-
-       @Test
-       fun `context is bound`() {
-               assertThat(getInstance<Context>().context, equalTo("Sone"))
-       }
-
-       @Test
-       fun `optional context is bound`() {
-               assertThat(getInstance<Optional<Context>>().get().context, equalTo("Sone"))
-       }
-
-       @Test
-       fun `sone plugin is bound`() {
-               assertThat(getInstance(), sameInstance(sonePlugin))
-       }
-
-       @Test
-       fun `version is bound`() {
-               assertThat(getInstance(), equalTo(pluginVersion))
-       }
-
-       @Test
-       fun `plugin version is bound`() {
-               assertThat(getInstance(), equalTo(PluginVersion(pluginVersion.toString())))
-       }
-
-       @Test
-       fun `plugin year is bound`() {
-               assertThat(getInstance(), equalTo(PluginYear(pluginYear)))
-       }
-
-       @Test
-       fun `plugin homepage in bound`() {
-               assertThat(getInstance(), equalTo(PluginHomepage(pluginHomepage)))
-       }
-
-       @Test
-       fun `database is bound correctly`() {
-               assertThat(getInstance<Database>(), instanceOf(MemoryDatabase::class.java))
-       }
-
-       @Test
-       fun `default loader is used without dev options`() {
-               assertThat(getInstance<Loaders>(), instanceOf(DefaultLoaders::class.java))
-       }
-
-       @Test
-       fun `default loaders are used if no path is given`() {
-               File(currentDir, "sone.properties").writeText("Developer.LoadFromFilesystem=true")
-               assertThat(getInstance<Loaders>(), instanceOf(DefaultLoaders::class.java))
-       }
-
-       @Test
-       fun `debug loaders are used if path is given`() {
-               File(currentDir, "sone.properties").writeText("Developer.LoadFromFilesystem=true\nDeveloper.FilesystemPath=/tmp")
-               assertThat(getInstance<Loaders>(), instanceOf(DebugLoaders::class.java))
-       }
-
-       class TestObject {
-               val ref: AtomicReference<Any?> = AtomicReference()
-               @Subscribe
-               fun testEvent(event: Any?) {
-                       ref.set(event)
-               }
-       }
-
-       @Test
-       fun `created objects are registered with event bus`() {
-               val injector = createInjector()
-               val eventBus: EventBus = getInstance(injector = injector)
-               val testObject = getInstance<TestObject>(injector = injector)
-               val event = Any()
-               eventBus.post(event)
-               assertThat(testObject.ref.get(), sameInstance(event))
-       }
-
-       private fun createInjector(): Injector = SoneModuleCreator()
-                       .createModule(sonePlugin)
-                       .let { Guice.createInjector(it) }
-
-       private inline fun <reified R : Any> getInstance(annotation: Annotation? = null, injector: Injector = createInjector()): R =
-                       annotation
-                                       ?.let { injector.getInstance(Key.get(object : TypeLiteral<R>() {}, it)) }
-                                       ?: injector.getInstance(Key.get(object : TypeLiteral<R>() {}))
-
-}
diff --git a/src/test/kotlin/net/pterodactylus/sone/main/SoneModuleTest.kt b/src/test/kotlin/net/pterodactylus/sone/main/SoneModuleTest.kt
new file mode 100644 (file)
index 0000000..01d1285
--- /dev/null
@@ -0,0 +1,241 @@
+package net.pterodactylus.sone.main
+
+import com.codahale.metrics.*
+import com.google.common.base.*
+import com.google.common.eventbus.*
+import com.google.inject.Guice.*
+import com.google.inject.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.*
+import net.pterodactylus.sone.database.memory.*
+import net.pterodactylus.sone.freenet.*
+import net.pterodactylus.sone.freenet.plugin.*
+import net.pterodactylus.sone.freenet.wot.*
+import net.pterodactylus.sone.test.*
+import net.pterodactylus.sone.web.SessionProvider
+import net.pterodactylus.util.config.*
+import net.pterodactylus.util.version.Version
+import org.hamcrest.MatcherAssert.*
+import org.hamcrest.Matchers.*
+import org.junit.experimental.categories.*
+import org.mockito.Mockito.*
+import java.io.*
+import java.util.concurrent.*
+import java.util.concurrent.atomic.*
+import kotlin.test.*
+
+const val versionString = "v80"
+
+@Category(NotParallel::class)
+class SoneModuleTest {
+
+       private val currentDir: File = File(".")
+       private val pluginVersion = Version("", 80)
+       private val pluginYear = 2019
+       private val pluginHomepage = "home://page"
+       private val l10n = deepMock<PluginL10n>()
+       private val sonePlugin = mock<SonePlugin>().apply {
+               whenever(version).thenReturn(versionString)
+               whenever(year).thenReturn(pluginYear)
+               whenever(homepage).thenReturn(pluginHomepage)
+               whenever(l10n()).thenReturn(l10n)
+       }
+
+       private val injector by lazy { createInjector() }
+
+       @AfterTest
+       fun removePropertiesFromCurrentDirectory() {
+               File(currentDir, "sone.properties").delete()
+       }
+
+       @Test
+       fun `creator binds configuration when no file is present`() {
+               File(currentDir, "sone.properties").delete()
+               assertThat(injector.getInstance<Configuration>(), notNullValue())
+       }
+
+       @Test
+       fun `creator binds first start to true when no file is present`() {
+               File(currentDir, "sone.properties").delete()
+               assertThat(injector.getInstance(named("FirstStart")), equalTo(true))
+       }
+
+       @Test
+       fun `config file is created in current directory if not present`() {
+               File(currentDir, "sone.properties").delete()
+               val configuration = injector.getInstance<Configuration>()
+               configuration.save()
+               assertThat(File(currentDir, "sone.properties").exists(), equalTo(true))
+       }
+
+       @Test
+       fun `creator binds configuration when file is present`() {
+               File(currentDir, "sone.properties").writeText("Option=old")
+               assertThat(injector.getInstance<Configuration>().getStringValue("Option").value, equalTo("old"))
+       }
+
+       @Test
+       fun `creator binds first start to false when file is present`() {
+               File(currentDir, "sone.properties").writeText("Option=old")
+               assertThat(injector.getInstance(named("FirstStart")), equalTo(false))
+       }
+
+       @Test
+       fun `invalid config file leads to new config being created`() {
+               File(currentDir, "sone.properties").writeText("Option=old\nbroken")
+               val configuration = injector.getInstance<Configuration>()
+               assertThat(configuration.getStringValue("Option").getValue(null), nullValue())
+       }
+
+       @Test
+       fun `invalid config file leads to new config being set to true`() {
+               File(currentDir, "sone.properties").writeText("Option=old\nbroken")
+               assertThat(injector.getInstance(named("NewConfig")), equalTo(true))
+       }
+
+       @Test
+       fun `valid config file leads to new config being set to false`() {
+               File(currentDir, "sone.properties").writeText("Option=old")
+               assertThat(injector.getInstance(named("NewConfig")), equalTo(false))
+       }
+
+       @Test
+       fun `event bus is bound`() {
+               assertThat(injector.getInstance<EventBus>(), notNullValue())
+       }
+
+       @Test
+       fun `context is bound`() {
+               assertThat(injector.getInstance<Context>().context, equalTo("Sone"))
+       }
+
+       @Test
+       fun `optional context is bound`() {
+               assertThat(injector.getInstance<Optional<Context>>().get().context, equalTo("Sone"))
+       }
+
+       @Test
+       fun `sone plugin is bound`() {
+               assertThat(injector.getInstance(), sameInstance(sonePlugin))
+       }
+
+       @Test
+       fun `version is bound`() {
+               assertThat(injector.getInstance(), equalTo(pluginVersion))
+       }
+
+       @Test
+       fun `plugin version is bound`() {
+               assertThat(injector.getInstance(), equalTo(PluginVersion(versionString)))
+       }
+
+       @Test
+       fun `plugin year is bound`() {
+               assertThat(injector.getInstance(), equalTo(PluginYear(pluginYear)))
+       }
+
+       @Test
+       fun `plugin homepage in bound`() {
+               assertThat(injector.getInstance(), equalTo(PluginHomepage(pluginHomepage)))
+       }
+
+       @Test
+       fun `database is bound correctly`() {
+               assertThat(injector.getInstance<Database>(), instanceOf(MemoryDatabase::class.java))
+       }
+
+       @Test
+       fun `translation is bound correctly`() {
+               assertThat(injector.getInstance<Translation>(), notNullValue())
+       }
+
+       @Test
+       fun `default loader is used without dev options`() {
+               assertThat(injector.getInstance<Loaders>(), instanceOf(DefaultLoaders::class.java))
+       }
+
+       @Test
+       fun `default loaders are used if no path is given`() {
+               File(currentDir, "sone.properties").writeText("Developer.LoadFromFilesystem=true")
+               assertThat(injector.getInstance<Loaders>(), instanceOf(DefaultLoaders::class.java))
+       }
+
+       @Test
+       fun `debug loaders are used if path is given`() {
+               File(currentDir, "sone.properties").writeText("Developer.LoadFromFilesystem=true\nDeveloper.FilesystemPath=/tmp")
+               assertThat(injector.getInstance<Loaders>(), instanceOf(DebugLoaders::class.java))
+       }
+
+       class TestObject {
+               val ref: AtomicReference<Any?> = AtomicReference()
+               @Subscribe
+               fun testEvent(event: Any?) {
+                       ref.set(event)
+               }
+       }
+
+       @Test
+       fun `created objects are registered with event bus`() {
+               val eventBus: EventBus = injector.getInstance()
+               val testObject = injector.getInstance<TestObject>()
+               val event = Any()
+               eventBus.post(event)
+               assertThat(testObject.ref.get(), sameInstance(event))
+       }
+
+       @Test
+       fun `core is created as singleton`() {
+               injector.verifySingletonInstance<Core>()
+       }
+
+       @Test
+       fun `core is registered with event bus`() {
+               val eventBus = mock<EventBus>()
+               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>()
+       }
+
+       @Test
+       fun `wot connector is created as singleton`() {
+               injector.verifySingletonInstance<WebOfTrustConnector>()
+       }
+
+       @Test
+       fun `notification ticker is created as singleton`() {
+               injector.verifySingletonInstance<ScheduledExecutorService>(named("notification"))
+       }
+
+       @Test
+       fun `ticker shutdown is created as singleton`() {
+               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 6d57b9f..d1e2cec 100644 (file)
@@ -1,19 +1,34 @@
 package net.pterodactylus.sone.main
 
-import freenet.client.async.USKManager
-import freenet.l10n.BaseL10n.LANGUAGE.ENGLISH
-import freenet.node.Node
-import freenet.node.NodeClientCore
-import freenet.pluginmanager.PluginRespirator
+import com.google.common.eventbus.*
+import com.google.inject.*
+import freenet.client.async.*
+import freenet.l10n.BaseL10n.LANGUAGE.*
+import freenet.node.*
+import freenet.pluginmanager.*
+import net.pterodactylus.sone.core.*
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.sone.fcp.*
+import net.pterodactylus.sone.freenet.wot.*
 import net.pterodactylus.sone.test.*
-import org.junit.Test
+import net.pterodactylus.sone.web.*
+import net.pterodactylus.sone.web.notification.*
+import org.hamcrest.MatcherAssert.*
+import org.hamcrest.Matchers.*
+import org.junit.experimental.categories.*
+import org.mockito.Mockito.*
+import java.io.*
+import java.util.concurrent.atomic.*
+import kotlin.test.*
 
 /**
  * Unit test for [SonePlugin].
  */
+@Dirty
+@Category(NotParallel::class)
 class SonePluginTest {
 
-       private val sonePlugin = SonePlugin()
+       private var sonePlugin = SonePlugin { injector }
        private val pluginRespirator = deepMock<PluginRespirator>()
        private val node = deepMock<Node>()
        private val clientCore = deepMock<NodeClientCore>()
@@ -31,4 +46,204 @@ class SonePluginTest {
                sonePlugin.runPlugin(pluginRespirator)
        }
 
+       @Test
+       fun `core can be created`() {
+               val injector: Injector = runSonePluginWithRealInjector()
+               assertThat(injector.getInstance<Core>(), notNullValue())
+       }
+
+       @Test
+       fun `fcp interface can be created`() {
+               val injector: Injector = runSonePluginWithRealInjector()
+               assertThat(injector.getInstance<FcpInterface>(), notNullValue())
+       }
+
+       @Test
+       fun `web interface can be created`() {
+               val injector: Injector = runSonePluginWithRealInjector()
+               assertThat(injector.getInstance<WebInterface>(), notNullValue())
+       }
+
+       @Test
+       fun `web of trust connector can be created`() {
+               val injector: Injector = runSonePluginWithRealInjector()
+               assertThat(injector.getInstance<WebOfTrustConnector>(), notNullValue())
+       }
+
+       @Test
+       fun `notification handler can be created`() {
+               val injector: Injector = runSonePluginWithRealInjector()
+               assertThat(injector.getInstance<NotificationHandler>(), notNullValue())
+       }
+
+       private fun runSonePluginWithRealInjector(injectorConsumer: (Injector) -> Unit = {}): Injector {
+               lateinit var injector: Injector
+               sonePlugin = SonePlugin {
+                       Guice.createInjector(*it).also {
+                               injector = it
+                               injectorConsumer(it)
+                       }
+               }
+               sonePlugin.setLanguage(ENGLISH)
+               sonePlugin.runPlugin(pluginRespirator)
+               return injector
+       }
+
+       @Test
+       fun `core is being started`() {
+               sonePlugin.runPlugin(pluginRespirator)
+               val core = injector.getInstance<Core>()
+               verify(core).start()
+       }
+
+       @Test
+       fun `notification handler is being requested`() {
+               sonePlugin.runPlugin(pluginRespirator)
+               assertThat(getInjected(NotificationHandler::class.java), notNullValue())
+       }
+
+       @Test
+       fun `ticker shutdown is being requested`() {
+               sonePlugin.runPlugin(pluginRespirator)
+               assertThat(getInjected(TickerShutdown::class.java), notNullValue())
+       }
+
+       private class FirstStartListener(private val firstStartReceived: AtomicBoolean) {
+               @Subscribe
+               fun firstStart(@Suppress("UNUSED_PARAMETER") firstStart: FirstStart) {
+                       firstStartReceived.set(true)
+               }
+       }
+
+       @Test
+       fun `first-start event is sent to event bus when first start is true`() {
+               File("sone.properties").delete()
+               val firstStartReceived = AtomicBoolean()
+               runSonePluginWithRealInjector {
+                       val eventBus = it.getInstance(EventBus::class.java)
+                       eventBus.register(FirstStartListener(firstStartReceived))
+               }
+               assertThat(firstStartReceived.get(), equalTo(true))
+       }
+
+       @Test
+       fun `first-start event is not sent to event bus when first start is false`() {
+               File("sone.properties").deleteAfter {
+                       writeText("# empty")
+                       val firstStartReceived = AtomicBoolean()
+                       runSonePluginWithRealInjector {
+                               val eventBus = it.getInstance(EventBus::class.java)
+                               eventBus.register(FirstStartListener(firstStartReceived))
+                       }
+                       assertThat(firstStartReceived.get(), equalTo(false))
+               }
+       }
+
+       private class ConfigNotReadListener(private val configNotReadReceiver: AtomicBoolean) {
+               @Subscribe
+               fun configNotRead(@Suppress("UNUSED_PARAMETER") configNotRead: ConfigNotRead) {
+                       configNotReadReceiver.set(true)
+               }
+       }
+
+       @Test
+       fun `config-not-read event is sent to event bus when new config is true`() {
+               File("sone.properties").deleteAfter {
+                       writeText("Invalid")
+                       val configNotReadReceived = AtomicBoolean()
+                       runSonePluginWithRealInjector {
+                               val eventBus = it.getInstance(EventBus::class.java)
+                               eventBus.register(ConfigNotReadListener(configNotReadReceived))
+                       }
+                       assertThat(configNotReadReceived.get(), equalTo(true))
+               }
+       }
+
+       @Test
+       fun `config-not-read event is not sent to event bus when first start is true`() {
+               File("sone.properties").delete()
+               val configNotReadReceived = AtomicBoolean()
+               runSonePluginWithRealInjector {
+                       val eventBus = it.getInstance(EventBus::class.java)
+                       eventBus.register(ConfigNotReadListener(configNotReadReceived))
+               }
+               assertThat(configNotReadReceived.get(), equalTo(false))
+       }
+
+       @Test
+       fun `config-not-read event is not sent to event bus when new config is false`() {
+               File("sone.properties").deleteAfter {
+                       writeText("# comment")
+                       val configNotReadReceived = AtomicBoolean()
+                       runSonePluginWithRealInjector {
+                               val eventBus = it.getInstance(EventBus::class.java)
+                               eventBus.register(ConfigNotReadListener(configNotReadReceived))
+                       }
+                       assertThat(configNotReadReceived.get(), equalTo(false))
+               }
+       }
+
+       private class StartupListener(private val startupReceived: () -> Unit) {
+               @Subscribe
+               fun startup(@Suppress("UNUSED_PARAMETER") startup: Startup) {
+                       startupReceived()
+               }
+       }
+
+       @Test
+       fun `startup event is sent to event bus`() {
+               val startupReceived = AtomicBoolean()
+               runSonePluginWithRealInjector {
+                       val eventBus = it.getInstance(EventBus::class.java)
+                       eventBus.register(StartupListener { startupReceived.set(true) })
+               }
+               assertThat(startupReceived.get(), equalTo(true))
+       }
+
+       private class ShutdownListener(private val shutdownReceived: () -> Unit) {
+               @Subscribe
+               fun shutdown(@Suppress("UNUSED_PARAMETER") shutdown: Shutdown) {
+                       shutdownReceived()
+               }
+       }
+
+       @Test
+       fun `shutdown event is sent to event bus on terminate`() {
+               val shutdownReceived = AtomicBoolean()
+               runSonePluginWithRealInjector {
+                       val eventBus = it.getInstance(EventBus::class.java)
+                       eventBus.register(ShutdownListener { shutdownReceived.set(true) })
+               }
+               sonePlugin.terminate()
+               assertThat(shutdownReceived.get(), equalTo(true))
+       }
+
+       private fun <T> getInjected(clazz: Class<T>, annotation: Annotation? = null): T? =
+                       injected[TypeLiteral.get(clazz) to annotation] as? T
+
+       private val injected =
+                       mutableMapOf<Pair<TypeLiteral<*>, Annotation?>, Any>()
+
+       private val injector = mock<Injector>().apply {
+               fun mockValue(clazz: Class<*>) = false.takeIf { clazz.name == java.lang.Boolean::class.java.name } ?: mock(clazz)
+               whenever(getInstance(any<Key<*>>())).then {
+                       injected.getOrPut((it.getArgument(0) as Key<*>).let { it.typeLiteral to it.annotation }) {
+                               it.getArgument<Key<*>>(0).typeLiteral.type.typeName.toClass().let(::mockValue)
+                       }
+               }
+               whenever(getInstance(any<Class<*>>())).then {
+                       injected.getOrPut(TypeLiteral.get(it.getArgument(0) as Class<*>) to null) {
+                               it.getArgument<Class<*>>(0).let(::mockValue)
+                       }
+               }
+       }
+
+}
+
+private fun String.toClass(): Class<*> = SonePlugin::class.java.classLoader.loadClass(this)
+
+private fun File.deleteAfter(action: File.() -> Unit) = try {
+       action(this)
+} finally {
+       this.delete()
 }
diff --git a/src/test/kotlin/net/pterodactylus/sone/main/TickerShutdownTest.kt b/src/test/kotlin/net/pterodactylus/sone/main/TickerShutdownTest.kt
new file mode 100644 (file)
index 0000000..827131f
--- /dev/null
@@ -0,0 +1,46 @@
+/**
+ * Sone - TickerShutdownTest.kt - Copyright © 2019–2020 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.main
+
+import com.google.common.eventbus.*
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.sone.test.*
+import org.mockito.Mockito.*
+import java.util.concurrent.*
+import kotlin.test.*
+
+/**
+ * Unit test for [TickerShutdown].
+ */
+@Suppress("UnstableApiUsage")
+class TickerShutdownTest {
+
+       private val eventBus = EventBus()
+       private val notificationTicker = mock<ScheduledExecutorService>()
+
+       init {
+               eventBus.register(TickerShutdown(notificationTicker))
+       }
+
+       @Test
+       fun `ticker is shutdown on shutdown`() {
+               eventBus.post(Shutdown())
+               verify(notificationTicker).shutdown()
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/notify/ListNotificationTest.kt b/src/test/kotlin/net/pterodactylus/sone/notify/ListNotificationTest.kt
new file mode 100644 (file)
index 0000000..4c82035
--- /dev/null
@@ -0,0 +1,125 @@
+package net.pterodactylus.sone.notify
+
+import net.pterodactylus.util.notify.*
+import net.pterodactylus.util.template.*
+import org.hamcrest.MatcherAssert.*
+import org.hamcrest.Matchers.*
+import org.junit.*
+import java.util.concurrent.atomic.*
+
+/**
+ * Unit test for [ListNotification].
+ */
+class ListNotificationTest {
+
+       private val template = Template()
+       private val listNotification = ListNotification<String>(ID, KEY, template)
+
+       @Test
+       fun `creating a list notification sets empty iterable on element key in template context`() {
+               assertThat(template.initialContext.get(KEY) as Iterable<*>, emptyIterable())
+       }
+
+       @Test
+       @Suppress("UNCHECKED_CAST")
+       fun `list in template context gets updated when elements are added`() {
+               listNotification.add("a")
+               listNotification.add("b")
+               assertThat(template.initialContext.get(KEY) as Iterable<String>, contains("a", "b"))
+       }
+
+       @Test
+       fun `new list notification has no element`() {
+               assertThat(listNotification.elements, emptyIterable())
+       }
+
+       @Test
+       fun `new list notification is empty`() {
+               assertThat(listNotification.isEmpty, equalTo(true))
+       }
+
+       @Test
+       fun `list notification retains set elements`() {
+               listNotification.setElements(listOf("a", "b", "c"))
+               assertThat(listNotification.elements, contains("a", "b", "c"))
+       }
+
+       @Test
+       fun `list notification deduplicates set elements`() {
+               listNotification.setElements(listOf("a", "b", "a"))
+               assertThat(listNotification.elements, contains("a", "b"))
+       }
+
+       @Test
+       fun `list notification retains added elements`() {
+               listNotification.add("a")
+               listNotification.add("b")
+               listNotification.add("c")
+               assertThat(listNotification.elements, contains("a", "b", "c"))
+       }
+
+       @Test
+       fun `list notification deduplicates elements`() {
+               listNotification.add("a")
+               listNotification.add("b")
+               listNotification.add("a")
+               assertThat(listNotification.elements, contains("a", "b"))
+       }
+
+       @Test
+       fun `list notification removes correct element`() {
+               listNotification.setElements(listOf("a", "b", "c"))
+               listNotification.remove("b")
+               assertThat(listNotification.elements, contains("a", "c"))
+       }
+
+       @Test
+       fun `removing the last element dismisses the notification`() {
+               val notificationDismissed = AtomicBoolean()
+               val notificationListener = NotificationListener { notificationDismissed.set(it == listNotification) }
+               listNotification.addNotificationListener(notificationListener)
+               listNotification.add("a")
+               listNotification.remove("a")
+               assertThat(notificationDismissed.get(), equalTo(true))
+       }
+
+       @Test
+       fun `dismissing the list notification removes all elements`() {
+               listNotification.setElements(listOf("a", "b", "c"))
+               listNotification.dismiss()
+               assertThat(listNotification.elements, emptyIterable())
+       }
+
+       @Test
+       fun `list notification with different elements is not equal`() {
+               val secondNotification = ListNotification<String>(ID, KEY, template)
+               listNotification.add("a")
+               secondNotification.add("b")
+               assertThat(listNotification, not(equalTo(secondNotification)))
+       }
+
+       @Test
+       fun `list notification with different key is not equal`() {
+               val secondNotification = ListNotification<String>(ID, OTHER_KEY, template)
+               assertThat(listNotification, not(equalTo(secondNotification)))
+       }
+
+       @Test
+       fun `copied notifications have the same hash code`() {
+               val secondNotification = ListNotification(listNotification)
+               listNotification.add("a")
+               secondNotification.add("a")
+               listNotification.setLastUpdateTime(secondNotification.lastUpdatedTime)
+               assertThat(listNotification.hashCode(), equalTo(secondNotification.hashCode()))
+       }
+
+       @Test
+       fun `list notification is not equal to other objects`() {
+               assertThat(listNotification, not(equalTo(Any())))
+       }
+
+}
+
+private const val ID = "notification-id"
+private const val KEY = "element-key"
+private const val OTHER_KEY = "other-key"
diff --git a/src/test/kotlin/net/pterodactylus/sone/template/DurationFormatFilterTest.kt b/src/test/kotlin/net/pterodactylus/sone/template/DurationFormatFilterTest.kt
new file mode 100644 (file)
index 0000000..0af4a8e
--- /dev/null
@@ -0,0 +1,98 @@
+/**
+ * Sone - DurationFormatFilterTest.kt - Copyright © 2019–2020 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.template
+
+import org.hamcrest.MatcherAssert.*
+import org.hamcrest.Matchers.*
+import kotlin.test.*
+
+class DurationFormatFilterTest {
+
+       private val filter = DurationFormatFilter()
+
+       @Test
+       fun `random object is returned as it is`() {
+               val randomObject = Any()
+               assertThat(filter.format(null, randomObject, emptyMap()), sameInstance(randomObject))
+       }
+
+       @Test
+       fun `integer 0 is rendered as “0s”`() {
+               verifyDuration(0, "0s")
+       }
+
+       @Test
+       fun `long 0 is rendered as “0s”`() {
+               verifyDuration(0L, "0s")
+       }
+
+       @Test
+       fun `12 is rendered as “12_0s”`() {
+               verifyDuration(12, "12.0s")
+       }
+
+       @Test
+       fun `123 is rendered as “2_1m”`() {
+               verifyDuration(123, "2.1m")
+       }
+
+       @Test
+       fun `12345 is rendered as “3_4h”`() {
+               verifyDuration(12345, "3.4h")
+       }
+
+       @Test
+       fun `123456 is rendered as “1_4d”`() {
+               verifyDuration(123456, "1.4d")
+       }
+
+       @Test
+       fun `1234567 is rendered as “2_0w”`() {
+               verifyDuration(1234567, "2.0w")
+       }
+
+       @Test
+       fun `123456789 with scale ms is rendered as “1_4d”`() {
+               verifyDuration(123456789, "1.4d", "ms")
+       }
+
+       @Test
+       fun `123456789 with scale μs is rendered as “2_1m”`() {
+               verifyDuration(123456789, "2.1m", "μs")
+       }
+
+       @Test
+       fun `123456789 with scale ns is rendered as “123_5ms”`() {
+               verifyDuration(123456789, "123.5ms", "ns")
+       }
+
+       @Test
+       fun `123456 with scale ns is rendered as “123_5μs”`() {
+               verifyDuration(123456, "123.5μs", "ns")
+       }
+
+       @Test
+       fun `123 with scale ns is rendered as “123_0ns”`() {
+               verifyDuration(123, "123.0ns", "ns")
+       }
+
+       private fun verifyDuration(value: Any, expectedRendering: String, scale: String? = null) {
+               assertThat(filter.format(null, value, scale?.let { mapOf("scale" to scale) } ?: emptyMap()), equalTo<Any>(expectedRendering))
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/template/FilesystemTemplateTest.kt b/src/test/kotlin/net/pterodactylus/sone/template/FilesystemTemplateTest.kt
new file mode 100644 (file)
index 0000000..f6c704c
--- /dev/null
@@ -0,0 +1,117 @@
+package net.pterodactylus.sone.template
+
+import net.pterodactylus.util.template.*
+import org.hamcrest.MatcherAssert.*
+import org.hamcrest.Matchers.*
+import org.junit.*
+import org.junit.rules.*
+import java.io.*
+import java.lang.Thread.*
+import kotlin.test.Test
+
+/**
+ * Unit test for [FilesystemTemplate].
+ */
+class FilesystemTemplateTest() {
+
+       @Rule
+       @JvmField
+       val expectedException: ExpectedException = ExpectedException.none()
+
+       private val tempFile = File.createTempFile("template-", ".dat")
+       private val filesystemTemplate: FilesystemTemplate
+       private val templateContext = TemplateContext()
+
+       private val renderedString: String
+               get() =
+                       StringWriter().use { stringWriter ->
+                               filesystemTemplate.render(templateContext, stringWriter)
+                               stringWriter
+                       }.toString()
+
+       init {
+               writeTemplate("Text")
+               filesystemTemplate = FilesystemTemplate(tempFile.absolutePath)
+       }
+
+       private fun writeTemplate(text: String) {
+               tempFile.writer().use {
+                       it.write("$text.<%foreach values value><% value><%/foreach>")
+               }
+       }
+
+       @Before
+       fun setupTemplateContext() {
+               templateContext.set("values", listOf("a", 1))
+       }
+
+       @Test
+       fun `loading template from non existing file throws exception`() {
+               val filesystemTemplate = FilesystemTemplate("/a/b/c.dat")
+               expectedException.expect(FilesystemTemplate.TemplateFileNotFoundException::class.java)
+               filesystemTemplate.initialContext
+       }
+
+       @Test
+       fun `template can be loaded from the filesystem`() {
+               assertThat(renderedString, equalTo("Text.a1"))
+       }
+
+       @Test
+       fun `template can be reloaded`() {
+               assertThat(renderedString, equalTo("Text.a1"))
+               sleep(1000)
+               writeTemplate("New")
+               assertThat(renderedString, equalTo("New.a1"))
+       }
+
+       @Test
+       fun `template is not reloaded if not changed`() {
+               assertThat(renderedString, equalTo("Text.a1"))
+               assertThat(renderedString, equalTo("Text.a1"))
+       }
+
+       @Test
+       fun `initial context is copied to reloaded templates`() {
+               filesystemTemplate.initialContext.set("values", "test")
+               sleep(1000)
+               writeTemplate("New")
+               assertThat(filesystemTemplate.initialContext.get("values"), equalTo("test" as Any))
+       }
+
+       @Test
+       fun `parts are copied to currently loaded templates`() {
+               writeTemplate("New")
+               renderedString
+               filesystemTemplate.add { _, writer ->
+                       writer.write(".Test")
+               }
+               assertThat(renderedString, equalTo("New.a1.Test"))
+       }
+
+       @Test
+       fun `parts are copied to reloaded templates`() {
+               filesystemTemplate.add { _, writer ->
+                       writer.write(".Test")
+               }
+               sleep(1000)
+               writeTemplate("New")
+               assertThat(renderedString, equalTo("New.a1.Test"))
+       }
+
+       @Test
+       fun `column of returned template is returned as zero`() {
+               assertThat(filesystemTemplate.column, equalTo(0))
+       }
+
+       @Test
+       fun `line of returned template is returned as zero`() {
+               assertThat(filesystemTemplate.line, equalTo(0))
+       }
+
+       @Test
+       fun `template can be iterated over`() {
+               assertThat<Iterator<Part>>(filesystemTemplate.iterator(), notNullValue())
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/template/HistogramRendererTest.kt b/src/test/kotlin/net/pterodactylus/sone/template/HistogramRendererTest.kt
new file mode 100644 (file)
index 0000000..bc87086
--- /dev/null
@@ -0,0 +1,204 @@
+/**
+ * Sone - HistogramRendererTest.kt - Copyright © 2019–2020 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.template
+
+import com.codahale.metrics.*
+import net.pterodactylus.sone.freenet.*
+import net.pterodactylus.util.template.*
+import org.hamcrest.MatcherAssert.*
+import org.hamcrest.Matchers.*
+import org.jsoup.*
+import org.jsoup.nodes.*
+import org.junit.*
+import java.util.*
+
+/**
+ * Unit test for [HistogramRenderer].
+ */
+class HistogramRendererTest {
+
+       private val translation = object : Translation {
+               override val currentLocale = Locale.ENGLISH
+               override fun translate(key: String) = "Metric Name".takeIf { key == "Page.Metrics.TestHistogram.Title" } ?: ""
+       }
+       private val metricRenderer = HistogramRenderer()
+       private val templateContext = TemplateContext().apply {
+               addFilter("html", HtmlFilter())
+               addFilter("duration", DurationFormatFilter())
+               addFilter("l10n", L10nFilter(translation))
+       }
+
+       @Test
+       fun `histogram is rendered as table row`() {
+               createAndVerifyTableRow {
+                       assertThat(it.nodeName(), equalTo("tr"))
+               }
+       }
+
+       @Test
+       fun `histogram has eleven columns`() {
+               createAndVerifyTableRow {
+                       assertThat(it.getElementsByTag("td"), hasSize(11))
+               }
+       }
+
+       @Test
+       fun `first column contains translated metric name`() {
+               createAndVerifyTableRow(mapOf("name" to "test.histogram")) {
+                       assertThat(it.getElementsByTag("td")[0].text(), equalTo("Metric Name"))
+               }
+       }
+
+       @Test
+       fun `second column is numeric`() {
+               verifyColumnIsNumeric(1)
+       }
+
+       @Test
+       fun `second column contains count`() {
+               createAndVerifyTableRow {
+                       assertThat(it.getElementsByTag("td")[1].text(), equalTo("2001"))
+               }
+       }
+
+       @Test
+       fun `third column is numeric`() {
+               verifyColumnIsNumeric(2)
+       }
+
+       @Test
+       fun `third column contains min value`() {
+               createAndVerifyTableRow {
+                       assertThat(it.getElementsByTag("td")[2].text(), equalTo("2.0ms"))
+               }
+       }
+
+       @Test
+       fun `fourth column is numeric`() {
+               verifyColumnIsNumeric(3)
+       }
+
+       @Test
+       fun `fourth column contains max value`() {
+               createAndVerifyTableRow {
+                       assertThat(it.getElementsByTag("td")[3].text(), equalTo("998.0ms"))
+               }
+       }
+
+       @Test
+       fun `fifth column is numeric`() {
+               verifyColumnIsNumeric(4)
+       }
+
+       @Test
+       fun `fifth column contains mean value`() {
+               createAndVerifyTableRow {
+                       assertThat(it.getElementsByTag("td")[4].text(), equalTo("492.7ms"))
+               }
+       }
+
+       @Test
+       fun `sixth column is numeric`() {
+               verifyColumnIsNumeric(5)
+       }
+
+       @Test
+       fun `sixth column contains median value`() {
+               createAndVerifyTableRow {
+                       assertThat(it.getElementsByTag("td")[5].text(), equalTo("483.6ms"))
+               }
+       }
+
+       @Test
+       fun `seventh column is numeric`() {
+               verifyColumnIsNumeric(6)
+       }
+
+       @Test
+       fun `seventh column contains 75th percentile`() {
+               createAndVerifyTableRow {
+                       assertThat(it.getElementsByTag("td")[6].text(), equalTo("740.9ms"))
+               }
+       }
+
+       @Test
+       fun `eighth column is numeric`() {
+               verifyColumnIsNumeric(7)
+       }
+
+       @Test
+       fun `eighth column contains 95th percentile`() {
+               createAndVerifyTableRow {
+                       assertThat(it.getElementsByTag("td")[7].text(), equalTo("940.9ms"))
+               }
+       }
+
+       @Test
+       fun `ninth column is numeric`() {
+               verifyColumnIsNumeric(8)
+       }
+
+       @Test
+       fun `ninth column contains 98th percentile`() {
+               createAndVerifyTableRow {
+                       assertThat(it.getElementsByTag("td")[8].text(), equalTo("975.6ms"))
+               }
+       }
+
+       @Test
+       fun `tenth column is numeric`() {
+               verifyColumnIsNumeric(9)
+       }
+
+       @Test
+       fun `tenth column contains 99th percentile`() {
+               createAndVerifyTableRow {
+                       assertThat(it.getElementsByTag("td")[9].text(), equalTo("991.6ms"))
+               }
+       }
+
+       @Test
+       fun `eleventh column is numeric`() {
+               verifyColumnIsNumeric(10)
+       }
+
+       @Test
+       fun `eleventh column contains 99,9th percentile`() {
+               createAndVerifyTableRow {
+                       assertThat(it.getElementsByTag("td")[10].text(), equalTo("998.0ms"))
+               }
+       }
+
+       private fun createAndVerifyTableRow(parameters: Map<String, Any?>? = null, verify: (Element) -> Unit) =
+                       metricRenderer.format(templateContext, histogram, parameters)
+                                       .let { "<table id='t'>$it</table>" }
+                                       .let(Jsoup::parseBodyFragment)
+                                       .getElementById("t").child(0).child(0)
+                                       .let(verify)
+
+       private fun verifyColumnIsNumeric(column: Int) =
+                       createAndVerifyTableRow {
+                               assertThat(it.getElementsByTag("td")[column].classNames(), hasItem("numeric"))
+                       }
+
+}
+
+private val random = Random(1)
+private val histogram = MetricRegistry().histogram("test.histogram") { Histogram(SlidingWindowReservoir(1028)) }.apply {
+       (0..2000).map { random.nextInt(1_000_000) }.forEach(this::update)
+}
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 1e83b85..32e8587 100644 (file)
@@ -10,7 +10,7 @@ import net.pterodactylus.sone.data.SoneOptions.LoadExternalContent.MANUALLY_TRUS
 import net.pterodactylus.sone.data.SoneOptions.LoadExternalContent.TRUSTED
 import net.pterodactylus.sone.freenet.wot.OwnIdentity
 import net.pterodactylus.sone.freenet.wot.Trust
-import net.pterodactylus.sone.test.mock
+import net.pterodactylus.sone.test.*
 import net.pterodactylus.sone.text.FreenetLinkPart
 import net.pterodactylus.sone.text.LinkPart
 import net.pterodactylus.sone.text.Part
@@ -21,7 +21,6 @@ import org.hamcrest.Matchers.contains
 import org.hamcrest.Matchers.emptyIterable
 import org.junit.Before
 import org.junit.Test
-import org.mockito.Mockito.`when`
 
 /**
  * Unit test for [LinkedElementsFilter].
@@ -44,14 +43,14 @@ class LinkedElementsFilterTest {
 
        @Before
        fun setupSone() {
-               `when`(sone.options).thenReturn(DefaultSoneOptions())
+               whenever(sone.options).thenReturn(DefaultSoneOptions())
        }
 
        @Before
        fun setupImageLoader() {
-               `when`(imageLoader.loadElement("KSK@link")).thenReturn(LinkedElement("KSK@link", failed = true))
-               `when`(imageLoader.loadElement("KSK@loading.png")).thenReturn(LinkedElement("KSK@loading.png", loading = true))
-               `when`(imageLoader.loadElement("KSK@link.png")).thenReturn(LinkedElement("KSK@link.png"))
+               whenever(imageLoader.loadElement("KSK@link")).thenReturn(LinkedElement("KSK@link", failed = true))
+               whenever(imageLoader.loadElement("KSK@loading.png")).thenReturn(LinkedElement("KSK@loading.png", loading = true))
+               whenever(imageLoader.loadElement("KSK@link.png")).thenReturn(LinkedElement("KSK@link.png"))
        }
 
        @Test
@@ -89,7 +88,7 @@ class LinkedElementsFilterTest {
        fun `filter finds images if the remote sone is local`() {
                sone.options.loadLinkedImages = MANUALLY_TRUSTED
                templateContext.set("currentSone", sone)
-               `when`(remoteSone.isLocal).thenReturn(true)
+               whenever(remoteSone.isLocal).thenReturn(true)
                parameters["sone"] = remoteSone
                verifyThatImagesArePresent()
        }
@@ -106,7 +105,7 @@ class LinkedElementsFilterTest {
        fun `filter does not find images if local sone requires manual trust and remote sone has only implicit trust`() {
                sone.options.loadLinkedImages = MANUALLY_TRUSTED
                templateContext.set("currentSone", sone)
-               `when`(remoteSone.identity.getTrust(this.sone.identity as OwnIdentity)).thenReturn(Trust(null, 100, null))
+               whenever(remoteSone.identity.getTrust(this.sone.identity as OwnIdentity)).thenReturn(Trust(null, 100, null))
                parameters["sone"] = remoteSone
                verifyThatImagesAreNotPresent()
        }
@@ -115,7 +114,7 @@ class LinkedElementsFilterTest {
        fun `filter does not find images if local sone requires manual trust and remote sone has explicit trust of zero`() {
                sone.options.loadLinkedImages = MANUALLY_TRUSTED
                templateContext.set("currentSone", sone)
-               `when`(remoteSone.identity.getTrust(this.sone.identity as OwnIdentity)).thenReturn(Trust(0, null, null))
+               whenever(remoteSone.identity.getTrust(this.sone.identity as OwnIdentity)).thenReturn(Trust(0, null, null))
                parameters["sone"] = remoteSone
                verifyThatImagesAreNotPresent()
        }
@@ -124,7 +123,7 @@ class LinkedElementsFilterTest {
        fun `filter finds images if local sone requires manual trust and remote sone has explicit trust of one`() {
                sone.options.loadLinkedImages = MANUALLY_TRUSTED
                templateContext.set("currentSone", sone)
-               `when`(remoteSone.identity.getTrust(this.sone.identity as OwnIdentity)).thenReturn(Trust(1, null, null))
+               whenever(remoteSone.identity.getTrust(this.sone.identity as OwnIdentity)).thenReturn(Trust(1, null, null))
                parameters["sone"] = remoteSone
                verifyThatImagesArePresent()
        }
@@ -140,7 +139,7 @@ class LinkedElementsFilterTest {
        @Test
        fun `filter finds images if local sone requires following and remote sone is followed`() {
            sone.options.loadLinkedImages = FOLLOWED
-               `when`(sone.hasFriend("remote-id")).thenReturn(true)
+               whenever(sone.hasFriend("remote-id")).thenReturn(true)
                templateContext["currentSone"] = sone
                parameters["sone"] = remoteSone
                verifyThatImagesArePresent()
@@ -158,7 +157,7 @@ class LinkedElementsFilterTest {
        fun `filter finds images if following is required and remote sone is a local sone`() {
                sone.options.loadLinkedImages = FOLLOWED
                templateContext["currentSone"] = sone
-               `when`(remoteSone.isLocal).thenReturn(true)
+               whenever(remoteSone.isLocal).thenReturn(true)
                parameters["sone"] = remoteSone
                verifyThatImagesArePresent()
        }
@@ -175,7 +174,7 @@ class LinkedElementsFilterTest {
        fun `filter does not find images if any trust is required and remote sone has implicit trust of zero`() {
            sone.options.loadLinkedImages = TRUSTED
                templateContext["currentSone"] = sone
-               `when`(remoteSone.identity.getTrust(sone.identity as OwnIdentity)).thenReturn(Trust(null, 0, null))
+               whenever(remoteSone.identity.getTrust(sone.identity as OwnIdentity)).thenReturn(Trust(null, 0, null))
                parameters["sone"] = remoteSone
                verifyThatImagesAreNotPresent()
        }
@@ -184,7 +183,7 @@ class LinkedElementsFilterTest {
        fun `filter finds images if any trust is required and remote sone has implicit trust of one`() {
            sone.options.loadLinkedImages = TRUSTED
                templateContext["currentSone"] = sone
-               `when`(remoteSone.identity.getTrust(sone.identity as OwnIdentity)).thenReturn(Trust(null, 1, null))
+               whenever(remoteSone.identity.getTrust(sone.identity as OwnIdentity)).thenReturn(Trust(null, 1, null))
                parameters["sone"] = remoteSone
                verifyThatImagesArePresent()
        }
@@ -193,7 +192,7 @@ class LinkedElementsFilterTest {
        fun `filter does not find images if any trust is required and remote sone has explicit trust of zero but implicit trust of one`() {
                sone.options.loadLinkedImages = TRUSTED
                templateContext["currentSone"] = sone
-               `when`(remoteSone.identity.getTrust(sone.identity as OwnIdentity)).thenReturn(Trust(0, 1, null))
+               whenever(remoteSone.identity.getTrust(sone.identity as OwnIdentity)).thenReturn(Trust(0, 1, null))
                parameters["sone"] = remoteSone
                verifyThatImagesAreNotPresent()
        }
@@ -202,7 +201,7 @@ class LinkedElementsFilterTest {
        fun `filter finds images if any trust is required and remote sone has explicit trust of one but no implicit trust`() {
                sone.options.loadLinkedImages = TRUSTED
                templateContext["currentSone"] = sone
-               `when`(remoteSone.identity.getTrust(sone.identity as OwnIdentity)).thenReturn(Trust(1, null, null))
+               whenever(remoteSone.identity.getTrust(sone.identity as OwnIdentity)).thenReturn(Trust(1, null, null))
                parameters["sone"] = remoteSone
                verifyThatImagesArePresent()
        }
@@ -211,7 +210,7 @@ class LinkedElementsFilterTest {
        fun `filter finds images if any trust is required and remote sone is a local sone`() {
                sone.options.loadLinkedImages = TRUSTED
                templateContext["currentSone"] = sone
-               `when`(remoteSone.isLocal).thenReturn(true)
+               whenever(remoteSone.isLocal).thenReturn(true)
                parameters["sone"] = remoteSone
                verifyThatImagesArePresent()
        }
@@ -238,9 +237,9 @@ class LinkedElementsFilterTest {
 
        private fun createSone(id: String = "sone-id"): Sone {
                val sone = mock<Sone>()
-               `when`(sone.id).thenReturn(id)
-               `when`(sone.options).thenReturn(DefaultSoneOptions())
-               `when`(sone.identity).thenReturn(mock<OwnIdentity>())
+               whenever(sone.id).thenReturn(id)
+               whenever(sone.options).thenReturn(DefaultSoneOptions())
+               whenever(sone.identity).thenReturn(mock<OwnIdentity>())
                return sone
        }
 
index 0e44568..5864a6a 100644 (file)
@@ -3,20 +3,17 @@ package net.pterodactylus.sone.template
 import com.google.inject.Guice
 import net.pterodactylus.sone.core.Core
 import net.pterodactylus.sone.data.Sone
-import net.pterodactylus.sone.test.getInstance
-import net.pterodactylus.sone.test.isProvidedByMock
-import net.pterodactylus.sone.test.mock
+import net.pterodactylus.sone.test.*
 import net.pterodactylus.sone.text.SoneTextParser
 import net.pterodactylus.sone.text.SoneTextParserContext
 import net.pterodactylus.util.template.TemplateContext
 import org.hamcrest.MatcherAssert.assertThat
-import org.hamcrest.Matchers.`is`
+import org.hamcrest.Matchers.equalTo
 import org.hamcrest.Matchers.emptyIterable
 import org.hamcrest.Matchers.notNullValue
 import org.hamcrest.Matchers.sameInstance
 import org.junit.Test
 import org.mockito.ArgumentCaptor.forClass
-import org.mockito.Mockito.`when`
 import org.mockito.Mockito.eq
 import org.mockito.Mockito.verify
 
@@ -38,8 +35,8 @@ class ParserFilterTest {
 
        private fun setupSone(identity: String): Sone {
                val sone = mock<Sone>()
-               `when`(sone.id).thenReturn(identity)
-               `when`(core.getSone(identity)).thenReturn(sone)
+               whenever(sone.id).thenReturn(identity)
+               whenever(core.getSone(identity)).thenReturn(sone)
                return sone
        }
 
@@ -63,7 +60,7 @@ class ParserFilterTest {
                filter.format(templateContext, "text", parameters)
                val context = forClass(SoneTextParserContext::class.java)
                verify(soneTextParser).parse(eq<String>("text") ?: "", context.capture())
-               assertThat(context.value.postingSone, `is`(sone))
+               assertThat(context.value.postingSone, equalTo(sone))
        }
 
        @Test
diff --git a/src/test/kotlin/net/pterodactylus/sone/template/PostAccessorTest.kt b/src/test/kotlin/net/pterodactylus/sone/template/PostAccessorTest.kt
new file mode 100644 (file)
index 0000000..bb92810
--- /dev/null
@@ -0,0 +1,197 @@
+package net.pterodactylus.sone.template
+
+import net.pterodactylus.sone.core.*
+import net.pterodactylus.sone.data.*
+import net.pterodactylus.sone.test.*
+import net.pterodactylus.sone.utils.*
+import net.pterodactylus.util.template.*
+import org.hamcrest.MatcherAssert.*
+import org.hamcrest.Matchers.*
+import org.junit.*
+
+/**
+ * Unit test for [PostAccessor].
+ */
+class PostAccessorTest {
+
+       private val core = mock<Core>()
+       private val accessor = PostAccessor(core)
+       private val post = mock<Post>()
+       private val now = System.currentTimeMillis()
+
+       @Before
+       fun setupPost() {
+               whenever(post.id).thenReturn("post-id")
+       }
+
+       @Test
+       fun `accessor returns the correct replies`() {
+               val replies = listOf(
+                               createPostReply(2000),
+                               createPostReply(-1000),
+                               createPostReply(-2000),
+                               createPostReply(-3000),
+                               createPostReply(-4000)
+               )
+               whenever(core.getReplies("post-id")).thenReturn(replies)
+               val repliesForPost = accessor[null, post, "replies"] as Collection<PostReply>
+               assertThat(repliesForPost, contains(
+                               replies[1],
+                               replies[2],
+                               replies[3],
+                               replies[4]
+               ))
+       }
+
+       private fun createPostReply(timeOffset: Long) = mock<PostReply>().apply {
+               whenever(time).thenReturn(now + timeOffset)
+       }
+
+       @Test
+       fun `accessor returns the liking sones`() {
+               val sones = setOf<Sone>()
+               whenever(core.getLikes(post)).thenReturn(sones)
+               val likingSones = accessor[null, post, "likes"] as Set<Sone>
+               assertThat(likingSones, equalTo(sones))
+       }
+
+       @Test
+       fun `accessor returns whether the current sone liked a post`() {
+               val sone = mock<Sone>()
+               whenever(sone.isLikedPostId("post-id")).thenReturn(true)
+               val templateContext = TemplateContext()
+               templateContext["currentSone"] = sone
+               assertThat(accessor[templateContext, post, "liked"], equalTo<Any>(true))
+       }
+
+       @Test
+       fun `accessor returns false if post is not liked`() {
+               val sone = mock<Sone>()
+               val templateContext = TemplateContext()
+               templateContext["currentSone"] = sone
+               assertThat(accessor[templateContext, post, "liked"], equalTo<Any>(false))
+       }
+
+       @Test
+       fun `accessor returns false if there is no current sone`() {
+               val templateContext = TemplateContext()
+               assertThat(accessor[templateContext, post, "liked"], equalTo<Any>(false))
+       }
+
+       @Test
+       fun `accessor returns that not known post is new`() {
+               assertThat(accessor[null, post, "new"], equalTo<Any>(true))
+       }
+
+       @Test
+       fun `accessor returns that known post is not new`() {
+               whenever(post.isKnown).thenReturn(true)
+               assertThat(accessor[null, post, "new"], equalTo<Any>(false))
+       }
+
+       @Test
+       fun `accessor returns if post is bookmarked`() {
+               whenever(core.isBookmarked(post)).thenReturn(true)
+               assertThat(accessor[null, post, "bookmarked"], equalTo<Any>(true))
+       }
+
+       @Test
+       fun `reply sone for remote post without replies is current sone`() {
+               val post = mockPostFrom(remoteSone)
+               assertThat(accessor[templateContext, post, "replySone"], equalTo<Any>(currentSone))
+       }
+
+       @Test
+       fun `reply sone for remote post with remote replies is current sone`() {
+               val post = mockPostFrom(remoteSone)
+               val replies = listOf(mockReplyFrom(remoteSone), mockReplyFrom(remoteSone))
+               whenever(core.getReplies("post-id")).thenReturn(replies)
+               assertThat(accessor[templateContext, post, "replySone"], equalTo<Any>(currentSone))
+       }
+
+       @Test
+       fun `reply sone for remote post with remote and one local replies is sone of local reply`() {
+               val post = mockPostFrom(remoteSone)
+               val localSone = mockLocalSone()
+               val replies = listOf(mockReplyFrom(remoteSone), mockReplyFrom(localSone))
+               whenever(core.getReplies("post-id")).thenReturn(replies)
+               assertThat(accessor[templateContext, post, "replySone"], equalTo<Any>(localSone))
+       }
+
+       @Test
+       fun `reply sone for remote post with remote and several local replies is sone of latest local reply`() {
+               val post = mockPostFrom(remoteSone)
+               val localSone1 = mockLocalSone()
+               val localSone2 = mockLocalSone()
+               val replies = listOf(mockReplyFrom(remoteSone), mockReplyFrom(localSone1), mockReplyFrom(localSone2))
+               whenever(core.getReplies("post-id")).thenReturn(replies)
+               assertThat(accessor[templateContext, post, "replySone"], equalTo<Any>(localSone2))
+       }
+
+       @Test
+       fun `reply sone for local post without replies is post sone`() {
+               val localSone = mockLocalSone()
+               val post = mockPostFrom(localSone)
+               assertThat(accessor[templateContext, post, "replySone"], equalTo<Any>(localSone))
+       }
+
+       @Test
+       fun `reply sone for local post with remote replies is local sone`() {
+               val localSone = mockLocalSone()
+               val post = mockPostFrom(localSone)
+               val replies = listOf(mockReplyFrom(remoteSone), mockReplyFrom(remoteSone))
+               whenever(core.getReplies("post-id")).thenReturn(replies)
+               assertThat(accessor[templateContext, post, "replySone"], equalTo<Any>(localSone))
+       }
+
+       @Test
+       fun `reply sone for local post with remote and one local replies is local reply sone`() {
+               val localSone1 = mockLocalSone()
+               val post = mockPostFrom(localSone1)
+               val localSone2 = mockLocalSone()
+               val replies = listOf(mockReplyFrom(remoteSone), mockReplyFrom(localSone2))
+               whenever(core.getReplies("post-id")).thenReturn(replies)
+               assertThat(accessor[templateContext, post, "replySone"], equalTo<Any>(localSone2))
+       }
+
+       @Test
+       fun `reply sone for local post with remote and several local replies is latest local reply sone`() {
+               val localSone1 = mockLocalSone()
+               val post = mockPostFrom(localSone1)
+               val localSone2 = mockLocalSone()
+               val localSone3 = mockLocalSone()
+               val replies = listOf(mockReplyFrom(remoteSone), mockReplyFrom(localSone2), mockReplyFrom(localSone3))
+               whenever(core.getReplies("post-id")).thenReturn(replies)
+               assertThat(accessor[templateContext, post, "replySone"], equalTo<Any>(localSone3))
+       }
+
+       @Test
+       fun `reply sone for post directed at local sone is local sone`() {
+               val localSone = mockLocalSone()
+               val post = mockPostFrom(remoteSone, localSone)
+               assertThat(accessor[templateContext, post, "replySone"], equalTo<Any>(localSone))
+       }
+
+
+       @Test
+       fun `accessor returns other properties`() {
+               assertThat(accessor[null, post, "hashCode"], equalTo<Any>(post.hashCode()))
+       }
+
+}
+
+private val currentSone = mock<Sone>()
+private val remoteSone = mock<Sone>()
+private fun mockLocalSone() = mock<Sone>().apply { whenever(isLocal).thenReturn(true) }
+
+private val templateContext = TemplateContext().apply {
+       this["currentSone"] = currentSone
+}
+
+private fun mockPostFrom(sone: Sone, recipient: Sone? = null) = mock<Post>().apply {
+       whenever(id).thenReturn("post-id")
+       whenever(this.sone).thenReturn(sone)
+       whenever(this.recipient).thenReturn(recipient.asOptional())
+}
+
+private fun mockReplyFrom(sone: Sone) = mock<PostReply>().apply { whenever(this.sone).thenReturn(sone) }
index a2c4ad7..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
@@ -17,7 +16,7 @@ import net.pterodactylus.sone.test.mock
 import net.pterodactylus.sone.test.whenever
 import net.pterodactylus.util.template.TemplateContext
 import org.hamcrest.MatcherAssert.assertThat
-import org.hamcrest.Matchers.`is`
+import org.hamcrest.Matchers.equalTo
 import org.hamcrest.Matchers.nullValue
 import org.junit.Before
 import org.junit.Test
@@ -86,7 +85,7 @@ class ProfileAccessorTest {
        @Test
        fun `avatar ID is returned if profile belongs to local sone`() {
                whenever(remoteSone.isLocal).thenReturn(true)
-               assertThat(accessor.get(templateContext, profile, "avatar"), `is`<Any>("avatar-id"))
+               assertThat(accessor.get(templateContext, profile, "avatar"), equalTo<Any>("avatar-id"))
        }
 
        @Test
@@ -98,14 +97,14 @@ class ProfileAccessorTest {
        @Test
        fun `avatar ID is returned if sone is configure to always show avatars`() {
                currentSone.options.showCustomAvatars = ALWAYS
-               assertThat(accessor.get(templateContext, profile, "avatar"), `is`<Any>("avatar-id"))
+               assertThat(accessor.get(templateContext, profile, "avatar"), equalTo<Any>("avatar-id"))
        }
 
        @Test
        fun `avatar ID is returned if sone is configure to show avatars of followed sones and remote sone is followed`() {
                currentSone.options.showCustomAvatars = FOLLOWED
                whenever(currentSone.hasFriend("remote-sone")).thenReturn(true)
-               assertThat(accessor.get(templateContext, profile, "avatar"), `is`<Any>("avatar-id"))
+               assertThat(accessor.get(templateContext, profile, "avatar"), equalTo<Any>("avatar-id"))
        }
 
        @Test
@@ -142,7 +141,7 @@ class ProfileAccessorTest {
        fun `avatar ID is returned if sone is configure to show avatars based on manual trust and explicit trust is one`() {
                currentSone.options.showCustomAvatars = MANUALLY_TRUSTED
                setTrust(Trust(1, null, null))
-               assertThat(accessor.get(templateContext, profile, "avatar"), `is`<Any>("avatar-id"))
+               assertThat(accessor.get(templateContext, profile, "avatar"), equalTo<Any>("avatar-id"))
        }
 
        @Test
@@ -156,7 +155,7 @@ class ProfileAccessorTest {
        fun `avatar ID is returned if sone is configure to show avatars based on trust and explicit trust is one`() {
                currentSone.options.showCustomAvatars = TRUSTED
                setTrust(Trust(1, null, null))
-               assertThat(accessor.get(templateContext, profile, "avatar"), `is`<Any>("avatar-id"))
+               assertThat(accessor.get(templateContext, profile, "avatar"), equalTo<Any>("avatar-id"))
        }
 
        @Test
@@ -177,12 +176,12 @@ class ProfileAccessorTest {
        fun `avatar ID is returned if sone is configure to show avatars based on trust and implicit trust is one`() {
                currentSone.options.showCustomAvatars = TRUSTED
                setTrust(Trust(0, 1, null))
-               assertThat(accessor.get(templateContext, profile, "avatar"), `is`<Any>("avatar-id"))
+               assertThat(accessor.get(templateContext, profile, "avatar"), equalTo<Any>("avatar-id"))
        }
 
        @Test
        fun `accessing other members uses reflection accessor`() {
-               assertThat(accessor.get(templateContext, profile, "hashCode"), `is`<Any>(profile.hashCode()))
+               assertThat(accessor.get(templateContext, profile, "hashCode"), equalTo<Any>(profile.hashCode()))
        }
 
 }
index c6990a7..efed7a3 100644 (file)
@@ -2,7 +2,7 @@ package net.pterodactylus.sone.template
 
 import net.pterodactylus.sone.data.Profile
 import net.pterodactylus.sone.data.Sone
-import net.pterodactylus.sone.test.mock
+import net.pterodactylus.sone.test.*
 import net.pterodactylus.sone.text.FreenetLinkPart
 import net.pterodactylus.sone.text.Part
 import net.pterodactylus.sone.text.PlainTextPart
@@ -10,7 +10,6 @@ import net.pterodactylus.sone.text.SonePart
 import org.hamcrest.MatcherAssert.assertThat
 import org.hamcrest.Matchers.contains
 import org.junit.Test
-import org.mockito.Mockito.`when`
 
 /**
  * Unit test for [ShortenFilter].
@@ -76,7 +75,7 @@ class ShortenFilterTest {
        @Test
        fun `sone parts are added but their length is ignored`() {
                val sone = mock<Sone>()
-               `when`(sone.profile).thenReturn(Profile(sone))
+               whenever(sone.profile).thenReturn(Profile(sone))
                assertThat(shortenParts(15, 10, SonePart(sone), PlainTextPart("This is a long text.")), contains<Part>(
                                SonePart(sone),
                                PlainTextPart("This is a …")
@@ -86,7 +85,7 @@ class ShortenFilterTest {
        @Test
        fun `additional sone parts are ignored`() {
                val sone = mock<Sone>()
-               `when`(sone.profile).thenReturn(Profile(sone))
+               whenever(sone.profile).thenReturn(Profile(sone))
                assertThat(shortenParts(15, 10, PlainTextPart("This is a long text."), SonePart(sone)), contains<Part>(
                                PlainTextPart("This is a …")
                ))
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 f18033e..96a3cd5 100644 (file)
@@ -1,30 +1,31 @@
 package net.pterodactylus.sone.template
 
-import freenet.l10n.BaseL10n
-import net.pterodactylus.sone.test.mock
-import net.pterodactylus.sone.test.whenever
-import org.hamcrest.MatcherAssert.assertThat
-import org.hamcrest.Matchers.equalTo
-import org.junit.Test
+import net.pterodactylus.sone.freenet.*
+import org.hamcrest.MatcherAssert.*
+import org.hamcrest.Matchers.*
+import org.junit.*
+import java.util.*
 
 /**
  * Unit test for [UnknownDateFilter].
  */
 class UnknownDateFilterTest {
 
-       private val baseL10n = mock<BaseL10n>()
+       private val translation = object : Translation {
+               override val currentLocale = Locale.ENGLISH
+               override fun translate(key: String) = if (key == unknownKey) "translated" else ""
+       }
        private val unknownKey = "unknown.key"
-       private val filter = UnknownDateFilter(baseL10n, unknownKey)
+       private val filter = UnknownDateFilter(translation, unknownKey)
 
        @Test
        fun `filter returns given object for non-longs`() {
-           val someObject = Any()
+               val someObject = Any()
                assertThat(filter.format(null, someObject, null), equalTo<Any>(someObject))
        }
 
        @Test
        fun `filter returns translated value of unknown key if zero is given`() {
-           whenever(baseL10n.getString(unknownKey)).thenReturn("translated")
                assertThat(filter.format(null, 0L, null), equalTo<Any>("translated"))
        }
 
index 2fc4a96..b0bed5c 100644 (file)
@@ -1,11 +1,12 @@
 package net.pterodactylus.sone.test
 
-import com.google.inject.Injector
-import com.google.inject.Module
+import com.google.inject.*
 import com.google.inject.name.*
+import org.hamcrest.MatcherAssert.*
+import org.hamcrest.Matchers.*
 import org.mockito.*
 import javax.inject.Provider
-import kotlin.reflect.KClass
+import kotlin.reflect.*
 
 fun <T : Any> KClass<T>.isProvidedBy(instance: T) = Module { it.bind(this.java).toProvider(Provider<T> { instance }) }
 fun <T : Any> KClass<T>.withNameIsProvidedBy(instance: T, name: String) = Module { it.bind(this.java).annotatedWith(Names.named(name)).toProvider(Provider<T> { instance }) }
@@ -14,7 +15,16 @@ fun <T : Any> KClass<T>.isProvidedBy(provider: KClass<out Provider<T>>) = Module
 inline fun <reified T : Any> KClass<T>.isProvidedByMock() = Module { it.bind(this.java).toProvider(Provider<T> { mock() }) }
 inline fun <reified T : Any> KClass<T>.isProvidedByDeepMock() = Module { it.bind(this.java).toProvider(Provider<T> { deepMock() }) }
 
-inline fun <reified T : Any> Injector.getInstance() = getInstance(T::class.java)!!
+inline fun <reified T : Any> Injector.getInstance(annotation: Annotation? = null): T = annotation
+               ?.let { getInstance(Key.get(object : TypeLiteral<T>() {}, it)) }
+               ?: getInstance(Key.get(object : TypeLiteral<T>() {}))
+
+
+inline fun <reified T : Any> Injector.verifySingletonInstance(annotation: Annotation? = null) {
+       val firstInstance = getInstance<T>(annotation)
+       val secondInstance = getInstance<T>(annotation)
+       assertThat(firstInstance, sameInstance(secondInstance))
+}
 
 fun <T : Any> supply(javaClass: Class<T>): Source<T> = object : Source<T> {
        override fun fromInstance(instance: T) = Module { it.bind(javaClass).toInstance(instance) }
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 c084d35..1f479f6 100644 (file)
@@ -1,7 +1,29 @@
 package net.pterodactylus.sone.test
 
+import freenet.support.*
+import net.pterodactylus.sone.freenet.wot.*
+import net.pterodactylus.sone.utils.*
 import net.pterodactylus.util.web.*
 import org.hamcrest.*
+import org.hamcrest.Matchers.*
+
+/**
+ * Returns a [hamcrest matcher][Matcher] constructed from the given predicate.
+ */
+fun <T> matches(description: String? = null, predicate: (T) -> Boolean) = object : TypeSafeDiagnosingMatcher<T>() {
+
+       override fun matchesSafely(item: T, mismatchDescription: Description) =
+                       predicate(item).onFalse {
+                               mismatchDescription.appendValue(item).appendText(" did not match predicate")
+                       }
+
+       override fun describeTo(description: Description) {
+               description.appendText("matches predicate ").appendValue(predicate)
+       }
+
+}.let { matcher ->
+       description?.let { describedAs(description, matcher) } ?: matcher
+}
 
 fun hasHeader(name: String, value: String) = object : TypeSafeDiagnosingMatcher<Header>() {
        override fun matchesSafely(item: Header, mismatchDescription: Description) =
@@ -19,3 +41,123 @@ fun <T : Any> compare(value: T, comparison: (T) -> Boolean, onError: (T) -> Unit
                false.takeUnless { comparison(value) }
                                ?.also { onError(value) }
 
+fun <K, V> isEmptyMap() = object : TypeSafeDiagnosingMatcher<Map<K, V>>() {
+       override fun describeTo(description: Description) {
+               description.appendText("empty map")
+       }
+
+       override fun matchesSafely(item: Map<K, V>, mismatchDescription: Description) =
+                       item.isEmpty().onFalse {
+                               mismatchDescription.appendText("was ").appendValue(item)
+                       }
+}
+
+fun isTrust(trust: Int?, score: Int?, rank: Int?) =
+               AttributeMatcher<Trust>("trust")
+                               .addAttribute("trust", trust, Trust::explicit)
+                               .addAttribute("score", score, Trust::implicit)
+                               .addAttribute("rank", rank, Trust::distance)
+
+fun isTrusted(ownIdentity: OwnIdentity, trust: Matcher<Trust>) = object : TypeSafeDiagnosingMatcher<Identity>() {
+       override fun matchesSafely(item: Identity, mismatchDescription: Description) =
+                       item.getTrust(ownIdentity)?.let { foundTrust ->
+                               trust.matches(foundTrust).onFalse {
+                                       trust.describeMismatch(foundTrust, mismatchDescription)
+                               }
+                       } ?: {
+                               mismatchDescription.appendText("not trusted")
+                               false
+                       }()
+
+       override fun describeTo(description: Description) {
+               description
+                               .appendText("trusted by ").appendValue(ownIdentity)
+                               .appendText(" with ").appendValue(trust)
+       }
+}
+
+fun isIdentity(id: String, nickname: String?, requestUri: String, contexts: Matcher<out Iterable<String>>, properties: Matcher<out Map<out String, String>>) =
+               AttributeMatcher<Identity>("identity")
+                               .addAttribute("id", id, Identity::getId)
+                               .addAttribute("nickname", nickname, Identity::getNickname)
+                               .addAttribute("requestUri", requestUri, Identity::getRequestUri)
+                               .addAttribute("contexts", Identity::getContexts, contexts)
+                               .addAttribute("properties", Identity::getProperties, properties)
+
+fun isOwnIdentity(id: String, nickname: String, requestUri: String, insertUri: String, contexts: Matcher<Iterable<String>>, properties: Matcher<Map<out String, String>>) =
+               AttributeMatcher<OwnIdentity>("own identity")
+                               .addAttribute("id", id, OwnIdentity::getId)
+                               .addAttribute("nickname", nickname, OwnIdentity::getNickname)
+                               .addAttribute("request uri", requestUri, OwnIdentity::getRequestUri)
+                               .addAttribute("insert uri", insertUri, OwnIdentity::getInsertUri)
+                               .addAttribute("contexts", OwnIdentity::getContexts, contexts)
+                               .addAttribute("properties", OwnIdentity::getProperties, properties)
+
+fun hasField(name: String, valueMatcher: Matcher<String>) = object : TypeSafeDiagnosingMatcher<SimpleFieldSet>() {
+       override fun matchesSafely(item: SimpleFieldSet, mismatchDescription: Description) =
+                       valueMatcher.matches(item.get(name)).onFalse {
+                               valueMatcher.describeMismatch(item, mismatchDescription)
+                       }
+
+       override fun describeTo(description: Description) {
+               description
+                               .appendText("simple field set with key ").appendValue(name)
+                               .appendText(", value ").appendValue(valueMatcher)
+       }
+}
+
+/**
+ * [TypeSafeDiagnosingMatcher] implementation that aims to cut down boilerplate on verifying the attributes
+ * of typical container objects.
+ */
+class AttributeMatcher<T>(private val objectName: String) : TypeSafeDiagnosingMatcher<T>() {
+
+       private data class AttributeToMatch<T, V>(
+                       val name: String,
+                       val getter: (T) -> V,
+                       val matcher: Matcher<out V>
+       )
+
+       private val attributesToMatch = mutableListOf<AttributeToMatch<T, *>>()
+
+       /**
+        * Adds an attribute to check for equality, returning `this`.
+        */
+       fun <V> addAttribute(name: String, expected: V, getter: (T) -> V): AttributeMatcher<T> = apply {
+               attributesToMatch.add(AttributeToMatch(name, getter, describedAs("$name %0", equalTo(expected), expected)))
+       }
+
+       /**
+        * Adds an attribute to check with the given [hamcrest matcher][Matcher].
+        */
+       fun <V> addAttribute(name: String, getter: (T) -> V, matcher: Matcher<out V>) = apply {
+               attributesToMatch.add(AttributeToMatch(name, getter, matcher))
+       }
+
+       override fun describeTo(description: Description) {
+               attributesToMatch.forEachIndexed { index, attributeToMatch ->
+                       if (index == 0) {
+                               description.appendText("$objectName with ")
+                       } else {
+                               description.appendText(", ")
+                       }
+                       attributeToMatch.matcher.describeTo(description)
+               }
+       }
+
+       override fun matchesSafely(item: T, mismatchDescription: Description): Boolean =
+                       attributesToMatch.fold(true) { matches, attributeToMatch ->
+                               if (!matches) {
+                                       false
+                               } else {
+                                       if (!attributeToMatch.matcher.matches(attributeToMatch.getter(item))) {
+                                               mismatchDescription.appendText("but ${attributeToMatch.name} ")
+                                               attributeToMatch.matcher.describeMismatch(attributeToMatch.getter(item), mismatchDescription)
+                                               false
+                                       } else {
+                                               true
+                                       }
+                               }
+                       }
+
+}
index 85b86d5..4d345b3 100644 (file)
@@ -3,7 +3,7 @@ package net.pterodactylus.sone.test
 import com.google.inject.Module
 import org.mockito.*
 import org.mockito.invocation.InvocationOnMock
-import org.mockito.stubbing.OngoingStubbing
+import org.mockito.stubbing.*
 
 inline fun <reified T : Any> mock(): T = Mockito.mock<T>(T::class.java)!!
 inline fun <reified T : Any> mockBuilder(): T = Mockito.mock<T>(T::class.java, Mockito.RETURNS_SELF)!!
@@ -20,6 +20,7 @@ inline fun <reified T : Any> bindMock(): Module =
                Module { it!!.bind(T::class.java).toInstance(mock()) }
 
 inline fun <reified T: Any?> whenever(methodCall: T) = Mockito.`when`(methodCall)!!
+inline fun <reified T: Any?> Stubber.whenever(mock: T) = `when`(mock)!!
 
 inline fun <reified T : Any> OngoingStubbing<T>.thenReturnMock(): OngoingStubbing<T> = this.thenReturn(mock())
 
diff --git a/src/test/kotlin/net/pterodactylus/sone/test/Mocks.kt b/src/test/kotlin/net/pterodactylus/sone/test/Mocks.kt
new file mode 100644 (file)
index 0000000..a25fa5d
--- /dev/null
@@ -0,0 +1,65 @@
+/**
+ * Sone - Mocks.kt - Copyright © 2019–2020 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.test
+
+import com.google.common.base.*
+import freenet.crypt.*
+import freenet.keys.*
+import net.pterodactylus.sone.data.*
+import net.pterodactylus.sone.data.SoneOptions.*
+import net.pterodactylus.sone.data.impl.*
+import net.pterodactylus.sone.utils.*
+
+val remoteSone1 = createRemoteSone()
+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) {
+       private val options = DefaultSoneOptions()
+       override fun getOptions() = options
+       override fun isLocal() = true
+}
+fun createRemoteSone(id: String? = createId()) = IdOnlySone(id)
+
+fun createPost(text: String = "", sone: Sone = remoteSone1, known: Boolean = false, 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, 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() = time
+       override fun getText() = text
+       override fun isKnown() = known
+}
+
+fun createImage(sone: Sone): Image =
+               ImageImpl().modify().setSone(sone).update()
diff --git a/src/test/kotlin/net/pterodactylus/sone/test/NotParallel.kt b/src/test/kotlin/net/pterodactylus/sone/test/NotParallel.kt
new file mode 100644 (file)
index 0000000..6e00fbf
--- /dev/null
@@ -0,0 +1,8 @@
+package net.pterodactylus.sone.test
+
+/**
+ * Marker class for a JUnit [org.junit.experimental.categories.Category], to
+ * mark tests that should not be run parallel to other tests.
+ */
+class NotParallel
+
diff --git a/src/test/kotlin/net/pterodactylus/sone/test/TestLoaders.kt b/src/test/kotlin/net/pterodactylus/sone/test/TestLoaders.kt
new file mode 100644 (file)
index 0000000..f71e9f5
--- /dev/null
@@ -0,0 +1,21 @@
+package net.pterodactylus.sone.test
+
+import net.pterodactylus.sone.main.*
+import net.pterodactylus.util.template.*
+import net.pterodactylus.util.web.*
+
+/**
+ * [Loaders] implementation for use in tests. Use [templates] to control what templates are
+ * returned by the [loadTemplate] method.
+ */
+class TestLoaders : Loaders {
+
+       val templates = mutableMapOf<String, Template>()
+
+       override fun loadTemplate(path: String) = templates[path] ?: Template()
+
+       override fun <REQ : Request> loadStaticPage(basePath: String, prefix: String, mimeType: String) = TestPage<REQ>()
+
+       override fun getTemplateProvider() = TemplateProvider { _, _ -> Template() }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/test/TestPage.kt b/src/test/kotlin/net/pterodactylus/sone/test/TestPage.kt
new file mode 100644 (file)
index 0000000..762f789
--- /dev/null
@@ -0,0 +1,14 @@
+package net.pterodactylus.sone.test
+
+import net.pterodactylus.util.web.*
+
+/**
+ * Dummy implementation of a [Page].
+ */
+class TestPage<REQ : Request> : Page<REQ> {
+
+       override fun getPath() = ""
+       override fun isPrefixPage() = false
+       override fun handleRequest(freenetRequest: REQ, response: Response) = response
+
+}
index 9996925..ef23d62 100644 (file)
@@ -1,5 +1,6 @@
 package net.pterodactylus.sone.test
 
+import org.junit.rules.*
 import java.lang.reflect.*
 
 private val modifiers = Field::class.java.getDeclaredField("modifiers").apply {
@@ -17,3 +18,5 @@ fun setField(instance: Any, name: String, value: Any?) {
                                field.set(instance, value)
                        }
 }
+
+inline fun <reified T : Throwable> ExpectedException.expect() = expect(T::class.java)
index fcf6e67..aeba2c7 100644 (file)
@@ -1,7 +1,7 @@
 package net.pterodactylus.sone.text
 
 import org.hamcrest.MatcherAssert.assertThat
-import org.hamcrest.Matchers.`is`
+import org.hamcrest.Matchers.equalTo
 import org.junit.Test
 
 /**
@@ -11,7 +11,7 @@ class FreenetLinkPartTest {
 
        @Test
        fun linkIsUsedAsTitleIfNoTextIsGiven() {
-               assertThat(FreenetLinkPart("link", "text", true).title, `is`("link"))
+               assertThat(FreenetLinkPart("link", "text", true).title, equalTo("link"))
        }
 
 }
index 373a848..e38343c 100644 (file)
@@ -1,7 +1,7 @@
 package net.pterodactylus.sone.text
 
 import org.hamcrest.MatcherAssert.assertThat
-import org.hamcrest.Matchers.`is`
+import org.hamcrest.Matchers.equalTo
 import org.junit.Test
 
 /**
@@ -11,7 +11,7 @@ class LinkPartTest {
 
        @Test
        fun linkIsUsedAsTitleIfNoTitleIsGiven() {
-               assertThat(LinkPart("link", "text").title, `is`("link"))
+               assertThat(LinkPart("link", "text").title, equalTo("link"))
        }
 
 }
diff --git a/src/test/kotlin/net/pterodactylus/sone/text/SoneMentionDetectorTest.kt b/src/test/kotlin/net/pterodactylus/sone/text/SoneMentionDetectorTest.kt
new file mode 100644 (file)
index 0000000..552b7ee
--- /dev/null
@@ -0,0 +1,263 @@
+/**
+ * Sone - SoneMentionDetectorTest.kt - Copyright © 2019–2020 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.text
+
+import com.google.common.eventbus.*
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.sone.data.*
+import net.pterodactylus.sone.database.*
+import net.pterodactylus.sone.test.*
+import org.hamcrest.MatcherAssert.*
+import org.hamcrest.Matchers.*
+import kotlin.test.*
+
+/**
+ * Unit test for [SoneMentionDetector].
+ */
+@Suppress("UnstableApiUsage")
+class SoneMentionDetectorTest {
+
+       private val caughtExceptions = mutableListOf<Throwable>()
+       private val eventBus = EventBus { exception, _ -> caughtExceptions += exception }
+       private val soneProvider = TestSoneProvider()
+       private val postProvider = TestPostProvider()
+       private val soneTextParser = SoneTextParser(soneProvider, postProvider)
+       private val capturedFoundEvents = mutableListOf<MentionOfLocalSoneFoundEvent>()
+       private val capturedRemovedEvents = mutableListOf<MentionOfLocalSoneRemovedEvent>()
+       private val postReplyProvider = TestPostReplyProvider()
+
+       init {
+               eventBus.register(SoneMentionDetector(eventBus, soneTextParser, postReplyProvider))
+               eventBus.register(object : Any() {
+                       @Subscribe
+                       fun captureFoundEvent(mentionOfLocalSoneFoundEvent: MentionOfLocalSoneFoundEvent) {
+                               capturedFoundEvents += mentionOfLocalSoneFoundEvent
+                       }
+
+                       @Subscribe
+                       fun captureRemovedEvent(event: MentionOfLocalSoneRemovedEvent) {
+                               capturedRemovedEvents += event
+                       }
+               })
+       }
+
+       @Test
+       fun `detector does not emit event on post that does not contain any sones`() {
+               val post = createPost()
+               eventBus.post(NewPostFoundEvent(post))
+               assertThat(capturedFoundEvents, emptyIterable())
+       }
+
+       @Test
+       fun `detector does not emit event on post that does contain two remote sones`() {
+               val post = createPost("text mentions sone://${remoteSone1.id} and sone://${remoteSone2.id}.")
+               eventBus.post(NewPostFoundEvent(post))
+               assertThat(capturedFoundEvents, emptyIterable())
+       }
+
+       @Test
+       fun `detector emits event on post that contains links to a remote and a local sone`() {
+               val post = createPost("text mentions sone://${localSone1.id} and sone://${remoteSone2.id}.")
+               eventBus.post(NewPostFoundEvent(post))
+               assertThat(capturedFoundEvents, contains(MentionOfLocalSoneFoundEvent(post)))
+       }
+
+       @Test
+       fun `detector emits one event on post that contains two links to the same local sone`() {
+               val post = createPost("text mentions sone://${localSone1.id} and sone://${localSone1.id}.")
+               eventBus.post(NewPostFoundEvent(post))
+               assertThat(capturedFoundEvents, contains(MentionOfLocalSoneFoundEvent(post)))
+       }
+
+       @Test
+       fun `detector emits one event on post that contains links to two local sones`() {
+               val post = createPost("text mentions sone://${localSone1.id} and sone://${localSone2.id}.")
+               eventBus.post(NewPostFoundEvent(post))
+               assertThat(capturedFoundEvents, contains(MentionOfLocalSoneFoundEvent(post)))
+       }
+
+       @Test
+       fun `detector does not emit event for post by local sone`() {
+               val post = createPost("text mentions sone://${localSone1.id} and sone://${localSone2.id}.", localSone1)
+               eventBus.post(NewPostFoundEvent(post))
+               assertThat(capturedFoundEvents, emptyIterable())
+       }
+
+       @Test
+       fun `detector does not emit event for reply that contains no sones`() {
+               val reply = emptyPostReply()
+               eventBus.post(NewPostReplyFoundEvent(reply))
+               assertThat(capturedFoundEvents, emptyIterable())
+       }
+
+       @Test
+       fun `detector does not emit event for reply that contains two links to remote sones`() {
+               val reply = emptyPostReply("text mentions sone://${remoteSone1.id} and sone://${remoteSone2.id}.")
+               eventBus.post(NewPostReplyFoundEvent(reply))
+               assertThat(capturedFoundEvents, emptyIterable())
+       }
+
+       @Test
+       fun `detector emits event on reply that contains links to a remote and a local sone`() {
+               val post = createPost()
+               val reply = emptyPostReply("text mentions sone://${remoteSone1.id} and sone://${localSone1.id}.", post)
+               eventBus.post(NewPostReplyFoundEvent(reply))
+               assertThat(capturedFoundEvents, contains(MentionOfLocalSoneFoundEvent(post)))
+       }
+
+       @Test
+       fun `detector emits one event on reply that contains two links to the same local sone`() {
+               val post = createPost()
+               val reply = emptyPostReply("text mentions sone://${localSone1.id} and sone://${localSone1.id}.", post)
+               eventBus.post(NewPostReplyFoundEvent(reply))
+               assertThat(capturedFoundEvents, contains(MentionOfLocalSoneFoundEvent(post)))
+       }
+
+       @Test
+       fun `detector emits one event on reply that contains two links to local sones`() {
+               val post = createPost()
+               val reply = emptyPostReply("text mentions sone://${localSone1.id} and sone://${localSone2.id}.", post)
+               eventBus.post(NewPostReplyFoundEvent(reply))
+               assertThat(capturedFoundEvents, contains(MentionOfLocalSoneFoundEvent(post)))
+       }
+
+       @Test
+       fun `detector does not emit event for reply by local sone`() {
+               val reply = emptyPostReply("text mentions sone://${localSone1.id} and sone://${localSone2.id}.", sone = localSone1)
+               eventBus.post(NewPostReplyFoundEvent(reply))
+               assertThat(capturedFoundEvents, emptyIterable())
+       }
+
+       @Test
+       fun `detector does not emit event for reply without post`() {
+               val reply = emptyPostReply("text mentions sone://${localSone1.id} and sone://${localSone2.id}.", post = null)
+               eventBus.post(NewPostReplyFoundEvent(reply))
+               assertThat(caughtExceptions, emptyIterable())
+               assertThat(capturedFoundEvents, emptyIterable())
+       }
+
+       @Test
+       fun `detector does not emit removed event when a post without mention is removed`() {
+               val post = createPost()
+               eventBus.post(PostRemovedEvent(post))
+               assertThat(capturedRemovedEvents, emptyIterable())
+       }
+
+       @Test
+       fun `detector does emit removed event when post with mention is removed`() {
+               val post = createPost("sone://${localSone1.id}")
+               eventBus.post(NewPostFoundEvent(post))
+               eventBus.post(PostRemovedEvent(post))
+               assertThat(capturedRemovedEvents, contains(MentionOfLocalSoneRemovedEvent(post)))
+       }
+
+       @Test
+       fun `detector does not emit removed event when a post without mention is marked as known`() {
+               val post = createPost()
+               eventBus.post(MarkPostKnownEvent(post))
+               assertThat(capturedRemovedEvents, emptyIterable())
+       }
+
+       @Test
+       fun `detector does emit removed event when post with mention is marked as known`() {
+               val post = createPost("sone://${localSone1.id}")
+               eventBus.post(NewPostFoundEvent(post))
+               eventBus.post(MarkPostKnownEvent(post))
+               assertThat(capturedRemovedEvents, contains(MentionOfLocalSoneRemovedEvent(post)))
+       }
+
+       @Test
+       fun `detector does emit removed event when reply with mention is removed and no more mentions in that post exist`() {
+               val post = createPost()
+               val reply = emptyPostReply("sone://${localSone1.id}", post)
+               postReplyProvider.postReplies[post.id] = listOf(reply)
+               eventBus.post(NewPostReplyFoundEvent(reply))
+               eventBus.post(PostReplyRemovedEvent(reply))
+               assertThat(capturedRemovedEvents, contains(MentionOfLocalSoneRemovedEvent(post)))
+       }
+
+       @Test
+       fun `detector does not emit removed event when reply with mention is removed and post mentions local sone`() {
+               val post = createPost("sone://${localSone1.id}")
+               val reply = emptyPostReply("sone://${localSone1.id}", post)
+               eventBus.post(NewPostReplyFoundEvent(reply))
+               eventBus.post(PostReplyRemovedEvent(reply))
+               assertThat(capturedRemovedEvents, emptyIterable())
+       }
+
+       @Test
+       fun `detector does emit removed event when reply with mention is removed and post mentions local sone but is known`() {
+               val post = createPost("sone://${localSone1.id}", known = true)
+               val reply = emptyPostReply("sone://${localSone1.id}", post)
+               eventBus.post(NewPostReplyFoundEvent(reply))
+               eventBus.post(PostReplyRemovedEvent(reply))
+               assertThat(capturedRemovedEvents, contains(MentionOfLocalSoneRemovedEvent(post)))
+       }
+
+       @Test
+       fun `detector does not emit removed event when reply with mention is removed and post has other replies with mentions`() {
+               val post = createPost()
+               val reply1 = emptyPostReply("sone://${localSone1.id}", post)
+               val reply2 = emptyPostReply("sone://${localSone1.id}", post)
+               postReplyProvider.postReplies[post.id] = listOf(reply1, reply2)
+               eventBus.post(NewPostReplyFoundEvent(reply1))
+               eventBus.post(PostReplyRemovedEvent(reply1))
+               assertThat(capturedRemovedEvents, emptyIterable())
+       }
+
+       @Test
+       fun `detector does emit removed event when reply with mention is removed and post has other replies with mentions which are known`() {
+               val post = createPost()
+               val reply1 = emptyPostReply("sone://${localSone1.id}", post)
+               val reply2 = emptyPostReply("sone://${localSone1.id}", post, known = true)
+               postReplyProvider.postReplies[post.id] = listOf(reply1, reply2)
+               eventBus.post(NewPostReplyFoundEvent(reply1))
+               eventBus.post(PostReplyRemovedEvent(reply1))
+               assertThat(capturedRemovedEvents, contains(MentionOfLocalSoneRemovedEvent(post)))
+       }
+
+}
+
+private class TestSoneProvider : SoneProvider {
+
+       override val sones: Collection<Sone> get() = remoteSones + localSones
+       override val localSones: Collection<Sone> get() = setOf(localSone1, localSone2)
+       override val remoteSones: Collection<Sone> get() = setOf(remoteSone1, remoteSone2)
+       override val soneLoader: (String) -> Sone? get() = this::getSone
+       override fun getSone(soneId: String): Sone? =
+                       localSones.firstOrNull { it.id == soneId } ?: remoteSones.firstOrNull { it.id == soneId }
+
+}
+
+private class TestPostProvider : PostProvider {
+
+       override fun getPost(postId: String): Post? = null
+       override fun getPosts(soneId: String): Collection<Post> = emptyList()
+       override fun getDirectedPosts(recipientId: String): Collection<Post> = emptyList()
+
+}
+
+private class TestPostReplyProvider : PostReplyProvider {
+
+       val replies = mutableMapOf<String, PostReply>()
+       val postReplies = mutableMapOf<String, List<PostReply>>()
+
+       override fun getPostReply(id: String) = replies[id]
+       override fun getReplies(postId: String) = postReplies[postId] ?: emptyList()
+
+}
index 31fca48..ef1b1c0 100644 (file)
@@ -1,12 +1,10 @@
 package net.pterodactylus.sone.text
 
-import net.pterodactylus.sone.data.Profile
 import net.pterodactylus.sone.data.Sone
-import net.pterodactylus.sone.test.mock
+import net.pterodactylus.sone.test.*
 import org.hamcrest.MatcherAssert.assertThat
-import org.hamcrest.Matchers.`is`
+import org.hamcrest.Matchers.equalTo
 import org.junit.Test
-import org.mockito.Mockito.`when`
 
 /**
  * Unit test for [SonePart].
@@ -16,15 +14,15 @@ class SonePartTest {
        private val sone = mock<Sone>()
 
        init {
-               `when`(sone.profile).thenReturn(mock())
-               `when`(sone.name).thenReturn("sone")
+               whenever(sone.profile).thenReturn(mock())
+               whenever(sone.name).thenReturn("sone")
        }
 
        private val part = SonePart(sone)
 
        @Test
        fun textIsConstructedFromSonesNiceName() {
-               assertThat<String>(part.text, `is`<String>("sone"))
+               assertThat<String>(part.text, equalTo<String>("sone"))
        }
 
 }
index b20225a..3367faf 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - SoneTextParserTest.kt - Copyright © 2011–2019 David Roden
+ * Sone - SoneTextParserTest.kt - Copyright © 2011–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
diff --git a/src/test/kotlin/net/pterodactylus/sone/utils/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()
-       }
-
-}
index 56627c3..bd81f08 100644 (file)
@@ -1,9 +1,8 @@
 package net.pterodactylus.sone.utils
 
-import org.hamcrest.MatcherAssert.assertThat
-import org.hamcrest.Matchers.equalTo
-import org.hamcrest.Matchers.nullValue
-import org.junit.Test
+import org.hamcrest.MatcherAssert.*
+import org.hamcrest.Matchers.*
+import kotlin.test.*
 
 /**
  * Unit test for [Booleans].
@@ -30,4 +29,44 @@ class BooleansTest {
                assertThat(true.ifFalse { true }, nullValue())
        }
 
+       @Test
+       fun `onTrue returns true on true`() {
+               assertThat(true.onTrue {}, equalTo(true))
+       }
+
+       @Test
+       fun `onTrue returns false on false`() {
+               assertThat(false.onTrue {}, equalTo(false))
+       }
+
+       @Test
+       fun `onTrue is not executed on false`() {
+               assertThat(false.onTrue { throw RuntimeException() }, equalTo(false))
+       }
+
+       @Test(expected = RuntimeException::class)
+       fun `onTrue is executed on true`() {
+               true.onTrue { throw RuntimeException() }
+       }
+
+       @Test
+       fun `onFalse returns true on true`() {
+               assertThat(true.onFalse {}, equalTo(true))
+       }
+
+       @Test
+       fun `onFalse returns false on false`() {
+               assertThat(false.onFalse {}, equalTo(false))
+       }
+
+       @Test
+       fun `onFalse is not executed on true`() {
+               assertThat(true.onFalse { throw RuntimeException() }, equalTo(true))
+       }
+
+       @Test(expected = RuntimeException::class)
+       fun `onFalse is executed on false`() {
+               false.onFalse { throw RuntimeException() }
+       }
+
 }
diff --git a/src/test/kotlin/net/pterodactylus/sone/utils/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 9e96bfe..76cc723 100644 (file)
@@ -1,8 +1,9 @@
 package net.pterodactylus.sone.utils
 
-import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.MatcherAssert.*
 import org.hamcrest.Matchers.*
-import org.junit.Test
+import java.util.concurrent.*
+import kotlin.test.*
 
 /**
  * Unit test for Object utils.
@@ -19,9 +20,11 @@ class ObjectsTest {
                assertThat(null.asList(), empty())
        }
 
-       @Test(expected = IllegalArgumentException::class)
+       @Test
        fun `exception is thrown for null and true condition`() {
-               null.throwOnNullIf(true) { IllegalArgumentException() }
+               assertFailsWith(IllegalArgumentException::class) {
+                       null.throwOnNullIf(true) { IllegalArgumentException() }
+               }
        }
 
        @Test
@@ -41,4 +44,29 @@ class ObjectsTest {
                assertThat(any.throwOnNullIf(false) { IllegalArgumentException() }, equalTo(any))
        }
 
+       @Test
+       fun `onNull is executed on null`() {
+               val called = CountDownLatch(1)
+               null.onNull { called.countDown() }
+               assertThat(called.count, equalTo(0L))
+       }
+
+       @Test
+       fun `onNull returns null when called on null`() {
+               assertThat(null.onNull {}, nullValue())
+       }
+
+       @Test
+       fun `onNull is not executed on non-null`() {
+               val called = CountDownLatch(1)
+               Any().onNull { called.countDown() }
+               assertThat(called.count, equalTo(1L))
+       }
+
+       @Test
+       fun `onNull returns object when called on non-null`() {
+               val any = Any()
+               assertThat(any.onNull {}, sameInstance(any))
+       }
+
 }
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/utils/RenderablesTest.kt b/src/test/kotlin/net/pterodactylus/sone/utils/RenderablesTest.kt
new file mode 100644 (file)
index 0000000..c2facce
--- /dev/null
@@ -0,0 +1,36 @@
+/**
+ * Sone - RenderablesTest.kt - Copyright © 2019–2020 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.utils
+
+import net.pterodactylus.util.io.*
+import org.hamcrest.MatcherAssert.*
+import org.hamcrest.Matchers.*
+import kotlin.test.*
+
+/**
+ * Unit tests for tools in `Renderables.kt`.
+ */
+class RenderablesTest {
+
+       @Test
+       fun `render method renders notification`() {
+               val notification = Renderable { writer -> writer.use { it.append("Test!\n") } }
+               assertThat(notification.render(), equalTo("Test!\n"))
+       }
+
+}
index cf6ab15..842feb4 100644 (file)
@@ -102,11 +102,6 @@ class AllPagesTest {
        }
 
        @Test
-       fun `distrust page can be injected`() {
-               assertThat(allPages.distrustPage, instanceOf<DistrustPage>())
-       }
-
-       @Test
        fun `edit album page can be injected`() {
                assertThat(allPages.editAlbumPage, instanceOf<EditAlbumPage>())
        }
@@ -197,11 +192,6 @@ class AllPagesTest {
        }
 
        @Test
-       fun `trust page can be injected`() {
-               assertThat(allPages.trustPage, instanceOf<TrustPage>())
-       }
-
-       @Test
        fun `unbookmark page can be injected`() {
                assertThat(allPages.unbookmarkPage, instanceOf<UnbookmarkPage>())
        }
@@ -222,11 +212,6 @@ class AllPagesTest {
        }
 
        @Test
-       fun `untrust page can be injected`() {
-               assertThat(allPages.untrustPage, instanceOf<UntrustPage>())
-       }
-
-       @Test
        fun `upload image page can be injected`() {
                assertThat(allPages.uploadImagePage, instanceOf<UploadImagePage>())
        }
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 d242b32..2134661 100644 (file)
@@ -5,10 +5,10 @@ import freenet.clients.http.*
 import net.pterodactylus.sone.main.*
 import net.pterodactylus.sone.test.*
 import net.pterodactylus.sone.web.page.*
-import net.pterodactylus.util.web.*
 import org.junit.*
 import org.junit.rules.*
 import org.mockito.Mockito.*
+import kotlin.test.Test
 
 class PageToadletRegistryTest {
 
@@ -35,7 +35,7 @@ class PageToadletRegistryTest {
                verify(pageMaker).addNavigationCategory("/Sone/index.html", "Navigation.Menu.Sone.Name", "Navigation.Menu.Sone.Tooltip", sonePlugin)
        }
 
-       private val page = TestPage()
+       private val page = TestPage<FreenetRequest>()
 
        @Test
        fun `adding a page without menuname will add it correctly`() {
@@ -80,16 +80,70 @@ class PageToadletRegistryTest {
                verify(toadletContainer).unregister(toadletWithMenuname)
        }
 
+       @Test
+       fun `adding a debug page will not add it to the container`() {
+               val toadlet = createPageToadlet()
+               whenever(pageToadletFactory.createPageToadlet(page)).thenReturn(toadlet)
+               pageToadletRegistry.addDebugPage(page)
+               pageToadletRegistry.registerToadlets()
+               verify(toadletContainer, never()).register(toadlet, null, "/Sone/", true, false)
+       }
+
+       @Test
+       fun `adding a debug page and activating debug mode will add it to the container`() {
+               val toadlet = createPageToadlet()
+               whenever(pageToadletFactory.createPageToadlet(page)).thenReturn(toadlet)
+               pageToadletRegistry.addDebugPage(page)
+               pageToadletRegistry.registerToadlets()
+               pageToadletRegistry.activateDebugMode()
+               verify(toadletContainer).register(toadlet, null, "/Sone/", true, false)
+       }
+
+       @Test
+       fun `adding a debug page and activating debug mode twice will add it to the container once`() {
+               val toadlet = createPageToadlet()
+               whenever(pageToadletFactory.createPageToadlet(page)).thenReturn(toadlet)
+               pageToadletRegistry.addDebugPage(page)
+               pageToadletRegistry.registerToadlets()
+               pageToadletRegistry.activateDebugMode()
+               pageToadletRegistry.activateDebugMode()
+               verify(toadletContainer, times(1)).register(toadlet, null, "/Sone/", true, false)
+       }
+
+       @Test
+       fun `debug pages are ungegistered from the container`() {
+               val toadlet = createPageToadlet()
+               whenever(pageToadletFactory.createPageToadlet(page)).thenReturn(toadlet)
+               pageToadletRegistry.addDebugPage(page)
+               pageToadletRegistry.registerToadlets()
+               pageToadletRegistry.activateDebugMode()
+               pageToadletRegistry.unregisterToadlets()
+               verify(toadletContainer).unregister(toadlet)
+       }
+
+       @Test
+       fun `inactive debug pages are not ungegistered from the container`() {
+               val toadlet = createPageToadlet()
+               whenever(pageToadletFactory.createPageToadlet(page)).thenReturn(toadlet)
+               pageToadletRegistry.addDebugPage(page)
+               pageToadletRegistry.registerToadlets()
+               pageToadletRegistry.unregisterToadlets()
+               verify(toadletContainer, never()).unregister(toadlet)
+       }
+
+       @Test
+       fun `debug page can not be added after registering`() {
+               val toadlet = createPageToadlet()
+               whenever(pageToadletFactory.createPageToadlet(page)).thenReturn(toadlet)
+               pageToadletRegistry.registerToadlets()
+               expectedException.expect(IllegalStateException::class.java)
+               pageToadletRegistry.addDebugPage(page)
+       }
+
        private fun createPageToadlet(menuName: String? = null) =
                        mock<PageToadlet>().apply {
                                whenever(this.path()).thenReturn("/Sone/")
                                whenever(this.menuName).thenReturn(menuName)
                        }
 
-       private class TestPage : Page<FreenetRequest> {
-               override fun getPath() = ""
-               override fun isPrefixPage() = false
-               override fun handleRequest(freenetRequest: FreenetRequest, response: Response) = response
-       }
-
 }
index 9137da3..9c0860c 100644 (file)
@@ -2,8 +2,6 @@ package net.pterodactylus.sone.web
 
 import com.google.inject.Guice.*
 import freenet.client.*
-import freenet.clients.http.*
-import freenet.l10n.*
 import freenet.support.api.*
 import net.pterodactylus.sone.core.*
 import net.pterodactylus.sone.data.*
@@ -15,26 +13,30 @@ import net.pterodactylus.sone.template.*
 import net.pterodactylus.sone.test.*
 import net.pterodactylus.sone.text.*
 import net.pterodactylus.sone.web.page.*
+import net.pterodactylus.util.notify.*
 import net.pterodactylus.util.template.*
 import net.pterodactylus.util.web.*
 import org.hamcrest.MatcherAssert.*
 import org.hamcrest.Matchers.*
-import org.junit.*
+import java.util.*
+import kotlin.test.*
 
 class WebInterfaceModuleTest {
 
        private val webInterfaceModule = WebInterfaceModule()
-       private val l10n = mock<BaseL10n>()
        private val loaders = mock<Loaders>()
+       private val translation = object : Translation {
+               override val currentLocale = Locale.ENGLISH
+               override fun translate(key: String) = if (key == "View.Sone.Text.UnknownDate") "unknown" else key
+       }
        private val additionalModules = arrayOf(
                        Core::class.isProvidedByMock(),
                        SoneProvider::class.isProvidedByMock(),
-                       BaseL10n::class.isProvidedBy(l10n),
+                       Translation::class.isProvidedBy(translation),
                        SoneTextParser::class.isProvidedByMock(),
                        ElementLoader::class.isProvidedByMock(),
                        Loaders::class.isProvidedBy(loaders),
-                       HighLevelSimpleClient::class.isProvidedByMock(),
-                       SessionManager::class.isProvidedByMock()
+                       HighLevelSimpleClient::class.isProvidedByMock()
        )
        private val injector = createInjector(webInterfaceModule, *additionalModules)!!
        private val templateContext by lazy { injector.getInstance<TemplateContextFactory>().createTemplateContext()!! }
@@ -190,7 +192,6 @@ class WebInterfaceModuleTest {
 
        @Test
        fun `unknown date filter uses correct l10n key`() {
-               whenever(l10n.getString("View.Sone.Text.UnknownDate")).thenReturn("unknown")
                assertThat(getFilter("unknown")!!.format(null, 0L, emptyMap()), equalTo<Any>("unknown"))
        }
 
@@ -200,6 +201,11 @@ class WebInterfaceModuleTest {
        }
 
        @Test
+       fun `template context contains duration format filter`() {
+               verifyFilter<DurationFormatFilter>("duration")
+       }
+
+       @Test
        fun `template context contains collection sort filter`() {
                verifyFilter<CollectionSortFilter>("sort")
        }
@@ -234,6 +240,11 @@ class WebInterfaceModuleTest {
                verifyFilter<PaginationFilter>("paginate")
        }
 
+       @Test
+       fun `template context histogram renderer`() {
+               verifyFilter<HistogramRenderer>("render-histogram")
+       }
+
        private inline fun <reified F : Filter> verifyFilter(name: String) {
                assertThat(getFilter(name), instanceOf(F::class.java))
        }
@@ -242,9 +253,7 @@ class WebInterfaceModuleTest {
 
        @Test
        fun `template context factory is created as singleton`() {
-           val factory1 = injector.getInstance<TemplateContextFactory>()
-           val factory2 = injector.getInstance<TemplateContextFactory>()
-               assertThat(factory1, sameInstance(factory2))
+               injector.verifySingletonInstance<TemplateContextFactory>()
        }
 
        @Test
@@ -266,7 +275,12 @@ class WebInterfaceModuleTest {
        @Test
        fun `page toadlet factory is created with correct prefix`() {
                val page = mock<Page<FreenetRequest>>()
-           assertThat(injector.getInstance<PageToadletFactory>().createPageToadlet(page).path(), startsWith("/Sone/"))
+               assertThat(injector.getInstance<PageToadletFactory>().createPageToadlet(page).path(), startsWith("/Sone/"))
+       }
+
+       @Test
+       fun `notification manager is created as singleton`() {
+               injector.verifySingletonInstance<NotificationManager>()
        }
 
 }
index 821198f..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
@@ -40,7 +39,7 @@ class CreatePostAjaxPageTest : JsonPageTest("createPost.ajax", pageSupplier = ::
        fun `request with valid data creates post`() {
                addRequestParameter("text", "test")
                val post = createPost()
-               whenever(core.createPost(currentSone, Optional.absent(), "test")).thenReturn(post)
+               whenever(core.createPost(currentSone, null, "test")).thenReturn(post)
                assertThatJsonIsSuccessful()
                assertThat(json["postId"]?.asText(), equalTo("id"))
                assertThat(json["sone"]?.asText(), equalTo(currentSone.id))
@@ -52,7 +51,7 @@ class CreatePostAjaxPageTest : JsonPageTest("createPost.ajax", pageSupplier = ::
                addRequestParameter("text", "test")
                addRequestParameter("recipient", "invalid")
                val post = createPost()
-               whenever(core.createPost(currentSone, Optional.absent(), "test")).thenReturn(post)
+               whenever(core.createPost(currentSone, null, "test")).thenReturn(post)
                assertThatJsonIsSuccessful()
                assertThat(json["postId"]?.asText(), equalTo("id"))
                assertThat(json["sone"]?.asText(), equalTo(currentSone.id))
@@ -66,7 +65,7 @@ class CreatePostAjaxPageTest : JsonPageTest("createPost.ajax", pageSupplier = ::
                val recipient = mock<Sone>().apply { whenever(id).thenReturn("valid") }
                addSone(recipient)
                val post = createPost("valid")
-               whenever(core.createPost(currentSone, Optional.of(recipient), "test")).thenReturn(post)
+               whenever(core.createPost(currentSone, recipient, "test")).thenReturn(post)
                assertThatJsonIsSuccessful()
                assertThat(json["postId"]?.asText(), equalTo("id"))
                assertThat(json["sone"]?.asText(), equalTo(currentSone.id))
@@ -78,7 +77,7 @@ class CreatePostAjaxPageTest : JsonPageTest("createPost.ajax", pageSupplier = ::
                addRequestParameter("text", "Link http://freenet.test:8888/KSK@foo is filtered")
                addRequestHeader("Host", "freenet.test:8888")
                val post = createPost()
-               whenever(core.createPost(currentSone, Optional.absent(), "Link KSK@foo is filtered")).thenReturn(post)
+               whenever(core.createPost(currentSone, null, "Link KSK@foo is filtered")).thenReturn(post)
                assertThatJsonIsSuccessful()
                assertThat(json["postId"]?.asText(), equalTo("id"))
                assertThat(json["sone"]?.asText(), equalTo(currentSone.id))
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/ajax/DistrustAjaxPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/ajax/DistrustAjaxPageTest.kt
deleted file mode 100644 (file)
index 868819e..0000000
+++ /dev/null
@@ -1,53 +0,0 @@
-package net.pterodactylus.sone.web.ajax
-
-import net.pterodactylus.sone.data.Sone
-import net.pterodactylus.sone.test.getInstance
-import net.pterodactylus.sone.test.mock
-import net.pterodactylus.sone.web.baseInjector
-import org.hamcrest.MatcherAssert.assertThat
-import org.hamcrest.Matchers.equalTo
-import org.hamcrest.Matchers.notNullValue
-import org.junit.Test
-import org.mockito.Mockito.verify
-
-/**
- * Unit test for [DistrustAjaxPage].
- */
-class DistrustAjaxPageTest : JsonPageTest("distrustSone.ajax", pageSupplier = ::DistrustAjaxPage) {
-
-       @Test
-       fun `request with missing sone results in invalid-sone-id`() {
-               assertThatJsonFailed("invalid-sone-id")
-       }
-
-       @Test
-       fun `request with invalid sone results in invalid-sone-id`() {
-               addRequestParameter("sone", "invalid-sone")
-               assertThatJsonFailed("invalid-sone-id")
-       }
-
-       @Test
-       fun `request with valid sone results in distrusted sone`() {
-               val sone = mock<Sone>()
-               addSone(sone, "sone-id")
-               addRequestParameter("sone", "sone-id")
-               assertThatJsonIsSuccessful()
-               verify(core).distrustSone(currentSone, sone)
-       }
-
-       @Test
-       fun `request with valid sone results in correct trust value being sent back`() {
-               core.preferences.newNegativeTrust = -33
-               val sone = mock<Sone>()
-               addSone(sone, "sone-id")
-               addRequestParameter("sone", "sone-id")
-               assertThatJsonIsSuccessful()
-               assertThat(json["trustValue"]?.asInt(), equalTo(-33))
-       }
-
-       @Test
-       fun `page can be created by dependency injection`() {
-           assertThat(baseInjector.getInstance<DistrustAjaxPage>(), notNullValue())
-       }
-
-}
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 a42e5ba..ffd6f15 100644 (file)
@@ -3,7 +3,6 @@ package net.pterodactylus.sone.web.ajax
 import com.fasterxml.jackson.databind.ObjectMapper
 import com.google.common.eventbus.EventBus
 import freenet.clients.http.ToadletContext
-import freenet.l10n.BaseL10n
 import freenet.support.SimpleReadOnlyArrayBucket
 import freenet.support.api.HTTPRequest
 import net.pterodactylus.sone.core.Core
@@ -20,6 +19,7 @@ import net.pterodactylus.sone.data.Sone
 import net.pterodactylus.sone.data.Sone.SoneStatus
 import net.pterodactylus.sone.data.Sone.SoneStatus.idle
 import net.pterodactylus.sone.data.SoneOptions.DefaultSoneOptions
+import net.pterodactylus.sone.freenet.*
 import net.pterodactylus.sone.test.deepMock
 import net.pterodactylus.sone.test.get
 import net.pterodactylus.sone.test.mock
@@ -32,7 +32,7 @@ import net.pterodactylus.util.template.TemplateContextFactory
 import net.pterodactylus.util.web.Method.GET
 import net.pterodactylus.util.web.Method.POST
 import org.mockito.ArgumentMatchers
-import java.util.NoSuchElementException
+import java.util.*
 import javax.naming.SizeLimitExceededException
 
 /**
@@ -44,7 +44,6 @@ open class TestObjects {
 
        val webInterface = mock<WebInterface>()
        var formPassword = "form-password"
-       val l10n = mock<BaseL10n>()
        val core = mock<Core>()
        val eventBus = mock<EventBus>()
        val preferences = Preferences(eventBus)
@@ -74,20 +73,21 @@ open class TestObjects {
        val images = mutableMapOf<String, Image>()
        val translations = mutableMapOf<String, String>()
 
+       private val translation = object : Translation {
+               override val currentLocale = Locale.ENGLISH
+               override fun translate(key: String) = translations[key] ?: ""
+       }
+
        init {
                whenever(webInterface.templateContextFactory).thenReturn(TemplateContextFactory())
-               whenever(webInterface.getCurrentSone(ArgumentMatchers.eq(toadletContext), ArgumentMatchers.anyBoolean())).thenReturn(currentSone)
-               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 }
                whenever(webInterface.getNotification(ArgumentMatchers.anyString())).then { notifications[it[0]].asOptional() }
                whenever(webInterface.getNewPosts(currentSone)).thenAnswer { newPosts.values }
                whenever(webInterface.getNewReplies(currentSone)).thenAnswer { newReplies.values }
-               whenever(webInterface.l10n).thenReturn(l10n)
-
-               whenever(l10n.getString(ArgumentMatchers.anyString())).then { translations[it[0]] }
+               whenever(webInterface.translation).thenReturn(translation)
 
                whenever(core.preferences).thenReturn(preferences)
                whenever(core.updateChecker).thenReturn(updateChecker)
@@ -137,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() {
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/ajax/TrustAjaxPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/ajax/TrustAjaxPageTest.kt
deleted file mode 100644 (file)
index d557790..0000000
+++ /dev/null
@@ -1,47 +0,0 @@
-package net.pterodactylus.sone.web.ajax
-
-import net.pterodactylus.sone.data.Sone
-import net.pterodactylus.sone.test.getInstance
-import net.pterodactylus.sone.test.mock
-import net.pterodactylus.sone.web.baseInjector
-import org.hamcrest.MatcherAssert.assertThat
-import org.hamcrest.Matchers.equalTo
-import org.hamcrest.Matchers.notNullValue
-import org.junit.Test
-import org.mockito.Mockito.verify
-
-/**
- * Unit test for [TrustAjaxPage].
- */
-class TrustAjaxPageTest : JsonPageTest("trustSone.ajax", requiresLogin = true, needsFormPassword = true, pageSupplier = ::TrustAjaxPage) {
-
-       private val sone = mock<Sone>()
-
-       @Test
-       fun `request with invalid sone results in invalid-sone-id`() {
-               assertThatJsonFailed("invalid-sone-id")
-       }
-
-       @Test
-       fun `request with valid sone trust sone`() {
-               addSone(sone, "sone-id")
-               addRequestParameter("sone", "sone-id")
-               assertThatJsonIsSuccessful()
-               verify(core).trustSone(currentSone, sone)
-       }
-
-       @Test
-       fun `request with valid sone returns positive trust value`() {
-               addSone(sone, "sone-id")
-               addRequestParameter("sone", "sone-id")
-               core.preferences.newPositiveTrust = 31
-               assertThatJsonIsSuccessful()
-               assertThat(json["trustValue"]?.asInt(), equalTo(31))
-       }
-
-       @Test
-       fun `page can be created by dependency injection`() {
-           assertThat(baseInjector.getInstance<TrustAjaxPage>(), notNullValue())
-       }
-
-}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/ajax/UntrustAjaxPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/ajax/UntrustAjaxPageTest.kt
deleted file mode 100644 (file)
index b693194..0000000
+++ /dev/null
@@ -1,50 +0,0 @@
-package net.pterodactylus.sone.web.ajax
-
-import net.pterodactylus.sone.data.*
-import net.pterodactylus.sone.test.*
-import net.pterodactylus.sone.web.*
-import org.hamcrest.MatcherAssert.*
-import org.hamcrest.Matchers.*
-import org.junit.*
-import org.mockito.Mockito.*
-
-/**
- * Unit test for [UntrustAjaxPage].
- */
-class UntrustAjaxPageTest : JsonPageTest("untrustSone.ajax", pageSupplier = ::UntrustAjaxPage) {
-
-       @Test
-       fun `request without sone results in invalid-sone-id`() {
-               assertThatJsonFailed("invalid-sone-id")
-       }
-
-       @Test
-       fun `request with invalid sone results in invalid-sone-id`() {
-               addRequestParameter("sone", "invalid")
-               assertThatJsonFailed("invalid-sone-id")
-       }
-
-       @Test
-       fun `request with valid sone results in sone being untrusted`() {
-               val sone = mock<Sone>()
-               addSone(sone, "sone-id")
-               addRequestParameter("sone", "sone-id")
-               assertThatJsonIsSuccessful()
-               verify(core).untrustSone(currentSone, sone)
-       }
-
-       @Test
-       fun `request with valid sone results in null trust value being returned`() {
-               val sone = mock<Sone>()
-               addSone(sone, "sone-id")
-               addRequestParameter("sone", "sone-id")
-               assertThatJsonIsSuccessful()
-               assertThat(json["trustValue"], nullValue())
-       }
-
-       @Test
-       fun `page can be created by dependency injection`() {
-           assertThat(baseInjector.getInstance<UntrustAjaxPage>(), notNullValue())
-       }
-
-}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/notification/ConfigNotReadHandlerTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/notification/ConfigNotReadHandlerTest.kt
new file mode 100644 (file)
index 0000000..7f3169b
--- /dev/null
@@ -0,0 +1,48 @@
+/**
+ * Sone - ConfigNotReadHandlerTest.kt - Copyright © 2019–2020 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.notification
+
+import com.google.common.eventbus.*
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.util.notify.*
+import net.pterodactylus.util.template.*
+import org.hamcrest.MatcherAssert.*
+import org.hamcrest.Matchers.*
+import org.junit.*
+
+/**
+ * Unit test for [ConfigNotReadHandler].
+ */
+@Suppress("UnstableApiUsage")
+class ConfigNotReadHandlerTest {
+
+       private val eventBus = EventBus()
+       private val notificationManager = NotificationManager()
+       private val notification = TemplateNotification("", Template())
+
+       init {
+               eventBus.register(ConfigNotReadHandler(notificationManager, notification))
+       }
+
+       @Test
+       fun `handler adds notification to manager when config was not read`() {
+               eventBus.post(ConfigNotRead())
+               assertThat(notificationManager.notifications, contains<Notification>(notification))
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/notification/FirstStartHandlerTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/notification/FirstStartHandlerTest.kt
new file mode 100644 (file)
index 0000000..356eeea
--- /dev/null
@@ -0,0 +1,53 @@
+/**
+ * Sone - FirstStartHandlerTest.kt - Copyright © 2019–2020 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.notification
+
+import com.google.common.eventbus.*
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.util.notify.*
+import net.pterodactylus.util.template.*
+import org.hamcrest.MatcherAssert.*
+import org.hamcrest.Matchers.*
+import kotlin.test.*
+
+/**
+ * Unit test for [FirstStartHandler].
+ */
+@Suppress("UnstableApiUsage")
+class FirstStartHandlerTest {
+
+       private val eventBus = EventBus()
+       private val notificationManager = NotificationManager()
+       private val notification = TemplateNotification(Template())
+
+       init {
+               eventBus.register(FirstStartHandler(notificationManager, notification))
+       }
+
+       @Test
+       fun `handler can be created`() {
+               FirstStartHandler(notificationManager, notification)
+       }
+
+       @Test
+       fun `handler adds notification to manager on first start event`() {
+               eventBus.post(FirstStart())
+               assertThat(notificationManager.notifications, contains<Notification>(notification))
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/notification/ImageInsertHandlerTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/notification/ImageInsertHandlerTest.kt
new file mode 100644 (file)
index 0000000..a50b4ae
--- /dev/null
@@ -0,0 +1,107 @@
+/**
+ * Sone - ImageInsertHandlerTest.kt - Copyright © 2019–2020 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.notification
+
+import com.google.common.eventbus.*
+import freenet.keys.FreenetURI.*
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.sone.data.*
+import net.pterodactylus.sone.data.impl.*
+import net.pterodactylus.sone.notify.*
+import net.pterodactylus.util.notify.*
+import net.pterodactylus.util.template.*
+import org.hamcrest.MatcherAssert.*
+import org.hamcrest.Matchers.*
+import kotlin.test.*
+
+/**
+ * Unit test for [ImageInsertHandler].
+ */
+@Suppress("UnstableApiUsage")
+class ImageInsertHandlerTest {
+
+       private val eventBus = EventBus()
+       private val notificationManager = NotificationManager()
+       private val imageInsertingNotification = ListNotification<Image>("", "", Template())
+       private val imageFailedNotification = ListNotification<Image>("", "", Template())
+       private val imageInsertedNotification = ListNotification<Image>("", "", Template())
+
+       init {
+               eventBus.register(ImageInsertHandler(notificationManager, imageInsertingNotification, imageFailedNotification, imageInsertedNotification))
+       }
+
+       @Test
+       fun `handler adds notification when image insert starts`() {
+               eventBus.post(ImageInsertStartedEvent(image))
+               assertThat(notificationManager.notifications, contains<Notification>(imageInsertingNotification))
+       }
+
+       @Test
+       fun `handler adds image to notification when image insert starts`() {
+               eventBus.post(ImageInsertStartedEvent(image))
+               assertThat(imageInsertingNotification.elements, contains(image))
+       }
+
+       @Test
+       fun `handler removes image from inserting notification when insert is aborted`() {
+               eventBus.post(ImageInsertStartedEvent(image))
+               eventBus.post(ImageInsertAbortedEvent(image))
+               assertThat(imageInsertingNotification.elements, emptyIterable())
+       }
+
+       @Test
+       fun `handler removes image from inserting notification when insert fails`() {
+               eventBus.post(ImageInsertStartedEvent(image))
+               eventBus.post(ImageInsertFailedEvent(image, Throwable()))
+               assertThat(imageInsertingNotification.elements, emptyIterable())
+       }
+
+       @Test
+       fun `handler adds image to insert-failed notification when insert fails`() {
+               eventBus.post(ImageInsertFailedEvent(image, Throwable()))
+               assertThat(imageFailedNotification.elements, contains(image))
+       }
+
+       @Test
+       fun `handler adds insert-failed notification to manager when insert fails`() {
+               eventBus.post(ImageInsertFailedEvent(image, Throwable()))
+               assertThat(notificationManager.notifications, contains<Notification>(imageFailedNotification))
+       }
+
+       @Test
+       fun `handler removes image from inserting notification when insert succeeds`() {
+               eventBus.post(ImageInsertStartedEvent(image))
+               eventBus.post(ImageInsertFinishedEvent(image, EMPTY_CHK_URI))
+               assertThat(imageInsertingNotification.elements, emptyIterable())
+       }
+
+       @Test
+       fun `handler adds image to inserted notification when insert succeeds`() {
+               eventBus.post(ImageInsertFinishedEvent(image, EMPTY_CHK_URI))
+               assertThat(imageInsertedNotification.elements, contains(image))
+       }
+
+       @Test
+       fun `handler adds inserted notification to manager when insert succeeds`() {
+               eventBus.post(ImageInsertFinishedEvent(image, EMPTY_CHK_URI))
+               assertThat(notificationManager.notifications, contains<Notification>(imageInsertedNotification))
+       }
+
+}
+
+private val image: Image = ImageImpl()
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/notification/LocalPostHandlerTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/notification/LocalPostHandlerTest.kt
new file mode 100644 (file)
index 0000000..22648b5
--- /dev/null
@@ -0,0 +1,118 @@
+/**
+ * Sone - LocalPostHandlerTest.kt - Copyright © 2019–2020 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.notification
+
+import com.google.common.eventbus.*
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.sone.data.*
+import net.pterodactylus.sone.data.impl.*
+import net.pterodactylus.sone.notify.*
+import net.pterodactylus.util.notify.*
+import net.pterodactylus.util.template.*
+import org.hamcrest.MatcherAssert.*
+import org.hamcrest.Matchers.*
+import kotlin.test.*
+
+/**
+ * Unit test for [LocalPostHandler].
+ */
+class LocalPostHandlerTest {
+
+       private val eventBus = EventBus()
+       private val notificationManager = NotificationManager()
+       private val notification = ListNotification<Post>("", "", Template())
+
+       init {
+               eventBus.register(LocalPostHandler(notificationManager, notification))
+       }
+
+       @Test
+       fun `handler adds post by local sone to notification`() {
+               eventBus.post(NewPostFoundEvent(localPost))
+               assertThat(notification.elements, contains<Post>(localPost))
+       }
+
+       @Test
+       fun `handler does not add post by remote sone to notification`() {
+               eventBus.post(NewPostFoundEvent(remotePost))
+               assertThat(notification.elements, emptyIterable())
+       }
+
+       @Test
+       fun `handler does not add notification to manager for post by remote sone`() {
+               eventBus.post(NewPostFoundEvent(remotePost))
+               assertThat(notificationManager.notifications, not(hasItem<Notification>(notification)))
+       }
+
+       @Test
+       fun `handler adds notification to manager`() {
+               eventBus.post(NewPostFoundEvent(localPost))
+               assertThat(notificationManager.notifications, contains<Notification>(notification))
+       }
+
+       @Test
+       fun `handler does not add notification during first start`() {
+               notificationManager.firstStart()
+               eventBus.post(NewPostFoundEvent(localPost))
+               assertThat(notificationManager.notifications, not(hasItem<Notification>(notification)))
+       }
+
+       @Test
+       fun `handler removes post from notification when post is removed`() {
+               notification.add(localPost)
+               notificationManager.addNotification(notification)
+               eventBus.post(PostRemovedEvent(localPost))
+               assertThat(notification.elements, emptyIterable())
+       }
+
+       @Test
+       fun `handler does not remove remote post from notification when post is removed`() {
+               notification.add(remotePost)
+               notificationManager.addNotification(notification)
+               eventBus.post(PostRemovedEvent(remotePost))
+               assertThat(notification.elements, contains(remotePost))
+       }
+
+       @Test
+       fun `handler removes post from notification when post is marked as known`() {
+               notification.add(localPost)
+               notificationManager.addNotification(notification)
+               eventBus.post(MarkPostKnownEvent(localPost))
+               assertThat(notification.elements, emptyIterable())
+       }
+
+       @Test
+       fun `handler does not remove remote post from notification when post is marked as known`() {
+               notification.add(remotePost)
+               notificationManager.addNotification(notification)
+               eventBus.post(MarkPostKnownEvent(remotePost))
+               assertThat(notification.elements, contains(remotePost))
+       }
+
+}
+
+private val localSone: Sone = object : IdOnlySone("local-sone") {
+       override fun isLocal() = true
+}
+private val localPost: Post = object : Post.EmptyPost("local-post") {
+       override fun getSone() = localSone
+}
+private val remoteSone: Sone = IdOnlySone("remote-sone")
+private val remotePost: Post = object : Post.EmptyPost("remote-post") {
+       override fun getSone() = remoteSone
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/notification/LocalReplyHandlerTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/notification/LocalReplyHandlerTest.kt
new file mode 100644 (file)
index 0000000..3532e77
--- /dev/null
@@ -0,0 +1,85 @@
+/**
+ * Sone - LocalReplyHandlerTest.kt - Copyright © 2019–2020 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.notification
+
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.sone.data.*
+import net.pterodactylus.sone.notify.*
+import net.pterodactylus.sone.test.*
+import net.pterodactylus.util.template.*
+import org.hamcrest.MatcherAssert.*
+import org.hamcrest.Matchers.*
+import kotlin.test.*
+
+/**
+ * Unit test for [LocalReplyHandler].
+ */
+class LocalReplyHandlerTest {
+
+       private val notification = ListNotification<PostReply>("", "", Template())
+       private val localReplyHandlerTester = NotificationHandlerTester { LocalReplyHandler(it, notification) }
+
+       @Test
+       fun `handler does not add reply to notification`() {
+               localReplyHandlerTester.sendEvent(NewPostReplyFoundEvent(remoteReply))
+               assertThat(notification.elements, emptyIterable())
+       }
+
+       @Test
+       fun `handler does add local reply to notification`() {
+               localReplyHandlerTester.sendEvent(NewPostReplyFoundEvent(localReply))
+               assertThat(notification.elements, contains(localReply))
+       }
+
+       @Test
+       fun `handler adds notification to manager`() {
+               localReplyHandlerTester.sendEvent(NewPostReplyFoundEvent(localReply))
+               assertThat(localReplyHandlerTester.notifications, hasItem(notification))
+       }
+
+       @Test
+       fun `handler does not add notification to manager for remote reply`() {
+               localReplyHandlerTester.sendEvent(NewPostReplyFoundEvent(remoteReply))
+               assertThat(localReplyHandlerTester.notifications, not(hasItem(notification)))
+       }
+
+       @Test
+       fun `handler does not add notification to manager during first start`() {
+               localReplyHandlerTester.firstStart()
+               localReplyHandlerTester.sendEvent(NewPostReplyFoundEvent(localReply))
+               assertThat(localReplyHandlerTester.notifications, not(hasItem(notification)))
+       }
+
+       @Test
+       fun `handler removes reply from notification if reply is removed`() {
+               notification.add(localReply)
+               localReplyHandlerTester.sendEvent(PostReplyRemovedEvent(localReply))
+               assertThat(notification.elements, not(hasItem(localReply)))
+       }
+
+       @Test
+       fun `handler removes reply from notification if reply is marked as known`() {
+               notification.add(localReply)
+               localReplyHandlerTester.sendEvent(MarkPostReplyKnownEvent(localReply))
+               assertThat(notification.elements, not(hasItem(localReply)))
+       }
+
+}
+
+private val localReply = emptyPostReply(sone = localSone1)
+private val remoteReply = emptyPostReply()
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/notification/MarkPostKnownDuringFirstStartHandlerTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/notification/MarkPostKnownDuringFirstStartHandlerTest.kt
new file mode 100644 (file)
index 0000000..5b8e41a
--- /dev/null
@@ -0,0 +1,59 @@
+/**
+ * Sone - MarkPostKnownDuringFirstStartHandlerTest.kt - Copyright © 2019–2020 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.notification
+
+import com.google.common.eventbus.*
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.sone.data.*
+import net.pterodactylus.util.notify.*
+import org.hamcrest.MatcherAssert.*
+import org.hamcrest.Matchers.*
+import java.util.function.*
+import kotlin.test.*
+
+/**
+ * Unit test for [MarkPostKnownDuringFirstStartHandler].
+ */
+@Suppress("UnstableApiUsage")
+class MarkPostKnownDuringFirstStartHandlerTest {
+
+       private val eventBus = EventBus()
+       private val notificationManager = NotificationManager()
+       private val markedPosts = mutableListOf<Post>()
+       private val handler = MarkPostKnownDuringFirstStartHandler(notificationManager, Consumer { markedPosts += it })
+
+       init {
+               eventBus.register(handler)
+       }
+
+       @Test
+       fun `post is not marked as known if not during first start`() {
+               eventBus.post(NewPostFoundEvent(post))
+               assertThat(markedPosts, emptyIterable())
+       }
+
+       @Test
+       fun `new post is marked as known during first start`() {
+               notificationManager.firstStart()
+               eventBus.post(NewPostFoundEvent(post))
+               assertThat(markedPosts, contains(post))
+       }
+
+}
+
+private val post: Post = Post.EmptyPost("post")
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/notification/MarkPostReplyKnownDuringFirstStartHandlerTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/notification/MarkPostReplyKnownDuringFirstStartHandlerTest.kt
new file mode 100644 (file)
index 0000000..3cb463f
--- /dev/null
@@ -0,0 +1,50 @@
+/**
+ * Sone - MarkPostReplyKnownDuringFirstStartHandlerTest.kt - Copyright © 2020 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.notification
+
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.sone.data.*
+import net.pterodactylus.sone.test.*
+import org.hamcrest.MatcherAssert.*
+import org.hamcrest.Matchers.*
+import java.util.function.*
+import kotlin.test.*
+
+/**
+ * Unit test for [MarkPostReplyKnownDuringFirstStartHandler].
+ */
+class MarkPostReplyKnownDuringFirstStartHandlerTest {
+
+       private val markedAsKnown = mutableListOf<PostReply>()
+       private val notificationTester = NotificationHandlerTester { MarkPostReplyKnownDuringFirstStartHandler(it, Consumer { markedAsKnown += it }) }
+       private val postReply = emptyPostReply()
+
+       @Test
+       fun `post reply is marked as known on new reply during first start`() {
+               notificationTester.firstStart()
+               notificationTester.sendEvent(NewPostReplyFoundEvent(postReply))
+               assertThat(markedAsKnown, contains(postReply))
+       }
+
+       @Test
+       fun `post reply is not marked as known on new reply if not during first start`() {
+               notificationTester.sendEvent(NewPostReplyFoundEvent(postReply))
+               assertThat(markedAsKnown, not(hasItem(postReply)))
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/notification/NewRemotePostHandlerTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/notification/NewRemotePostHandlerTest.kt
new file mode 100644 (file)
index 0000000..0f24aff
--- /dev/null
@@ -0,0 +1,97 @@
+/**
+ * Sone - NewRemotePostHandlerTest.kt - Copyright © 2019–2020 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.notification
+
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.sone.data.*
+import net.pterodactylus.sone.data.Post.*
+import net.pterodactylus.sone.data.impl.*
+import net.pterodactylus.sone.notify.*
+import net.pterodactylus.util.notify.*
+import net.pterodactylus.util.template.*
+import org.hamcrest.MatcherAssert.*
+import org.hamcrest.Matchers.*
+import kotlin.test.*
+
+/**
+ * Unit test for [NewRemotePostHandler].
+ */
+@Suppress("UnstableApiUsage")
+class NewRemotePostHandlerTest {
+
+       private val notification = ListNotification<Post>("", "", Template())
+       private val remotePostHandlerTest = NotificationHandlerTester { NewRemotePostHandler(it, notification) }
+
+       @Test
+       fun `handler adds remote post to new-post notification`() {
+               remotePostHandlerTest.sendEvent(NewPostFoundEvent(remotePost))
+               assertThat(notification.elements, contains(remotePost))
+       }
+
+       @Test
+       fun `handler does not add local post to new-post notification`() {
+               remotePostHandlerTest.sendEvent(NewPostFoundEvent(localPost))
+               assertThat(notification.elements, emptyIterable())
+       }
+
+       @Test
+       fun `handler adds notification for remote post to notification manager`() {
+               remotePostHandlerTest.sendEvent(NewPostFoundEvent(remotePost))
+               assertThat(remotePostHandlerTest.notifications, contains<Notification>(notification))
+       }
+
+       @Test
+       fun `handler does not add notification for local post to notification manager`() {
+               remotePostHandlerTest.sendEvent(NewPostFoundEvent(localPost))
+               assertThat(remotePostHandlerTest.notifications, emptyIterable())
+       }
+
+       @Test
+       fun `handler does not add notification to notification manager during first start`() {
+               remotePostHandlerTest.firstStart()
+               remotePostHandlerTest.sendEvent(NewPostFoundEvent(remotePost))
+               assertThat(remotePostHandlerTest.notifications, not(hasItem(notification)))
+       }
+
+       @Test
+       fun `handler removes post from notification if post is removed`() {
+               notification.add(remotePost)
+               remotePostHandlerTest.sendEvent(PostRemovedEvent(remotePost))
+               assertThat(notification.elements, not(hasItem(remotePost)))
+       }
+
+       @Test
+       fun `handler removes post from notification if post is marked as known`() {
+               notification.add(remotePost)
+               remotePostHandlerTest.sendEvent(MarkPostKnownEvent(remotePost))
+               assertThat(notification.elements, not(hasItem(remotePost)))
+       }
+
+}
+
+private val remoteSone: Sone = IdOnlySone("remote-sone")
+private val remotePost: Post = object : EmptyPost("remote-post") {
+       override fun getSone() = remoteSone
+}
+
+private val localSone: Sone = object : IdOnlySone("local-sone") {
+       override fun isLocal() = true
+}
+private val localPost: Post = object : EmptyPost("local-post") {
+       override fun getSone() = localSone
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/notification/NewSoneHandlerTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/notification/NewSoneHandlerTest.kt
new file mode 100644 (file)
index 0000000..e099974
--- /dev/null
@@ -0,0 +1,78 @@
+/**
+ * Sone - NewSoneHandlerTest.kt - Copyright © 2019–2020 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.notification
+
+import com.google.common.eventbus.*
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.sone.data.*
+import net.pterodactylus.sone.data.impl.*
+import net.pterodactylus.sone.notify.*
+import net.pterodactylus.util.notify.*
+import net.pterodactylus.util.template.*
+import org.hamcrest.MatcherAssert.*
+import org.hamcrest.Matchers.*
+import kotlin.test.*
+
+class NewSoneHandlerTest {
+
+       @Suppress("UnstableApiUsage")
+       private val eventBus = EventBus()
+       private val notificationManager = NotificationManager()
+       private val notification = ListNotification<Sone>("", "", Template())
+       private val handler = NewSoneHandler(notificationManager, notification)
+
+       init {
+               eventBus.register(handler)
+       }
+
+       @Test
+       fun `handler adds notification if new sone event is fired`() {
+               eventBus.post(NewSoneFoundEvent(sone))
+               assertThat(notificationManager.notifications, contains<Notification>(notification))
+       }
+
+       @Test
+       fun `handler adds sone to notification`() {
+               eventBus.post(NewSoneFoundEvent(sone))
+               assertThat(notification.elements, contains(sone))
+       }
+
+       @Test
+       fun `handler does not add notification on new sone event if first-start notification is present`() {
+               notificationManager.firstStart()
+               eventBus.post(NewSoneFoundEvent(sone))
+               assertThat(notificationManager.notifications, not(contains<Notification>(notification)))
+       }
+
+       @Test
+       fun `handler removes sone from notification if sone is marked as known`() {
+               notification.add(sone)
+               eventBus.post(MarkSoneKnownEvent(sone))
+               assertThat(notification.elements, emptyIterable())
+       }
+
+       @Test
+       fun `handler removes sone from notification if sone is removed`() {
+               notification.add(sone)
+               eventBus.post(SoneRemovedEvent(sone))
+               assertThat(notification.elements, emptyIterable())
+       }
+
+}
+
+private val sone: Sone = IdOnlySone("sone-id")
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/notification/NewVersionHandlerTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/notification/NewVersionHandlerTest.kt
new file mode 100644 (file)
index 0000000..6eb145a
--- /dev/null
@@ -0,0 +1,69 @@
+/**
+ * Sone - NewVersionHandlerTest.kt - Copyright © 2019–2020 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.notification
+
+import com.google.common.eventbus.*
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.util.notify.*
+import net.pterodactylus.util.template.*
+import net.pterodactylus.util.version.*
+import org.hamcrest.MatcherAssert.*
+import org.hamcrest.Matchers.*
+import kotlin.test.*
+
+/**
+ * Unit test for [NewVersionHandler].
+ */
+@Suppress("UnstableApiUsage")
+class NewVersionHandlerTest {
+
+       private val eventBus = EventBus()
+       private val notificationManager = NotificationManager()
+       private val notification = TemplateNotification(Template())
+
+       init {
+               eventBus.register(NewVersionHandler(notificationManager, notification))
+               eventBus.post(UpdateFoundEvent(Version(1, 2, 3), 1000L, 2000L, true))
+       }
+
+       @Test
+       fun `new-version handler adds notification to manager on new version`() {
+               assertThat(notificationManager.notifications, contains<Notification>(notification))
+       }
+
+       @Test
+       fun `handler sets version in notification`() {
+               assertThat(notification.get("latestVersion"), equalTo<Any>(Version(1, 2, 3)))
+       }
+
+       @Test
+       fun `handler sets release time in notification`() {
+               assertThat(notification.get("releaseTime"), equalTo<Any>(1000L))
+       }
+
+       @Test
+       fun `handler sets edition in notification`() {
+               assertThat(notification.get("latestEdition"), equalTo<Any>(2000L))
+       }
+
+       @Test
+       fun `handler sets disruptive flag in notification`() {
+               assertThat(notification.get("disruptive"), equalTo<Any>(true))
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/notification/NotificationHandlerModuleTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/notification/NotificationHandlerModuleTest.kt
new file mode 100644 (file)
index 0000000..f22e23a
--- /dev/null
@@ -0,0 +1,611 @@
+/**
+ * Sone - NotificationHandlerModuleTest.kt - Copyright © 2019–2020 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.notification
+
+import com.google.inject.*
+import com.google.inject.Guice.*
+import com.google.inject.name.Names.*
+import net.pterodactylus.sone.core.*
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.sone.data.*
+import net.pterodactylus.sone.data.Post.*
+import net.pterodactylus.sone.data.impl.*
+import net.pterodactylus.sone.database.*
+import net.pterodactylus.sone.freenet.wot.*
+import net.pterodactylus.sone.main.*
+import net.pterodactylus.sone.notify.*
+import net.pterodactylus.sone.test.*
+import net.pterodactylus.sone.text.*
+import net.pterodactylus.sone.utils.*
+import net.pterodactylus.util.notify.*
+import org.hamcrest.MatcherAssert.*
+import org.hamcrest.Matchers.*
+import org.mockito.*
+import org.mockito.Mockito.*
+import java.util.concurrent.*
+import java.util.concurrent.TimeUnit.*
+import java.util.function.*
+import kotlin.test.*
+
+/**
+ * Unit test for [NotificationHandlerModule].
+ */
+class NotificationHandlerModuleTest {
+
+       private val core = mock<Core>()
+       private val webOfTrustConnector = mock<WebOfTrustConnector>()
+       private val ticker = mock<ScheduledExecutorService>()
+       private val notificationManager = NotificationManager()
+       private val loaders = TestLoaders()
+       private val injector: Injector = createInjector(
+                       Core::class.isProvidedBy(core),
+                       NotificationManager::class.isProvidedBy(notificationManager),
+                       Loaders::class.isProvidedBy(loaders),
+                       WebOfTrustConnector::class.isProvidedBy(webOfTrustConnector),
+                       ScheduledExecutorService::class.withNameIsProvidedBy(ticker, "notification"),
+                       SoneTextParser::class.isProvidedByMock(),
+                       PostReplyProvider::class.isProvidedByMock(),
+                       NotificationHandlerModule()
+       )
+
+       @Test
+       fun `notification handler is created as singleton`() {
+               injector.verifySingletonInstance<NotificationHandler>()
+       }
+
+       @Test
+       fun `mark-post-known-during-first-start handler is created as singleton`() {
+               injector.verifySingletonInstance<MarkPostKnownDuringFirstStartHandler>()
+       }
+
+       @Test
+       fun `mark-post-known-during-first-start handler is created with correct action`() {
+               notificationManager.firstStart()
+               val handler = injector.getInstance<MarkPostKnownDuringFirstStartHandler>()
+               val post = mock<Post>()
+               handler.newPostFound(NewPostFoundEvent(post))
+               verify(core).markPostKnown(post)
+       }
+
+       @Test
+       fun `mark-post-reply-known-during-first-start handler is created as singleton`() {
+               injector.verifySingletonInstance<MarkPostReplyKnownDuringFirstStartHandler>()
+       }
+
+       @Test
+       fun `mark-post-reply-known-during-first-start handler is created with correct action`() {
+               notificationManager.firstStart()
+               val handler = injector.getInstance<MarkPostReplyKnownDuringFirstStartHandler>()
+               val postReply = mock<PostReply>()
+               handler.newPostReply(NewPostReplyFoundEvent(postReply))
+               verify(core).markReplyKnown(postReply)
+       }
+
+       @Test
+       fun `sone-locked-on-startup handler is created as singleton`() {
+               injector.verifySingletonInstance<SoneLockedOnStartupHandler>()
+       }
+
+       @Test
+       fun `module can create sone-locked-on-startup notification with correct id`() {
+               val notification = injector.getInstance<ListNotification<Sone>>(named("soneLockedOnStartup"))
+               assertThat(notification.id, equalTo("sone-locked-on-startup"))
+       }
+
+       @Test
+       fun `sone-locked-on-startup notification is created as singleton`() {
+               injector.verifySingletonInstance<ListNotification<Sone>>(named("soneLockedOnStartup"))
+       }
+
+       @Test
+       fun `module can create sone-locked-on-startup notification with correct template and key`() {
+               loaders.templates += "/templates/notify/soneLockedOnStartupNotification.html" to "<% sones>".asTemplate()
+               val notification = injector.getInstance<ListNotification<Sone>>(named("soneLockedOnStartup"))
+               val sone1 = IdOnlySone("sone1")
+               val sone2 = IdOnlySone("sone2")
+               notification.add(sone1)
+               notification.add(sone2)
+               assertThat(notification.render(), equalTo(listOf(sone1, sone2).toString()))
+       }
+
+       @Test
+       fun `sone-locked-on-startup notification is dismissable`() {
+               assertThat(injector.getInstance<ListNotification<Sone>>(named("soneLockedOnStartup")).isDismissable, equalTo(true))
+       }
+
+       @Test
+       fun `new-sone handler is created as singleton`() {
+               injector.verifySingletonInstance<NewSoneHandler>()
+       }
+
+       @Test
+       fun `new-sone notification has correct ID`() {
+               assertThat(injector.getInstance<ListNotification<Sone>>(named("newSone")).id, equalTo("new-sone-notification"))
+       }
+
+       @Test
+       fun `new-sone notification has correct key and template`() {
+               loaders.templates += "/templates/notify/newSoneNotification.html" to "<% sones>".asTemplate()
+               val notification = injector.getInstance<ListNotification<Sone>>(named("newSone"))
+               val sones = listOf(IdOnlySone("sone1"), IdOnlySone("sone2"))
+               sones.forEach(notification::add)
+               assertThat(notification.render(), equalTo(sones.toString()))
+       }
+
+       @Test
+       fun `new-sone notification is not dismissable`() {
+               assertThat(injector.getInstance<ListNotification<Sone>>(named("newSone")).isDismissable, equalTo(false))
+       }
+
+       @Test
+       fun `new-remote-post handler is created as singleton`() {
+               injector.verifySingletonInstance<NewRemotePostHandler>()
+       }
+
+       @Test
+       fun `new-remote-post notification is created as singleton`() {
+               injector.verifySingletonInstance<ListNotification<Post>>(named("newRemotePost"))
+       }
+
+       @Test
+       fun `new-remote-post notification has correct ID`() {
+               assertThat(injector.getInstance<ListNotification<Post>>(named("newRemotePost")).id, equalTo("new-post-notification"))
+       }
+
+       @Test
+       fun `new-remote-post notification is not dismissable`() {
+               assertThat(injector.getInstance<ListNotification<Post>>(named("newRemotePost")).isDismissable, equalTo(false))
+       }
+
+       @Test
+       fun `new-remote-post notification has correct key and template`() {
+               loaders.templates += "/templates/notify/newPostNotification.html" to "<% posts>".asTemplate()
+               val notification = injector.getInstance<ListNotification<Post>>(named("newRemotePost"))
+               val posts = listOf(EmptyPost("post1"), EmptyPost("post2"))
+               posts.forEach(notification::add)
+               assertThat(notification.render(), equalTo(posts.toString()))
+       }
+
+       @Test
+       fun `remote-post handler is created as singleton`() {
+               injector.verifySingletonInstance<RemotePostReplyHandler>()
+       }
+
+       @Test
+       fun `new-remote-post-reply notification is created as singleton`() {
+               injector.verifySingletonInstance<ListNotification<PostReply>>(named("newRemotePostReply"))
+       }
+
+       @Test
+       fun `new-remote-post-reply notification has correct ID`() {
+               assertThat(injector.getInstance<ListNotification<PostReply>>(named("newRemotePostReply")).id, equalTo("new-reply-notification"))
+       }
+
+       @Test
+       fun `new-remote-post-reply notification is not dismissable`() {
+               assertThat(injector.getInstance<ListNotification<PostReply>>(named("newRemotePostReply")).isDismissable, equalTo(false))
+       }
+
+       @Test
+       fun `new-remote-post-reply notification has correct key and template`() {
+               loaders.templates += "/templates/notify/newReplyNotification.html" to "<% replies>".asTemplate()
+               val notification = injector.getInstance<ListNotification<PostReply>>(named("newRemotePostReply"))
+               val postReplies = listOf(emptyPostReply(), emptyPostReply())
+               postReplies.forEach(notification::add)
+               assertThat(notification.render(), equalTo(postReplies.toString()))
+       }
+
+       @Test
+       fun `sone-locked notification is created as singleton`() {
+               injector.verifySingletonInstance<ListNotification<Sone>>(named("soneLocked"))
+       }
+
+       @Test
+       fun `sone-locked notification is dismissable`() {
+               assertThat(injector.getInstance<ListNotification<Sone>>(named("soneLocked")).isDismissable, equalTo(true))
+       }
+
+       @Test
+       fun `sone-locked notification has correct ID`() {
+               assertThat(injector.getInstance<ListNotification<Sone>>(named("soneLocked")).id, equalTo("sones-locked-notification"))
+       }
+
+       @Test
+       fun `sone-locked notification has correct key and template`() {
+               loaders.templates += "/templates/notify/lockedSonesNotification.html" to "<% sones>".asTemplate()
+               val notification = injector.getInstance<ListNotification<Sone>>(named("soneLocked"))
+               val sones = listOf(IdOnlySone("sone1"), IdOnlySone("sone2"))
+               sones.forEach(notification::add)
+               assertThat(notification.render(), equalTo(sones.toString()))
+       }
+
+       @Test
+       fun `sone-locked handler is created as singleton`() {
+               injector.verifySingletonInstance<SoneLockedHandler>()
+       }
+
+       @Test
+       fun `local-post notification is not dismissable`() {
+               assertThat(injector.getInstance<ListNotification<Post>>(named("localPost")).isDismissable, equalTo(false))
+       }
+
+       @Test
+       fun `local-post notification has correct ID`() {
+               assertThat(injector.getInstance<ListNotification<Post>>(named("localPost")).id, equalTo("local-post-notification"))
+       }
+
+       @Test
+       fun `local-post notification has correct key and template`() {
+               loaders.templates += "/templates/notify/newPostNotification.html" to "<% posts>".asTemplate()
+               val notification = injector.getInstance<ListNotification<Post>>(named("localPost"))
+               val posts = listOf(EmptyPost("post1"), EmptyPost("post2"))
+               posts.forEach(notification::add)
+               assertThat(notification.render(), equalTo(posts.toString()))
+       }
+
+       @Test
+       fun `local-post notification is created as singleton`() {
+               injector.verifySingletonInstance<ListNotification<Post>>(named("localPost"))
+       }
+
+       @Test
+       fun `local-post handler is created as singleton`() {
+               injector.verifySingletonInstance<LocalPostHandler>()
+       }
+
+       @Test
+       fun `local-reply notification is not dismissable`() {
+               assertThat(injector.getInstance<ListNotification<PostReply>>(named("localReply")).isDismissable, equalTo(false))
+       }
+
+       @Test
+       fun `local-reply notification has correct ID`() {
+               assertThat(injector.getInstance<ListNotification<PostReply>>(named("localReply")).id, equalTo("local-reply-notification"))
+       }
+
+       @Test
+       fun `local-reply notification has correct key and template`() {
+               loaders.templates += "/templates/notify/newReplyNotification.html" to "<% replies>".asTemplate()
+               val notification = injector.getInstance<ListNotification<PostReply>>(named("localReply"))
+               val replies = listOf(emptyPostReply("reply1"), emptyPostReply("reply2"))
+               replies.forEach(notification::add)
+               assertThat(notification.render(), equalTo(replies.toString()))
+       }
+
+       @Test
+       fun `local-reply notification is created as singleton`() {
+               injector.verifySingletonInstance<ListNotification<PostReply>>(named("localReply"))
+       }
+
+       @Test
+       fun `local-reply handler is created as singleton`() {
+               injector.verifySingletonInstance<LocalReplyHandler>()
+       }
+
+       @Test
+       fun `new-version notification is created as singleton`() {
+               injector.verifySingletonInstance<TemplateNotification>(named("newVersion"))
+       }
+
+       @Test
+       fun `new-version notification has correct ID`() {
+               assertThat(injector.getInstance<TemplateNotification>(named("newVersion")).id, equalTo("new-version-notification"))
+       }
+
+       @Test
+       fun `new-version notification is dismissable`() {
+               assertThat(injector.getInstance<TemplateNotification>(named("newVersion")).isDismissable, equalTo(true))
+       }
+
+       @Test
+       fun `new-version notification loads correct template`() {
+               loaders.templates += "/templates/notify/newVersionNotification.html" to "1".asTemplate()
+               val notification = injector.getInstance<TemplateNotification>(named("newVersion"))
+               assertThat(notification.render(), equalTo("1"))
+       }
+
+       @Test
+       fun `new-version handler is created as singleton`() {
+               injector.verifySingletonInstance<NewVersionHandler>()
+       }
+
+       @Test
+       fun `inserting-image notification is created as singleton`() {
+               injector.verifySingletonInstance<ListNotification<Image>>(named("imageInserting"))
+       }
+
+       @Test
+       fun `inserting-image notification has correct ID`() {
+               assertThat(injector.getInstance<ListNotification<Image>>(named("imageInserting")).id, equalTo("inserting-images-notification"))
+       }
+
+       @Test
+       fun `inserting-image notification is dismissable`() {
+               assertThat(injector.getInstance<ListNotification<Image>>(named("imageInserting")).isDismissable, equalTo(true))
+       }
+
+       @Test
+       fun `inserting-image notification loads correct template`() {
+               loaders.templates += "/templates/notify/inserting-images-notification.html" to "<% images>".asTemplate()
+               val notification = injector.getInstance<ListNotification<Image>>(named("imageInserting"))
+               val images = listOf(ImageImpl(), ImageImpl()).onEach(notification::add)
+               assertThat(notification.render(), equalTo(images.toString()))
+       }
+
+       @Test
+       fun `inserting-image-failed notification is created as singleton`() {
+               injector.verifySingletonInstance<ListNotification<Image>>(named("imageFailed"))
+       }
+
+       @Test
+       fun `inserting-image-failed notification has correct ID`() {
+               assertThat(injector.getInstance<ListNotification<Image>>(named("imageFailed")).id, equalTo("image-insert-failed-notification"))
+       }
+
+       @Test
+       fun `inserting-image-failed notification is dismissable`() {
+               assertThat(injector.getInstance<ListNotification<Image>>(named("imageFailed")).isDismissable, equalTo(true))
+       }
+
+       @Test
+       fun `inserting-image-failed notification loads correct template`() {
+               loaders.templates += "/templates/notify/image-insert-failed-notification.html" to "<% images>".asTemplate()
+               val notification = injector.getInstance<ListNotification<Image>>(named("imageFailed"))
+               val images = listOf(ImageImpl(), ImageImpl()).onEach(notification::add)
+               assertThat(notification.render(), equalTo(images.toString()))
+       }
+
+       @Test
+       fun `inserted-image notification is created as singleton`() {
+               injector.verifySingletonInstance<ListNotification<Image>>(named("imageInserted"))
+       }
+
+       @Test
+       fun `inserted-image notification has correct ID`() {
+               assertThat(injector.getInstance<ListNotification<Image>>(named("imageInserted")).id, equalTo("inserted-images-notification"))
+       }
+
+       @Test
+       fun `inserted-image notification is dismissable`() {
+               assertThat(injector.getInstance<ListNotification<Image>>(named("imageInserted")).isDismissable, equalTo(true))
+       }
+
+       @Test
+       fun `inserted-image notification loads correct template`() {
+               loaders.templates += "/templates/notify/inserted-images-notification.html" to "<% images>".asTemplate()
+               val notification = injector.getInstance<ListNotification<Image>>(named("imageInserted"))
+               val images = listOf(ImageImpl(), ImageImpl()).onEach(notification::add)
+               assertThat(notification.render(), equalTo(images.toString()))
+       }
+
+       @Test
+       fun `image insert handler is created as singleton`() {
+               injector.verifySingletonInstance<ImageInsertHandler>()
+       }
+
+       @Test
+       fun `first-start notification is created as singleton`() {
+               injector.verifySingletonInstance<TemplateNotification>(named("firstStart"))
+       }
+
+       @Test
+       fun `first-start notification has correct ID`() {
+               assertThat(injector.getInstance<TemplateNotification>(named("firstStart")).id, equalTo("first-start-notification"))
+       }
+
+       @Test
+       fun `first-start notification is dismissable`() {
+               assertThat(injector.getInstance<TemplateNotification>(named("firstStart")).isDismissable, equalTo(true))
+       }
+
+       @Test
+       fun `first-start notification loads correct template`() {
+               loaders.templates += "/templates/notify/firstStartNotification.html" to "1".asTemplate()
+               val notification = injector.getInstance<TemplateNotification>(named("firstStart"))
+               assertThat(notification.render(), equalTo("1"))
+       }
+
+       @Test
+       fun `first-start handler is created as singleton`() {
+               injector.verifySingletonInstance<FirstStartHandler>()
+       }
+
+       @Test
+       fun `config-not-read notification is created as singleton`() {
+               injector.verifySingletonInstance<TemplateNotification>(named("configNotRead"))
+       }
+
+       @Test
+       fun `config-not-read notification has correct ID `() {
+               assertThat(injector.getInstance<TemplateNotification>(named("configNotRead")).id, equalTo("config-not-read-notification"))
+       }
+
+       @Test
+       fun `config-not-read notification is dismissable`() {
+               assertThat(injector.getInstance<TemplateNotification>(named("configNotRead")).isDismissable, equalTo(true))
+       }
+
+       @Test
+       fun `config-not-read notification loads correct template`() {
+               loaders.templates += "/templates/notify/configNotReadNotification.html" to "1".asTemplate()
+               val notification = injector.getInstance<TemplateNotification>(named("configNotRead"))
+               assertThat(notification.render(), equalTo("1"))
+       }
+
+       @Test
+       fun `config-not-read handler is created as singleton`() {
+               injector.verifySingletonInstance<ConfigNotReadHandler>()
+       }
+
+       @Test
+       fun `startup notification can be created`() {
+               injector.verifySingletonInstance<TemplateNotification>(named("startup"))
+       }
+
+       @Test
+       fun `startup notification has correct ID`() {
+               assertThat(injector.getInstance<TemplateNotification>(named("startup")).id, equalTo("startup-notification"))
+       }
+
+       @Test
+       fun `startup notification is dismissable`() {
+               assertThat(injector.getInstance<TemplateNotification>(named("startup")).isDismissable, equalTo(true))
+       }
+
+       @Test
+       fun `startup notification loads correct template`() {
+               loaders.templates += "/templates/notify/startupNotification.html" to "1".asTemplate()
+               val notification = injector.getInstance<TemplateNotification>(named("startup"))
+               assertThat(notification.render(), equalTo("1"))
+       }
+
+       @Test
+       fun `startup handler is created as singleton`() {
+               injector.verifySingletonInstance<StartupHandler>()
+       }
+
+       @Test
+       fun `web-of-trust notification is created as singleton`() {
+               injector.verifySingletonInstance<TemplateNotification>(named("webOfTrust"))
+       }
+
+       @Test
+       fun `web-of-trust notification has correct ID`() {
+               assertThat(injector.getInstance<TemplateNotification>(named("webOfTrust")).id, equalTo("wot-missing-notification"))
+       }
+
+       @Test
+       fun `web-of-trust notification is dismissable`() {
+               assertThat(injector.getInstance<TemplateNotification>(named("webOfTrust")).isDismissable, equalTo(true))
+       }
+
+       @Test
+       fun `web-of-trust notification loads correct template`() {
+               loaders.templates += "/templates/notify/wotMissingNotification.html" to "1".asTemplate()
+               val notification = injector.getInstance<TemplateNotification>(named("webOfTrust"))
+               assertThat(notification.render(), equalTo("1"))
+       }
+
+       @Test
+       fun `web-of-trust handler is created as singleton`() {
+               injector.verifySingletonInstance<TemplateNotification>(named("webOfTrust"))
+       }
+
+       @Test
+       fun `web-of-trust reacher is created as singleton`() {
+               injector.verifySingletonInstance<Runnable>(named("webOfTrustReacher"))
+       }
+
+       @Test
+       fun `web-of-trust reacher access the wot connector`() {
+               injector.getInstance<Runnable>(named("webOfTrustReacher")).run()
+               verify(webOfTrustConnector).ping()
+       }
+
+       @Test
+       fun `web-of-trust reschedule is created as singleton`() {
+               injector.verifySingletonInstance<Consumer<Runnable>>(named("webOfTrustReschedule"))
+       }
+
+       @Test
+       fun `web-of-trust reschedule schedules at the correct delay`() {
+               val webOfTrustPinger = injector.getInstance<WebOfTrustPinger>()
+               injector.getInstance<Consumer<Runnable>>(named("webOfTrustReschedule"))(webOfTrustPinger)
+               verify(ticker).schedule(ArgumentMatchers.eq(webOfTrustPinger), ArgumentMatchers.eq(15L), ArgumentMatchers.eq(SECONDS))
+       }
+
+       @Test
+       fun `sone mention detector is created as singleton`() {
+               assertThat(injector.getInstance<SoneMentionDetector>(), notNullValue())
+       }
+
+       @Test
+       fun `sone-mentioned notification is created as singleton`() {
+               injector.verifySingletonInstance<ListNotification<Post>>(named("soneMentioned"))
+       }
+
+       @Test
+       fun `sone-mentioned notification has correct ID`() {
+               assertThat(injector.getInstance<ListNotification<Post>>(named("soneMentioned")).id, equalTo("mention-notification"))
+       }
+
+       @Test
+       fun `sone-mentioned notification is not dismissable`() {
+               assertThat(injector.getInstance<ListNotification<Post>>(named("soneMentioned")).isDismissable, equalTo(false))
+       }
+
+       @Test
+       fun `sone-mentioned notification loads correct template`() {
+               loaders.templates += "/templates/notify/mentionNotification.html" to "<% posts>".asTemplate()
+               val notification = injector.getInstance<ListNotification<Post>>(named("soneMentioned"))
+               val posts = listOf(EmptyPost("1"), EmptyPost("2")).onEach(notification::add)
+               assertThat(notification.render(), equalTo(posts.toString()))
+       }
+
+       @Test
+       fun `sone-mentioned handler is created as singleton`() {
+               injector.verifySingletonInstance<SoneMentionedHandler>()
+       }
+
+       @Test
+       fun `sone insert notification supplier is created as singleton`() {
+               injector.verifySingletonInstance<SoneInsertNotificationSupplier>()
+       }
+
+       @Test
+       fun `sone insert notification template is loaded correctly`() {
+               loaders.templates += "/templates/notify/soneInsertNotification.html" to "foo".asTemplate()
+               injector.getInstance<SoneInsertNotificationSupplier>()
+                               .invoke(createRemoteSone())
+                               .render()
+                               .let { assertThat(it, equalTo("foo")) }
+       }
+
+       @Test
+       fun `sone notification supplier returns different notifications for different sones`() {
+               val supplier = injector.getInstance<SoneInsertNotificationSupplier>()
+               listOf(createRemoteSone(), createRemoteSone(), createRemoteSone())
+                               .map(supplier)
+                               .distinct()
+                               .let { assertThat(it, hasSize(3)) }
+       }
+
+       @Test
+       fun `sone notification supplier caches notifications for a sone`() {
+               val supplier = injector.getInstance<SoneInsertNotificationSupplier>()
+               val sone = createRemoteSone()
+               listOf(sone, sone, sone)
+                               .map(supplier)
+                               .distinct()
+                               .let { assertThat(it, hasSize(1)) }
+       }
+
+       @Test
+       fun `sone notification supplier sets sone in notification template`() {
+               val supplier = injector.getInstance<SoneInsertNotificationSupplier>()
+               val sone = createRemoteSone()
+               val templateNotification = supplier(sone)
+               assertThat(templateNotification["insertSone"], sameInstance<Any>(sone))
+       }
+
+       @Test
+       fun `sone insert handler is created as singleton`() {
+               injector.verifySingletonInstance<SoneInsertHandler>()
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/notification/NotificationHandlerTester.kt b/src/test/kotlin/net/pterodactylus/sone/web/notification/NotificationHandlerTester.kt
new file mode 100644 (file)
index 0000000..6a06ef8
--- /dev/null
@@ -0,0 +1,58 @@
+/**
+ * Sone - NotificationHandlerTester.kt - Copyright © 2019–2020 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.notification
+
+import com.google.common.eventbus.*
+import net.pterodactylus.util.notify.*
+
+/**
+ * Helper for testing event handlers that deal with notifications. It contains
+ * a notification manager and an [event bus][EventBus] and automatically
+ * registers the created handler on the event bus.
+ *
+ * ```
+ * val notification = SomeNotification()
+ * val notificationTester = NotificationTester { SomeHandler(it, notification) }
+ *
+ * fun test() {
+ *     notificationTester.sendEvent(SomeEvent())
+ *     assertThat(notificationTester.elements, hasItem(notification))
+ * }
+ * ```
+ */
+@Suppress("UnstableApiUsage")
+class NotificationHandlerTester(createHandler: (NotificationManager) -> Any) {
+
+       private val eventBus = EventBus()
+       private val notificationManager = NotificationManager()
+
+       /** Returns all notifications of the notification manager. */
+       val notifications: Set<Notification>
+               get() = notificationManager.notifications
+
+       init {
+               eventBus.register(createHandler(notificationManager))
+       }
+
+       /** Sends an event to the event bus. */
+       fun sendEvent(event: Any) = eventBus.post(event)
+
+       /** Sets the first-start notification on the notification manager. */
+       fun firstStart() = notificationManager.firstStart()
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/notification/RemotePostReplyHandlerTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/notification/RemotePostReplyHandlerTest.kt
new file mode 100644 (file)
index 0000000..e4eb14f
--- /dev/null
@@ -0,0 +1,93 @@
+/**
+ * Sone - RemotePostReplyHandlerTest.kt - Copyright © 2020 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.notification
+
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.sone.data.*
+import net.pterodactylus.sone.notify.*
+import net.pterodactylus.sone.test.*
+import net.pterodactylus.util.notify.*
+import net.pterodactylus.util.template.*
+import org.hamcrest.MatcherAssert.*
+import org.hamcrest.Matchers.*
+import kotlin.test.*
+
+/**
+ * Unit test for [RemotePostReplyHandler].
+ */
+class RemotePostReplyHandlerTest {
+
+       private val notification = ListNotification<PostReply>("", "", Template())
+       private val notificationHandlerTester = NotificationHandlerTester { RemotePostReplyHandler(it, notification) }
+       private val postReply = emptyPostReply()
+
+       @Test
+       fun `reply is added to notification on new reply`() {
+               notificationHandlerTester.sendEvent(NewPostReplyFoundEvent(postReply))
+               assertThat(notification.elements, hasItem<PostReply>(postReply))
+       }
+
+       @Test
+       fun `notification is added to manager on new reply`() {
+               notificationHandlerTester.sendEvent(NewPostReplyFoundEvent(postReply))
+               assertThat(notificationHandlerTester.notifications, hasItem<Notification>(notification))
+       }
+
+       @Test
+       fun `reply is not added to notification on new reply during first start`() {
+               notificationHandlerTester.firstStart()
+               notificationHandlerTester.sendEvent(NewPostReplyFoundEvent(postReply))
+               assertThat(notification.elements, not(hasItem<PostReply>(postReply)))
+       }
+
+       @Test
+       fun `notification is not added to manager on new reply during first start`() {
+               notificationHandlerTester.firstStart()
+               notificationHandlerTester.sendEvent(NewPostReplyFoundEvent(postReply))
+               assertThat(notificationHandlerTester.notifications, not(hasItem<Notification>(notification)))
+       }
+
+       @Test
+       fun `reply is not added to notification on new local reply`() {
+               val postReply = emptyPostReply(sone = localSone1)
+               notificationHandlerTester.sendEvent(NewPostReplyFoundEvent(postReply))
+               assertThat(notification.elements, not(hasItem<PostReply>(postReply)))
+       }
+
+       @Test
+       fun `notification is not added to manager on new local reply`() {
+               val postReply = emptyPostReply(sone = localSone1)
+               notificationHandlerTester.sendEvent(NewPostReplyFoundEvent(postReply))
+               assertThat(notificationHandlerTester.notifications, not(hasItem<Notification>(notification)))
+       }
+
+       @Test
+       fun `reply is removed from notification when removed`() {
+               notification.add(postReply)
+               notificationHandlerTester.sendEvent(PostReplyRemovedEvent(postReply))
+               assertThat(notification.elements, not(hasItem<PostReply>(postReply)))
+       }
+
+       @Test
+       fun `reply is removed from notification when marked as known`() {
+               notification.add(postReply)
+               notificationHandlerTester.sendEvent(MarkPostReplyKnownEvent(postReply))
+               assertThat(notification.elements, not(hasItem<PostReply>(postReply)))
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/notification/SoneInsertHandlerTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/notification/SoneInsertHandlerTest.kt
new file mode 100644 (file)
index 0000000..6a11a90
--- /dev/null
@@ -0,0 +1,112 @@
+/**
+ * Sone - SoneInsertHandlerTest.kt - Copyright © 2020 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.notification
+
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.sone.test.*
+import net.pterodactylus.util.notify.*
+import net.pterodactylus.util.template.*
+import org.hamcrest.MatcherAssert.*
+import org.hamcrest.Matchers.*
+import kotlin.test.*
+
+/**
+ * Unit test for [SoneInsertHandler].
+ */
+class SoneInsertHandlerTest {
+
+       private val localSone = createLocalSone()
+       private val notification1 = TemplateNotification(Template())
+       private val notification2 = TemplateNotification(Template())
+       private val soneInsertHandlerTester = NotificationHandlerTester {
+               SoneInsertHandler(it) { sone ->
+                       if (sone == localSone) notification1 else notification2
+               }
+       }
+
+       @Test
+       fun `handler adds notification to manager when sone insert starts`() {
+               localSone.options.isSoneInsertNotificationEnabled = true
+               soneInsertHandlerTester.sendEvent(SoneInsertingEvent(localSone))
+               assertThat(soneInsertHandlerTester.notifications, hasItem(notification1))
+       }
+
+       @Test
+       fun `handler sets sone status in notification when sone insert starts`() {
+               localSone.options.isSoneInsertNotificationEnabled = true
+               soneInsertHandlerTester.sendEvent(SoneInsertingEvent(localSone))
+               assertThat(notification1.get("soneStatus"), equalTo<Any>("inserting"))
+       }
+
+       @Test
+       fun `handler does not add notification to manager if option is disabled`() {
+               localSone.options.isSoneInsertNotificationEnabled = false
+               soneInsertHandlerTester.sendEvent(SoneInsertingEvent(localSone))
+               assertThat(soneInsertHandlerTester.notifications, not(hasItem(notification1)))
+       }
+
+       @Test
+       fun `handler adds notification to manager when sone insert finishes`() {
+               localSone.options.isSoneInsertNotificationEnabled = true
+               soneInsertHandlerTester.sendEvent(SoneInsertedEvent(localSone, 123456, ""))
+               assertThat(soneInsertHandlerTester.notifications, hasItem(notification1))
+       }
+
+       @Test
+       fun `handler sets sone status in notification when sone insert finishes`() {
+               localSone.options.isSoneInsertNotificationEnabled = true
+               soneInsertHandlerTester.sendEvent(SoneInsertedEvent(localSone, 123456, ""))
+               assertThat(notification1.get("soneStatus"), equalTo<Any>("inserted"))
+       }
+
+       @Test
+       fun `handler sets insert duration in notification when sone insert finishes`() {
+               localSone.options.isSoneInsertNotificationEnabled = true
+               soneInsertHandlerTester.sendEvent(SoneInsertedEvent(localSone, 123456, ""))
+               assertThat(notification1.get("insertDuration"), equalTo<Any>(123L))
+       }
+
+       @Test
+       fun `handler does not add notification for finished insert to manager if option is disabled`() {
+               localSone.options.isSoneInsertNotificationEnabled = false
+               soneInsertHandlerTester.sendEvent(SoneInsertedEvent(localSone, 123456, ""))
+               assertThat(soneInsertHandlerTester.notifications, not(hasItem(notification1)))
+       }
+
+       @Test
+       fun `handler adds notification to manager when sone insert aborts`() {
+               localSone.options.isSoneInsertNotificationEnabled = true
+               soneInsertHandlerTester.sendEvent(SoneInsertAbortedEvent(localSone, Exception()))
+               assertThat(soneInsertHandlerTester.notifications, hasItem(notification1))
+       }
+
+       @Test
+       fun `handler sets sone status in notification when sone insert aborts`() {
+               localSone.options.isSoneInsertNotificationEnabled = true
+               soneInsertHandlerTester.sendEvent(SoneInsertAbortedEvent(localSone, Exception()))
+               assertThat(notification1.get("soneStatus"), equalTo<Any>("insert-aborted"))
+       }
+
+       @Test
+       fun `handler does not add notification for aborted insert to manager if option is disabled`() {
+               localSone.options.isSoneInsertNotificationEnabled = false
+               soneInsertHandlerTester.sendEvent(SoneInsertAbortedEvent(localSone, Exception()))
+               assertThat(soneInsertHandlerTester.notifications, not(hasItem(notification1)))
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/notification/SoneLockedHandlerTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/notification/SoneLockedHandlerTest.kt
new file mode 100644 (file)
index 0000000..4003dbb
--- /dev/null
@@ -0,0 +1,116 @@
+/**
+ * Sone - SoneLockedHandlerTest.kt - Copyright © 2019–2020 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.notification
+
+import com.google.common.eventbus.*
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.sone.data.*
+import net.pterodactylus.sone.data.impl.*
+import net.pterodactylus.sone.notify.*
+import net.pterodactylus.sone.utils.*
+import net.pterodactylus.util.notify.*
+import net.pterodactylus.util.template.*
+import org.hamcrest.MatcherAssert.*
+import org.hamcrest.Matchers.*
+import java.util.concurrent.*
+import kotlin.test.*
+
+/**
+ * Unit test for [SoneLockedHandler].
+ */
+@Suppress("UnstableApiUsage")
+class SoneLockedHandlerTest {
+
+       private val eventBus = EventBus()
+       private val notificationManager = NotificationManager()
+       private val notification = ListNotification<Sone>("", "", Template())
+       private val executor = TestScheduledThreadPoolExecutor()
+
+       init {
+               SoneLockedHandler(notificationManager, notification, executor).also(eventBus::register)
+       }
+
+       @AfterTest
+       fun shutdownExecutor() = executor.shutdown()
+
+       @Test
+       fun `notification is not added before the command is run`() {
+               eventBus.post(SoneLockedEvent(sone))
+               assertThat(notificationManager.notifications, emptyIterable())
+       }
+
+       @Test
+       fun `sone is added to notification immediately`() {
+               eventBus.post(SoneLockedEvent(sone))
+               assertThat(notification.elements, contains(sone))
+       }
+
+       @Test
+       fun `notification is added to notification manager from command`() {
+               eventBus.post(SoneLockedEvent(sone))
+               executor.scheduleds.single().command()
+               assertThat(notificationManager.notifications, contains<Any>(notification))
+       }
+
+       @Test
+       fun `command is registered with a delay of five minutes`() {
+               eventBus.post(SoneLockedEvent(sone))
+               with(executor.scheduleds.single()) {
+                       assertThat(timeUnit.toNanos(delay), equalTo(TimeUnit.MINUTES.toNanos(5)))
+               }
+       }
+
+       @Test
+       fun `unlocking sone after locking will cancel the future`() {
+               eventBus.post(SoneLockedEvent(sone))
+               eventBus.post(SoneUnlockedEvent(sone))
+               assertThat(executor.scheduleds.first().future.isCancelled, equalTo(true))
+       }
+
+       @Test
+       fun `unlocking sone after locking will remove the sone from the notification`() {
+               eventBus.post(SoneLockedEvent(sone))
+               eventBus.post(SoneUnlockedEvent(sone))
+               assertThat(notification.elements, emptyIterable())
+       }
+
+       @Test
+       fun `unlocking sone after showing the notification will remove the sone from the notification`() {
+               eventBus.post(SoneLockedEvent(sone))
+               executor.scheduleds.single().command()
+               eventBus.post(SoneUnlockedEvent(sone))
+               assertThat(notification.elements, emptyIterable())
+       }
+
+       @Test
+       fun `locking two sones will cancel the first command`() {
+               eventBus.post(SoneLockedEvent(sone))
+               eventBus.post(SoneLockedEvent(sone))
+               assertThat(executor.scheduleds.first().future.isCancelled, equalTo(true))
+       }
+
+       @Test
+       fun `locking two sones will schedule a second command`() {
+               eventBus.post(SoneLockedEvent(sone))
+               eventBus.post(SoneLockedEvent(sone))
+               assertThat(executor.scheduleds[1], notNullValue())
+       }
+
+}
+
+private val sone: Sone = IdOnlySone("sone")
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/notification/SoneLockedOnStartupHandlerTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/notification/SoneLockedOnStartupHandlerTest.kt
new file mode 100644 (file)
index 0000000..5ffac88
--- /dev/null
@@ -0,0 +1,59 @@
+/**
+ * Sone - SoneLockedOnStartupHandlerTest.kt - Copyright © 2019–2020 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.notification
+
+import com.google.common.eventbus.*
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.sone.data.*
+import net.pterodactylus.sone.data.impl.*
+import net.pterodactylus.sone.notify.*
+import net.pterodactylus.util.notify.*
+import net.pterodactylus.util.template.*
+import org.hamcrest.MatcherAssert.*
+import org.hamcrest.Matchers.*
+import kotlin.test.*
+
+/**
+ * Unit test for [SoneLockedOnStartupHandler].
+ */
+class SoneLockedOnStartupHandlerTest {
+
+       @Suppress("UnstableApiUsage")
+       private val eventBus = EventBus()
+       private val manager = NotificationManager()
+       private val notification = ListNotification<Sone>("", "", Template())
+
+       init {
+               SoneLockedOnStartupHandler(manager, notification).also(eventBus::register)
+       }
+
+       @Test
+       fun `handler adds sone to notification when event is posted`() {
+               eventBus.post(SoneLockedOnStartup(sone))
+               assertThat(notification.elements, contains<Any>(sone))
+       }
+
+       @Test
+       fun `handler adds notification to manager`() {
+               eventBus.post(SoneLockedOnStartup(sone))
+               assertThat(manager.notifications, contains<Notification>(notification))
+       }
+
+}
+
+private val sone = IdOnlySone("sone-id")
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/notification/SoneMentionedHandlerTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/notification/SoneMentionedHandlerTest.kt
new file mode 100644 (file)
index 0000000..4e91aa5
--- /dev/null
@@ -0,0 +1,87 @@
+/**
+ * Sone - SoneMentionedHandlerTest.kt - Copyright © 2020 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.notification
+
+import com.google.common.eventbus.*
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.sone.data.*
+import net.pterodactylus.sone.data.Post.*
+import net.pterodactylus.sone.notify.*
+import net.pterodactylus.util.notify.*
+import net.pterodactylus.util.template.*
+import org.hamcrest.MatcherAssert.*
+import org.hamcrest.Matchers.*
+import kotlin.test.*
+
+/**
+ * Unit test for [SoneMentionedHandler].
+ */
+@Suppress("UnstableApiUsage")
+class SoneMentionedHandlerTest {
+
+       private val notificationManager = NotificationManager()
+       private val notification = ListNotification<Post>("", "", Template())
+       private val eventBus = EventBus()
+
+       init {
+               eventBus.register(SoneMentionedHandler(notificationManager, notification))
+       }
+
+       @Test
+       fun `handler adds notification to manager on event`() {
+               eventBus.post(MentionOfLocalSoneFoundEvent(post))
+               assertThat(notificationManager.notifications, contains<Notification>(notification))
+       }
+
+       @Test
+       fun `handler adds post to notification on event`() {
+               eventBus.post(MentionOfLocalSoneFoundEvent(post))
+               assertThat(notification.elements, contains<Post>(post))
+       }
+
+       @Test
+       fun `handler does not add notification during first start`() {
+               notificationManager.firstStart()
+               eventBus.post(MentionOfLocalSoneFoundEvent(post))
+               assertThat(notificationManager.notifications, not(hasItem<Notification>(notification)))
+       }
+
+       @Test
+       fun `handler does not add post to notification during first start`() {
+               notificationManager.firstStart()
+               eventBus.post(MentionOfLocalSoneFoundEvent(post))
+               assertThat(notification.elements, not(hasItem<Post>(post)))
+       }
+
+       @Test
+       fun `handler removes post from notification`() {
+               notification.add(post)
+               eventBus.post(MentionOfLocalSoneRemovedEvent(post))
+               assertThat(notification.elements, not(hasItem(post)))
+       }
+
+       @Test
+       fun `handler removes notification from manager`() {
+               notificationManager.addNotification(notification)
+               eventBus.post(MentionOfLocalSoneRemovedEvent(post))
+               assertThat(notificationManager.notifications, not(hasItem<Notification>(notification)))
+       }
+
+}
+
+private val post = EmptyPost("")
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/notification/StartupHandlerTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/notification/StartupHandlerTest.kt
new file mode 100644 (file)
index 0000000..aaeea90
--- /dev/null
@@ -0,0 +1,66 @@
+/**
+ * Sone - StartupHandlerTest.kt - Copyright © 2019–2020 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.notification
+
+import com.google.common.eventbus.*
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.sone.utils.*
+import net.pterodactylus.util.notify.*
+import net.pterodactylus.util.template.*
+import org.hamcrest.MatcherAssert.*
+import org.hamcrest.Matchers.*
+import java.util.concurrent.TimeUnit.*
+import kotlin.test.*
+
+/**
+ * Unit test for [StartupHandler].
+ */
+class StartupHandlerTest {
+
+       private val eventBus = EventBus()
+       private val notificationManager = NotificationManager()
+       private val notification = TemplateNotification("", Template())
+       private val executor = TestScheduledThreadPoolExecutor()
+
+       init {
+               eventBus.register(StartupHandler(notificationManager, notification, executor))
+       }
+
+       @AfterTest
+       fun shutdownExecutor() = executor.shutdown()
+
+       @Test
+       fun `handler adds notification to manager on startup`() {
+               eventBus.post(Startup())
+               assertThat(notificationManager.notifications, contains<Notification>(notification))
+       }
+
+       @Test
+       fun `handler registers command on with 2-minute delay`() {
+               eventBus.post(Startup())
+               assertThat(with(executor.scheduleds.single()) { timeUnit.toNanos(delay) }, equalTo(MINUTES.toNanos(2)))
+       }
+
+       @Test
+       fun `registered command removes notification from manager`() {
+               eventBus.post(Startup())
+               executor.scheduleds.single().command()
+               assertThat(notificationManager.notifications, emptyIterable())
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/notification/Testing.kt b/src/test/kotlin/net/pterodactylus/sone/web/notification/Testing.kt
new file mode 100644 (file)
index 0000000..68bb103
--- /dev/null
@@ -0,0 +1,45 @@
+/**
+ * Sone - Testing.kt - Copyright © 2019–2020 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.notification
+
+import net.pterodactylus.util.notify.*
+import java.io.*
+import java.util.concurrent.*
+
+/** Information about a scheduled runnable. */
+data class Scheduled(val command: Runnable, val delay: Long, val timeUnit: TimeUnit, val future: ScheduledFuture<*>)
+
+/**
+ * [ScheduledThreadPoolExecutor] extension that stores parameters and return
+ * values for the [ScheduledThreadPoolExecutor.schedule] method.
+ */
+class TestScheduledThreadPoolExecutor : ScheduledThreadPoolExecutor(1) {
+
+       val scheduleds = mutableListOf<Scheduled>()
+
+       override fun schedule(command: Runnable, delay: Long, unit: TimeUnit): ScheduledFuture<*> =
+                       super.schedule(command, delay, unit)
+                                       .also { scheduleds += Scheduled(command, delay, unit, it) }
+
+}
+
+fun NotificationManager.firstStart() {
+       addNotification(object : AbstractNotification("first-start-notification") {
+               override fun render(writer: Writer?) = Unit
+       })
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/notification/WebOfTrustHandlerTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/notification/WebOfTrustHandlerTest.kt
new file mode 100644 (file)
index 0000000..8981a47
--- /dev/null
@@ -0,0 +1,54 @@
+/**
+ * Sone - WebOfTrustHandlerTest.kt - Copyright © 2019–2020 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.notification
+
+import com.google.common.eventbus.*
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.util.notify.*
+import net.pterodactylus.util.template.*
+import org.hamcrest.MatcherAssert.*
+import org.hamcrest.Matchers.*
+import kotlin.test.*
+
+/**
+ * Unit test for [WebOfTrustHandler].
+ */
+class WebOfTrustHandlerTest {
+
+       private val eventBus = EventBus()
+       private val notificationManager = NotificationManager()
+       private val notification = TemplateNotification("", Template())
+
+       init {
+               eventBus.register(WebOfTrustHandler(notificationManager, notification))
+       }
+
+       @Test
+       fun `handler adds notification if wot goes down`() {
+               eventBus.post(WebOfTrustDisappeared())
+               assertThat(notificationManager.notifications, contains<Notification>(notification))
+       }
+
+       @Test
+       fun `handler removes notification if wot appears`() {
+               notificationManager.addNotification(notification)
+               eventBus.post(WebOfTrustAppeared())
+               assertThat(notificationManager.notifications, emptyIterable())
+       }
+
+}
index e05ad45..1e3caab 100644 (file)
@@ -1,15 +1,11 @@
 package net.pterodactylus.sone.web.page
 
 import freenet.clients.http.*
-import freenet.clients.http.SessionManager.*
-import freenet.l10n.*
 import freenet.support.api.*
-import net.pterodactylus.sone.test.*
 import net.pterodactylus.util.web.*
 import org.hamcrest.MatcherAssert.*
 import org.hamcrest.Matchers.*
 import org.junit.*
-import org.mockito.*
 import org.mockito.Mockito.*
 import java.net.*
 
@@ -19,9 +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 l10n = mock<BaseL10n>()
-       private val sessionManager = mock<SessionManager>()
-       private val request = FreenetRequest(uri, method, httpRequest, toadletContext, l10n, sessionManager)
+       private val request = FreenetRequest(uri, method, httpRequest, toadletContext)
 
        @Test
        fun `uri is retained correctly`() {
@@ -43,35 +37,4 @@ class FreenetRequestTest {
                assertThat(request.toadletContext, equalTo(toadletContext))
        }
 
-       @Test
-       fun `l10n is retained correctly`() {
-               assertThat(request.l10n, equalTo(l10n))
-       }
-
-       @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 50c6ce7..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,11 +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 l10n = mock<BaseL10n>()
-       private val sessionManager = mock<SessionManager>()
        private val core = mock<Core>()
        private val webInterface = mock<WebInterface>()
-       private val soneRequest = SoneRequest(uri, method, httpRequest, toadletContext, l10n, sessionManager, core, webInterface)
+       private val soneRequest = SoneRequest(uri, method, httpRequest, toadletContext, core, webInterface)
 
        @Test
        fun `freenet request properties are retained correctly`() {
@@ -31,8 +28,6 @@ class SoneRequestTest {
                assertThat(soneRequest.method, equalTo(method))
                assertThat(soneRequest.httpRequest, equalTo(httpRequest))
                assertThat(soneRequest.toadletContext, equalTo(toadletContext))
-               assertThat(soneRequest.l10n, equalTo(l10n))
-               assertThat(soneRequest.sessionManager, equalTo(sessionManager))
        }
 
        @Test
@@ -47,14 +42,12 @@ class SoneRequestTest {
 
        @Test
        fun `freenet request is wrapped correctly`() {
-           val freenetRequest = FreenetRequest(uri, method, httpRequest, toadletContext, l10n, sessionManager)
+           val freenetRequest = FreenetRequest(uri, method, httpRequest, toadletContext)
                val wrappedSoneRequest = freenetRequest.toSoneRequest(core, webInterface)
                assertThat(wrappedSoneRequest.uri, equalTo(uri))
                assertThat(wrappedSoneRequest.method, equalTo(method))
                assertThat(wrappedSoneRequest.httpRequest, equalTo(httpRequest))
                assertThat(wrappedSoneRequest.toadletContext, equalTo(toadletContext))
-               assertThat(wrappedSoneRequest.l10n, equalTo(l10n))
-               assertThat(wrappedSoneRequest.sessionManager, equalTo(sessionManager))
                assertThat(wrappedSoneRequest.core, sameInstance(core))
                assertThat(wrappedSoneRequest.webInterface, sameInstance(webInterface))
        }
index 2bf0c05..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.*
@@ -40,7 +38,7 @@ class CreatePostPageTest : WebPageTest(::CreatePostPage) {
                addHttpRequestPart("returnPage", "return.html")
                addHttpRequestPart("text", "post text")
                verifyRedirect("return.html") {
-                       verify(core).createPost(currentSone, absent(), "post text")
+                       verify(core).createPost(currentSone, null, "post text")
                }
        }
 
@@ -62,7 +60,7 @@ class CreatePostPageTest : WebPageTest(::CreatePostPage) {
                val sender = mock<Sone>()
                addLocalSone("sender-id", sender)
                verifyRedirect("return.html") {
-                       verify(core).createPost(sender, absent(), "post text")
+                       verify(core).createPost(sender, null, "post text")
                }
        }
 
@@ -75,7 +73,7 @@ class CreatePostPageTest : WebPageTest(::CreatePostPage) {
                val recipient = mock<Sone>()
                addSone("recipient-id", recipient)
                verifyRedirect("return.html") {
-                       verify(core).createPost(currentSone, recipient.asOptional(), "post text")
+                       verify(core).createPost(currentSone, recipient, "post text")
                }
        }
 
@@ -86,7 +84,7 @@ class CreatePostPageTest : WebPageTest(::CreatePostPage) {
                addHttpRequestPart("text", "post http://localhost:12345/KSK@foo text")
                addHttpRequestHeader("Host", "localhost:12345")
                verifyRedirect("return.html") {
-                       verify(core).createPost(currentSone, absent(), "post KSK@foo text")
+                       verify(core).createPost(currentSone, null, "post KSK@foo text")
                }
        }
 
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/pages/DebugPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/pages/DebugPageTest.kt
new file mode 100644 (file)
index 0000000..2424b2d
--- /dev/null
@@ -0,0 +1,69 @@
+/**
+ * Sone - DebugPageTest.kt - Copyright © 2019–2020 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.pages
+
+import net.pterodactylus.sone.test.*
+import net.pterodactylus.sone.web.*
+import net.pterodactylus.sone.web.WebTestUtils.*
+import net.pterodactylus.sone.web.page.FreenetTemplatePage.*
+import org.hamcrest.MatcherAssert.*
+import org.hamcrest.Matchers.*
+import org.junit.Rule
+import org.junit.rules.*
+import org.junit.rules.ExpectedException.*
+import org.mockito.Mockito.*
+import kotlin.test.*
+
+class DebugPageTest : WebPageTest(::DebugPage) {
+
+       @Rule
+       @JvmField
+       val expectedException: ExpectedException = none()
+
+       @Test
+       fun `page returns correct path`() {
+               assertThat(page.path, equalTo("debug"))
+       }
+
+       @Test
+       fun `page does not require login`() {
+               assertThat(page.requiresLogin(), equalTo(false))
+       }
+
+       @Test
+       fun `page can be created by dependency injection`() {
+               assertThat(baseInjector.getInstance<DebugPage>(), notNullValue())
+       }
+
+       @Test
+       fun `get request activates debug mode`() {
+               try {
+                       page.handleRequest(soneRequest, templateContext)
+               } catch (_: RedirectException) {
+               }
+               verify(core).setDebug()
+       }
+
+       @Test
+       fun `get request redirects to index`() {
+               expectedException.expect(redirectsTo("./"))
+               page.handleRequest(soneRequest, templateContext)
+       }
+
+}
+
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 21cfdfc..5bcdd6a 100644 (file)
@@ -26,7 +26,7 @@ class DeleteSonePageTest : WebPageTest(::DeleteSonePage) {
 
        @Test
        fun `page returns correct title`() {
-               whenever(l10n.getString("Page.DeleteSone.Title")).thenReturn("delete sone page")
+               addTranslation("Page.DeleteSone.Title", "delete sone page")
                assertThat(page.getPageTitle(soneRequest), equalTo("delete sone page"))
        }
 
index e8583df..7893210 100644 (file)
@@ -28,7 +28,7 @@ class DismissNotificationPageTest : WebPageTest(::DismissNotificationPage) {
 
        @Test
        fun `page returns correct title`() {
-               whenever(l10n.getString("Page.DismissNotification.Title")).thenReturn("dismiss notification page")
+               addTranslation("Page.DismissNotification.Title", "dismiss notification page")
                assertThat(page.getPageTitle(soneRequest), equalTo("dismiss notification page"))
        }
 
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/pages/DistrustPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/pages/DistrustPageTest.kt
deleted file mode 100644 (file)
index d706e05..0000000
+++ /dev/null
@@ -1,62 +0,0 @@
-package net.pterodactylus.sone.web.pages
-
-import net.pterodactylus.sone.data.*
-import net.pterodactylus.sone.test.*
-import net.pterodactylus.sone.web.*
-import net.pterodactylus.util.web.Method.*
-import org.hamcrest.MatcherAssert.*
-import org.hamcrest.Matchers.*
-import org.junit.*
-import org.mockito.Mockito.*
-
-/**
- * Unit test for [DistrustPage].
- */
-class DistrustPageTest : WebPageTest(::DistrustPage) {
-
-       @Test
-       fun `page returns correct path`() {
-               assertThat(page.path, equalTo("distrust.html"))
-       }
-
-       @Test
-       fun `page requires login`() {
-               assertThat(page.requiresLogin(), equalTo(true))
-       }
-
-       @Test
-       fun `page returns correct title`() {
-               whenever(l10n.getString("Page.Distrust.Title")).thenReturn("distrust page title")
-               assertThat(page.getPageTitle(soneRequest), equalTo("distrust page title"))
-       }
-
-       @Test
-       fun `get request does not redirect`() {
-               page.processTemplate(freenetRequest, templateContext)
-       }
-
-       @Test
-       fun `post request with invalid sone redirects to return page`() {
-               setMethod(POST)
-               addHttpRequestPart("returnPage", "return.html")
-               verifyRedirect("return.html")
-       }
-
-       @Test
-       fun `post request with valid sone distrusts sone and redirects to return page`() {
-               setMethod(POST)
-               val remoteSone = mock<Sone>()
-               addSone("remote-sone-id", remoteSone)
-               addHttpRequestPart("returnPage", "return.html")
-               addHttpRequestPart("sone", "remote-sone-id")
-               verifyRedirect("return.html") {
-                       verify(core).distrustSone(currentSone, remoteSone)
-               }
-       }
-
-       @Test
-       fun `page can be created by dependency injection`() {
-               assertThat(baseInjector.getInstance<DistrustPage>(), notNullValue())
-       }
-
-}
index 4d435b4..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")
        }
 
@@ -45,7 +39,7 @@ class EditAlbumPageTest : WebPageTest(::EditAlbumPage) {
 
        @Test
        fun `page returns correct title`() {
-               whenever(l10n.getString("Page.EditAlbum.Title")).thenReturn("edit album page")
+               addTranslation("Page.EditAlbum.Title", "edit album page")
                assertThat(page.getPageTitle(soneRequest), equalTo("edit album page"))
        }
 
@@ -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 0961622..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
@@ -43,7 +40,7 @@ class EditImagePageTest : WebPageTest(::EditImagePage) {
 
        @Test
        fun `page returns correct title`() {
-               whenever(l10n.getString("Page.EditImage.Title")).thenReturn("edit image page title")
+               addTranslation("Page.EditImage.Title", "edit image page title")
                assertThat(page.getPageTitle(soneRequest), equalTo("edit image page title"))
        }
 
@@ -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 7731fa2..4a84644 100644 (file)
@@ -35,7 +35,7 @@ class EditProfileFieldPageTest : WebPageTest(::EditProfileFieldPage) {
 
        @Test
        fun `page returns correct title`() {
-               whenever(l10n.getString("Page.EditProfileField.Title")).thenReturn("edit profile field title")
+               addTranslation("Page.EditProfileField.Title", "edit profile field title")
                assertThat(page.getPageTitle(soneRequest), equalTo("edit profile field title"))
        }
 
index a31f04c..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)
        }
 
@@ -46,7 +44,7 @@ class EditProfilePageTest : WebPageTest(::EditProfilePage) {
 
        @Test
        fun `page returns correct title`() {
-               whenever(l10n.getString("Page.EditProfile.Title")).thenReturn("edit profile page title")
+               addTranslation("Page.EditProfile.Title", "edit profile page title")
                assertThat(page.getPageTitle(soneRequest), equalTo("edit profile page title"))
        }
 
@@ -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 ba1dc42..3873434 100644 (file)
@@ -30,7 +30,7 @@ class FollowSonePageTest : WebPageTest(::FollowSonePage) {
 
        @Test
        fun `page returns correct title`() {
-               whenever(l10n.getString("Page.FollowSone.Title")).thenReturn("follow sone page title")
+               addTranslation("Page.FollowSone.Title", "follow sone page title")
                assertThat(page.getPageTitle(soneRequest), equalTo("follow sone page title"))
        }
 
index a0e9372..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.*
@@ -26,13 +28,13 @@ class ImageBrowserPageTest : WebPageTest(::ImageBrowserPage) {
 
        @Test
        fun `page returns correct title`() {
-               whenever(l10n.getString("Page.ImageBrowser.Title")).thenReturn("image browser page title")
+               addTranslation("Page.ImageBrowser.Title", "image browser page title")
                assertThat(page.getPageTitle(soneRequest), equalTo("image browser page title"))
        }
 
        @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 9104127..8c86365 100644 (file)
@@ -34,7 +34,7 @@ class IndexPageTest : WebPageTest({ webInterface, loaders, templateRenderer -> I
 
        @Test
        fun `page returns correct title`() {
-               whenever(l10n.getString("Page.Index.Title")).thenReturn("index page title")
+               addTranslation("Page.Index.Title", "index page title")
                assertThat(page.getPageTitle(soneRequest), equalTo("index page title"))
        }
 
index 37e0e95..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())
@@ -73,7 +75,7 @@ class KnownSonesPageTest : WebPageTest(::KnownSonesPage) {
 
        @Test
        fun `page returns correct title`() {
-               whenever(l10n.getString("Page.KnownSones.Title")).thenReturn("known sones page title")
+               addTranslation("Page.KnownSones.Title", "known sones page title")
                assertThat(page.getPageTitle(soneRequest), equalTo("known sones page title"))
        }
 
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/pages/MetricsPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/pages/MetricsPageTest.kt
new file mode 100644 (file)
index 0000000..4d26b6c
--- /dev/null
@@ -0,0 +1,85 @@
+/**
+ * Sone - MetricsPageTest.kt - Copyright © 2019–2020 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.pages
+
+import com.codahale.metrics.*
+import net.pterodactylus.sone.test.*
+import net.pterodactylus.sone.web.*
+import net.pterodactylus.sone.web.page.*
+import org.hamcrest.MatcherAssert.*
+import org.hamcrest.Matchers.*
+import kotlin.test.*
+
+class MetricsPageTest : WebPageTest() {
+
+       private val metricRegistry = MetricRegistry()
+       override val page by lazy { MetricsPage(webInterface, loaders, templateRenderer, metricRegistry) }
+
+       @Test
+       fun `page returns correct path`() {
+               assertThat(page.path, equalTo("metrics.html"))
+       }
+
+       @Test
+       fun `page does not require login`() {
+               assertThat(page.requiresLogin(), equalTo(false))
+       }
+
+       @Test
+       fun `page returns correct title`() {
+               addTranslation("Page.Metrics.Title", "metrics page title")
+               assertThat(page.getPageTitle(soneRequest), equalTo("metrics page title"))
+       }
+
+       @Test
+       fun `page can be created by dependency injection`() {
+               assertThat(baseInjector.getInstance<MetricsPage>(), notNullValue())
+       }
+
+       @Test
+       fun `page is annotated with the correct menuname`() {
+               assertThat(page.menuName, equalTo("Metrics"))
+       }
+
+       @Test
+       fun `page is annotated with correct template path`() {
+               assertThat(page.templatePath, equalTo("/templates/metrics.html"))
+       }
+
+       @Test
+       @Suppress("UNCHECKED_CAST")
+       fun `metrics page stores histograms in template context`() {
+               createHistogram("sone.random.duration2")
+               createHistogram("sone.random.duration1")
+               page.handleRequest(soneRequest, templateContext)
+               val histograms = templateContext["histograms"] as Map<String, Histogram>
+               assertThat(histograms.entries.map { it.key to it.value }, containsInAnyOrder(
+                               "sone.random.duration1" to metricRegistry.histogram("sone.random.duration1"),
+                               "sone.random.duration2" to metricRegistry.histogram("sone.random.duration2")
+               ))
+       }
+
+       private fun createHistogram(name: String) = metricRegistry.histogram(name).run {
+               update(10)
+               update(9)
+               update(1)
+               update(1)
+               update(8)
+       }
+
+}
index 71e1b7b..d679e85 100644 (file)
@@ -25,11 +25,9 @@ class OptionsPageTest : WebPageTest(::OptionsPage) {
                core.preferences.newImagesPerPage = 4
                core.preferences.newFcpInterfaceActive = true
                core.preferences.newRequireFullAccess = true
-               core.preferences.newNegativeTrust = 7
-               core.preferences.newPositiveTrust = 8
                core.preferences.newPostCutOffLength = 51
                core.preferences.newPostsPerPage = 10
-               core.preferences.newTrustComment = "11"
+               core.preferences.newStrictFiltering = true
        }
 
        @Before
@@ -77,11 +75,9 @@ class OptionsPageTest : WebPageTest(::OptionsPage) {
                        assertThat(templateContext["images-per-page"], equalTo<Any>(4))
                        assertThat(templateContext["fcp-interface-active"], equalTo<Any>(true))
                        assertThat(templateContext["require-full-access"], equalTo<Any>(true))
-                       assertThat(templateContext["negative-trust"], equalTo<Any>(7))
-                       assertThat(templateContext["positive-trust"], equalTo<Any>(8))
                        assertThat(templateContext["post-cut-off-length"], equalTo<Any>(51))
                        assertThat(templateContext["posts-per-page"], equalTo<Any>(10))
-                       assertThat(templateContext["trust-comment"], equalTo<Any>("11"))
+                       assertThat(templateContext["strict-filtering"], equalTo<Any>(true))
                }
        }
 
@@ -284,56 +280,6 @@ class OptionsPageTest : WebPageTest(::OptionsPage) {
        }
 
        @Test
-       fun `negative trust can not be set to -101`() {
-               verifyThatWrongValueForPreferenceIsDetected("negative-trust", "-101")
-       }
-
-       @Test
-       fun `negative trust can be set to -100`() {
-               verifyThatPreferencesCanBeSet("negative-trust", "-100", -100) { core.preferences.negativeTrust }
-       }
-
-       @Test
-       fun `negative trust can be set to 100`() {
-               verifyThatPreferencesCanBeSet("negative-trust", "100", 100) { core.preferences.negativeTrust }
-       }
-
-       @Test
-       fun `negative trust can not be set to 101`() {
-               verifyThatWrongValueForPreferenceIsDetected("negative-trust", "101")
-       }
-
-       @Test
-       fun `negative trust is set to default on invalid value`() {
-               verifyThatPreferencesCanBeSet("negative-trust", "invalid", -25) { core.preferences.negativeTrust }
-       }
-
-       @Test
-       fun `positive trust can not be set to -1`() {
-               verifyThatWrongValueForPreferenceIsDetected("positive-trust", "-1")
-       }
-
-       @Test
-       fun `positive trust can be set to 0`() {
-               verifyThatPreferencesCanBeSet("positive-trust", "0", 0) { core.preferences.positiveTrust }
-       }
-
-       @Test
-       fun `positive trust can be set to 100`() {
-               verifyThatPreferencesCanBeSet("positive-trust", "100", 100) { core.preferences.positiveTrust }
-       }
-
-       @Test
-       fun `positive trust can not be set to 101`() {
-               verifyThatWrongValueForPreferenceIsDetected("positive-trust", "101")
-       }
-
-       @Test
-       fun `positive trust is set to default on invalid value`() {
-               verifyThatPreferencesCanBeSet("positive-trust", "invalid", 75) { core.preferences.positiveTrust }
-       }
-
-       @Test
        fun `post cut off length can not be set to -49`() {
                verifyThatWrongValueForPreferenceIsDetected("post-cut-off-length", "-49")
        }
@@ -364,13 +310,13 @@ class OptionsPageTest : WebPageTest(::OptionsPage) {
        }
 
        @Test
-       fun `trust comment can be set`() {
-               verifyThatPreferencesCanBeSet("trust-comment", "trust", "trust") { core.preferences.trustComment }
+       fun `strict filtering can be set to true`() {
+               verifyThatPreferencesCanBeSet("strict-filtering", "checked", true) { core.preferences.strictFiltering }
        }
 
        @Test
-       fun `trust comment is set to default when set to empty value`() {
-               verifyThatPreferencesCanBeSet("trust-comment", "", "Set from Sone Web Interface") { core.preferences.trustComment }
+       fun `strict filtering can be set to false`() {
+               verifyThatPreferencesCanBeSet("strict-filtering", null, false) { core.preferences.strictFiltering }
        }
 
        @Test
index 8e29df1..cffd478 100644 (file)
@@ -31,7 +31,7 @@ class SoneTemplatePageTest : WebPageTest({ webInterface, loaders, templateRender
        @Test
        fun `page title is retrieved from l10n if page title key is given`() {
                SoneTemplatePage(webInterface, loaders, templateRenderer, pageTitleKey = "page.title", requiresLogin = false).let { page ->
-                       whenever(l10n.getString("page.title")).thenReturn("Page Title")
+                       addTranslation("page.title", "Page Title")
                        assertThat(page.getPageTitle(soneRequest), equalTo("Page Title"))
                }
        }
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/pages/TrustPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/pages/TrustPageTest.kt
deleted file mode 100644 (file)
index 990c1b7..0000000
+++ /dev/null
@@ -1,78 +0,0 @@
-package net.pterodactylus.sone.web.pages
-
-import net.pterodactylus.sone.data.*
-import net.pterodactylus.sone.test.getInstance
-import net.pterodactylus.sone.test.mock
-import net.pterodactylus.sone.web.*
-import net.pterodactylus.util.web.Method.*
-import org.hamcrest.MatcherAssert.*
-import org.hamcrest.Matchers.*
-import org.junit.*
-import org.mockito.ArgumentMatchers.any
-import org.mockito.ArgumentMatchers.eq
-import org.mockito.Mockito.never
-import org.mockito.Mockito.verify
-
-/**
- * Unit test for [TrustPage].
- */
-class TrustPageTest : WebPageTest(::TrustPage) {
-
-       @Test
-       fun `page returns correct path`() {
-               assertThat(page.path, equalTo("trust.html"))
-       }
-
-       @Test
-       fun `page requires login`() {
-               assertThat(page.requiresLogin(), equalTo(true))
-       }
-
-       @Test
-       fun `page returns correct title`() {
-               addTranslation("Page.Trust.Title", "title trust page")
-               assertThat(page.getPageTitle(soneRequest), equalTo("title trust page"))
-       }
-
-       @Test
-       fun `get method does not redirect`() {
-               verifyNoRedirect { }
-       }
-
-       @Test
-       fun `post request without sone redirects to return page`() {
-               setMethod(POST)
-               addHttpRequestPart("returnPage", "return.html")
-               verifyRedirect("return.html") {
-                       verify(core, never()).trustSone(eq(currentSone), any())
-               }
-       }
-
-       @Test
-       fun `post request with missing sone redirects to return page`() {
-               setMethod(POST)
-               addHttpRequestPart("returnPage", "return.html")
-               addHttpRequestPart("sone", "sone-id")
-               verifyRedirect("return.html") {
-                       verify(core, never()).trustSone(eq(currentSone), any())
-               }
-       }
-
-       @Test
-       fun `post request with existing sone trusts the identity and redirects to return page`() {
-               setMethod(POST)
-               addHttpRequestPart("returnPage", "return.html")
-               addHttpRequestPart("sone", "sone-id")
-               val sone = mock<Sone>()
-               addSone("sone-id", sone)
-               verifyRedirect("return.html") {
-                       verify(core).trustSone(eq(currentSone), eq(sone))
-               }
-       }
-
-       @Test
-       fun `page can be created by dependency injection`() {
-               assertThat(baseInjector.getInstance<TrustPage>(), notNullValue())
-       }
-
-}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/pages/UntrustPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/pages/UntrustPageTest.kt
deleted file mode 100644 (file)
index eb0ebdd..0000000
+++ /dev/null
@@ -1,80 +0,0 @@
-package net.pterodactylus.sone.web.pages
-
-import net.pterodactylus.sone.data.*
-import net.pterodactylus.sone.test.getInstance
-import net.pterodactylus.sone.test.mock
-import net.pterodactylus.sone.web.*
-import net.pterodactylus.util.web.Method.*
-import org.hamcrest.MatcherAssert.*
-import org.hamcrest.Matchers.*
-import org.junit.*
-import org.mockito.ArgumentMatchers.any
-import org.mockito.ArgumentMatchers.eq
-import org.mockito.Mockito.never
-import org.mockito.Mockito.verify
-
-/**
- * Unit test for [UntrustPage].
- */
-class UntrustPageTest : WebPageTest(::UntrustPage) {
-
-       @Test
-       fun `page returns correct path`() {
-               assertThat(page.path, equalTo("untrust.html"))
-       }
-
-       @Test
-       fun `page requires login`() {
-               assertThat(page.requiresLogin(), equalTo(true))
-       }
-
-       @Test
-       fun `page returns correct title`() {
-               addTranslation("Page.Untrust.Title", "untrust page title")
-               assertThat(page.getPageTitle(soneRequest), equalTo("untrust page title"))
-       }
-
-       @Test
-       fun `get request does not redirect`() {
-               verifyNoRedirect {
-                       verify(core, never()).untrustSone(eq(currentSone), any())
-               }
-       }
-
-       @Test
-       fun `post request without sone parameter does not untrust but redirects`() {
-               setMethod(POST)
-               addHttpRequestPart("returnPage", "return.html")
-               verifyRedirect("return.html") {
-                       verify(core, never()).untrustSone(eq(currentSone), any())
-               }
-       }
-
-       @Test
-       fun `post request with invalid sone parameter does not untrust but redirects`() {
-               setMethod(POST)
-               addHttpRequestPart("returnPage", "return.html")
-               addHttpRequestPart("sone", "no-sone")
-               verifyRedirect("return.html") {
-                       verify(core, never()).untrustSone(eq(currentSone), any())
-               }
-       }
-
-       @Test
-       fun `post request with valid sone parameter untrusts and redirects`() {
-               setMethod(POST)
-               addHttpRequestPart("returnPage", "return.html")
-               addHttpRequestPart("sone", "sone-id")
-               val sone = mock<Sone>()
-               addSone("sone-id", sone)
-               verifyRedirect("return.html") {
-                       verify(core).untrustSone(currentSone, sone)
-               }
-       }
-
-       @Test
-       fun `page can be created by dependency injection`() {
-               assertThat(baseInjector.getInstance<UntrustPage>(), 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 f51bece..badc2bc 100644 (file)
@@ -6,6 +6,7 @@ import freenet.support.*
 import freenet.support.api.*
 import net.pterodactylus.sone.core.*
 import net.pterodactylus.sone.data.*
+import net.pterodactylus.sone.freenet.*
 import net.pterodactylus.sone.freenet.wot.*
 import net.pterodactylus.sone.main.*
 import net.pterodactylus.sone.test.deepMock
@@ -26,6 +27,7 @@ import org.mockito.ArgumentMatchers.eq
 import java.io.*
 import java.net.*
 import java.nio.charset.*
+import java.util.*
 import kotlin.text.Charsets.UTF_8
 
 /**
@@ -40,17 +42,13 @@ open class WebPageTest(pageSupplier: (WebInterface, Loaders, TemplateRenderer) -
        val core = webInterface.core
        val eventBus = mock<EventBus>()
        val preferences = Preferences(eventBus)
-       val l10n = webInterface.l10n!!
-       val sessionManager = mock<SessionManager>()
 
-       val page by lazy { pageSupplier(webInterface, loaders, templateRenderer) }
+       open val page by lazy { pageSupplier(webInterface, loaders, templateRenderer) }
 
        val httpRequest = mock<HTTPRequest>()
        val freenetRequest = mock<FreenetRequest>()
 
        init {
-               whenever(freenetRequest.l10n).thenReturn(l10n)
-               whenever(freenetRequest.sessionManager).thenReturn(sessionManager)
                whenever(freenetRequest.uri).thenReturn(mock())
        }
 
@@ -77,12 +75,16 @@ open class WebPageTest(pageSupplier: (WebInterface, Loaders, TemplateRenderer) -
        private val notifications = mutableMapOf<String, Notification>()
        private val translations = mutableMapOf<String, String>()
 
+       private val translation = object : Translation {
+               override val currentLocale = Locale.ENGLISH
+               override fun translate(key: String) = translations[key] ?: key
+       }
+
        init {
                setupCore()
                setupWebInterface()
                setupHttpRequest()
                setupFreenetRequest()
-               setupTranslations()
        }
 
        private fun setupCore() {
@@ -102,12 +104,10 @@ 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)
        }
 
        private fun setupHttpRequest() {
@@ -147,10 +147,6 @@ open class WebPageTest(pageSupplier: (WebInterface, Loaders, TemplateRenderer) -
                whenever(freenetRequest.toadletContext).thenReturn(toadletContext)
        }
 
-       private fun setupTranslations() {
-               whenever(l10n.getString(anyString())).then { translations[it[0]] ?: it[0] }
-       }
-
        fun setMethod(method: Method) {
                whenever(httpRequest.method).thenReturn(method.name)
                whenever(freenetRequest.method).thenReturn(method)
@@ -174,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) {
index e627651..ec7a190 100644 (file)
@@ -5,7 +5,7 @@
        <profile></profile>
        <posts>
                <post>
-                       <id>post-id</id>
+                       <id>3de12680-afef-11e9-a124-e713cf8912fe</id>
                        <time>1407197508000</time>
                        <text>text</text>
                        <recipient>123456789012345678901234567890123456789012</recipient>
index 14dabe1..a6e07ed 100644 (file)
@@ -5,7 +5,7 @@
        <profile></profile>
        <posts>
                <post>
-                       <id>post-id</id>
+                       <id>3de12680-afef-11e9-a124-e713cf8912fe</id>
                        <time>1407197508000</time>
                        <text>text</text>
                        <recipient>1234567890123456789012345678901234567890123</recipient>
index 4d77cb2..c206ef7 100644 (file)
@@ -5,8 +5,8 @@
        <profile></profile>
        <replies>
                <reply>
-                       <id>reply-id</id>
-                       <post-id>post-id</post-id>
+                       <id>5ccba7f4-aff0-11e9-b176-a7b9db60ce98</id>
+                       <post-id>3de12680-afef-11e9-a124-e713cf8912fe</post-id>
                        <time>1407197508000</time>
                        <text>reply-text</text>
                </reply>
index fdfb493..0bc7046 100644 (file)
@@ -5,7 +5,7 @@
        <profile></profile>
        <posts>
                <post>
-                       <id>post-id</id>
+                       <id>3de12680-afef-11e9-a124-e713cf8912fe</id>
                        <time>1407197508000</time>
                        <text>text</text>
                </post>