Merge branch 'release/0.9-rc1' 0.9-rc1
authorDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Tue, 16 Jun 2015 15:54:09 +0000 (17:54 +0200)
committerDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Tue, 16 Jun 2015 15:54:09 +0000 (17:54 +0200)
259 files changed:
pom.xml
src/main/java/net/pterodactylus/sone/core/ConfigurationSoneParser.java [new file with mode: 0644]
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/Preferences.java
src/main/java/net/pterodactylus/sone/core/PreferencesLoader.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/core/SoneChangeDetector.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/core/SoneDownloader.java
src/main/java/net/pterodactylus/sone/core/SoneDownloaderImpl.java [new file with mode: 0644]
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 [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/core/SoneParser.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/core/SoneRescuer.java
src/main/java/net/pterodactylus/sone/core/SoneUri.java
src/main/java/net/pterodactylus/sone/core/UpdateChecker.java
src/main/java/net/pterodactylus/sone/core/WebOfTrustUpdater.java
src/main/java/net/pterodactylus/sone/core/WebOfTrustUpdaterImpl.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/core/event/InsertionDelayChangedEvent.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/core/event/SoneInsertedEvent.java
src/main/java/net/pterodactylus/sone/data/Album.java
src/main/java/net/pterodactylus/sone/data/AlbumImpl.java [deleted file]
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/ImageImpl.java [deleted file]
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/SoneImpl.java [deleted file]
src/main/java/net/pterodactylus/sone/data/SoneOptions.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/data/impl/AbstractAlbumBuilder.java
src/main/java/net/pterodactylus/sone/data/impl/AbstractSoneBuilder.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/data/impl/AlbumBuilderImpl.java
src/main/java/net/pterodactylus/sone/data/impl/AlbumImpl.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/data/impl/IdOnlySone.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/data/impl/ImageBuilderImpl.java
src/main/java/net/pterodactylus/sone/data/impl/ImageImpl.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/data/impl/PostImpl.java
src/main/java/net/pterodactylus/sone/data/impl/SoneImpl.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/database/AlbumBuilder.java
src/main/java/net/pterodactylus/sone/database/BookmarkDatabase.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/database/Database.java
src/main/java/net/pterodactylus/sone/database/FriendDatabase.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/database/FriendProvider.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/database/FriendStore.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/database/PostBuilderFactory.java
src/main/java/net/pterodactylus/sone/database/PostProvider.java
src/main/java/net/pterodactylus/sone/database/PostReplyBuilderFactory.java
src/main/java/net/pterodactylus/sone/database/PostReplyStore.java
src/main/java/net/pterodactylus/sone/database/PostStore.java
src/main/java/net/pterodactylus/sone/database/SoneBuilder.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/database/SoneBuilderFactory.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/database/SoneDatabase.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/database/SoneProvider.java
src/main/java/net/pterodactylus/sone/database/SoneStore.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/database/memory/ConfigurationLoader.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/database/memory/MemoryBookmarkDatabase.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/database/memory/MemoryDatabase.java
src/main/java/net/pterodactylus/sone/database/memory/MemoryFriendDatabase.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/database/memory/MemoryPost.java
src/main/java/net/pterodactylus/sone/database/memory/MemorySoneBuilder.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/fcp/FcpInterface.java
src/main/java/net/pterodactylus/sone/fcp/event/FcpInterfaceActivatedEvent.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/fcp/event/FcpInterfaceDeactivatedEvent.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/fcp/event/FullAccessRequiredChanged.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/freenet/Key.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/freenet/PluginStoreConfigurationBackend.java
src/main/java/net/pterodactylus/sone/freenet/StringBucket.java [deleted file]
src/main/java/net/pterodactylus/sone/freenet/plugin/PluginConnector.java
src/main/java/net/pterodactylus/sone/freenet/wot/Context.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/freenet/wot/DefaultIdentity.java
src/main/java/net/pterodactylus/sone/freenet/wot/DefaultOwnIdentity.java
src/main/java/net/pterodactylus/sone/freenet/wot/Identity.java
src/main/java/net/pterodactylus/sone/freenet/wot/IdentityChangeDetector.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/freenet/wot/IdentityChangeEventSender.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/freenet/wot/IdentityLoader.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/freenet/wot/IdentityManager.java
src/main/java/net/pterodactylus/sone/freenet/wot/IdentityManagerImpl.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/freenet/wot/OwnIdentity.java
src/main/java/net/pterodactylus/sone/freenet/wot/Trust.java
src/main/java/net/pterodactylus/sone/freenet/wot/WebOfTrustConnector.java
src/main/java/net/pterodactylus/sone/freenet/wot/event/IdentityEvent.java
src/main/java/net/pterodactylus/sone/freenet/wot/event/OwnIdentityEvent.java
src/main/java/net/pterodactylus/sone/main/SonePlugin.java
src/main/java/net/pterodactylus/sone/notify/ListNotificationFilters.java
src/main/java/net/pterodactylus/sone/template/CollectionAccessor.java
src/main/java/net/pterodactylus/sone/template/IdentityAccessor.java
src/main/java/net/pterodactylus/sone/template/ImageLinkFilter.java
src/main/java/net/pterodactylus/sone/template/ParserFilter.java
src/main/java/net/pterodactylus/sone/template/PostAccessor.java
src/main/java/net/pterodactylus/sone/template/ProfileAccessor.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/text/SoneTextParser.java
src/main/java/net/pterodactylus/sone/utils/DefaultOption.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/utils/IntegerRangePredicate.java
src/main/java/net/pterodactylus/sone/utils/NumberParsers.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/utils/Option.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/utils/Optionals.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/web/BookmarkPage.java
src/main/java/net/pterodactylus/sone/web/BookmarksPage.java
src/main/java/net/pterodactylus/sone/web/CreateAlbumPage.java
src/main/java/net/pterodactylus/sone/web/CreatePostPage.java
src/main/java/net/pterodactylus/sone/web/CreateReplyPage.java
src/main/java/net/pterodactylus/sone/web/CreateSonePage.java
src/main/java/net/pterodactylus/sone/web/DeleteAlbumPage.java
src/main/java/net/pterodactylus/sone/web/DeletePostPage.java
src/main/java/net/pterodactylus/sone/web/EditAlbumPage.java
src/main/java/net/pterodactylus/sone/web/EditImagePage.java
src/main/java/net/pterodactylus/sone/web/EditProfilePage.java
src/main/java/net/pterodactylus/sone/web/ImageBrowserPage.java
src/main/java/net/pterodactylus/sone/web/IndexPage.java
src/main/java/net/pterodactylus/sone/web/KnownSonesPage.java
src/main/java/net/pterodactylus/sone/web/LockSonePage.java
src/main/java/net/pterodactylus/sone/web/LoginPage.java
src/main/java/net/pterodactylus/sone/web/NewPage.java
src/main/java/net/pterodactylus/sone/web/OptionsPage.java
src/main/java/net/pterodactylus/sone/web/RescuePage.java
src/main/java/net/pterodactylus/sone/web/SearchPage.java
src/main/java/net/pterodactylus/sone/web/UnbookmarkPage.java
src/main/java/net/pterodactylus/sone/web/UnlockSonePage.java
src/main/java/net/pterodactylus/sone/web/UploadImagePage.java
src/main/java/net/pterodactylus/sone/web/ViewSonePage.java
src/main/java/net/pterodactylus/sone/web/WebInterface.java
src/main/java/net/pterodactylus/sone/web/ajax/BookmarkAjaxPage.java
src/main/java/net/pterodactylus/sone/web/ajax/CreatePostAjaxPage.java
src/main/java/net/pterodactylus/sone/web/ajax/CreateReplyAjaxPage.java
src/main/java/net/pterodactylus/sone/web/ajax/EditAlbumAjaxPage.java
src/main/java/net/pterodactylus/sone/web/ajax/EditImageAjaxPage.java
src/main/java/net/pterodactylus/sone/web/ajax/GetNotificationsAjaxPage.java
src/main/java/net/pterodactylus/sone/web/ajax/GetStatusAjaxPage.java
src/main/java/net/pterodactylus/sone/web/ajax/JsonPage.java
src/main/java/net/pterodactylus/sone/web/ajax/LockSoneAjaxPage.java
src/main/java/net/pterodactylus/sone/web/ajax/UnbookmarkAjaxPage.java
src/main/java/net/pterodactylus/sone/web/ajax/UnlockSoneAjaxPage.java
src/main/java/net/pterodactylus/sone/web/page/FreenetTemplatePage.java
src/main/java/net/pterodactylus/sone/web/page/PageToadlet.java
src/main/resources/i18n/sone.de.properties
src/main/resources/i18n/sone.en.properties
src/main/resources/i18n/sone.fr.properties
src/main/resources/i18n/sone.ja.properties
src/main/resources/i18n/sone.no.properties
src/main/resources/i18n/sone.pl.properties
src/main/resources/i18n/sone.ru.properties
src/main/resources/templates/emptyAlbumTitle.html [new file with mode: 0644]
src/main/resources/templates/emptyImageTitle.html [new file with mode: 0644]
src/main/resources/templates/imageBrowser.html
src/main/resources/templates/include/viewSone.html
src/test/java/net/pterodactylus/sone/Matchers.java [new file with mode: 0644]
src/test/java/net/pterodactylus/sone/TestAlbumBuilder.java [new file with mode: 0644]
src/test/java/net/pterodactylus/sone/TestImageBuilder.java [new file with mode: 0644]
src/test/java/net/pterodactylus/sone/TestPostBuilder.java [new file with mode: 0644]
src/test/java/net/pterodactylus/sone/TestPostReplyBuilder.java [new file with mode: 0644]
src/test/java/net/pterodactylus/sone/TestUtil.java [new file with mode: 0644]
src/test/java/net/pterodactylus/sone/TestValue.java [new file with mode: 0644]
src/test/java/net/pterodactylus/sone/core/ConfigurationSoneParserTest.java [new file with mode: 0644]
src/test/java/net/pterodactylus/sone/core/CoreTest.java [new file with mode: 0644]
src/test/java/net/pterodactylus/sone/core/FreenetInterfaceTest.java [new file with mode: 0644]
src/test/java/net/pterodactylus/sone/core/ImageInserterTest.java [new file with mode: 0644]
src/test/java/net/pterodactylus/sone/core/OptionsTest.java [new file with mode: 0644]
src/test/java/net/pterodactylus/sone/core/PreferencesLoaderTest.java [new file with mode: 0644]
src/test/java/net/pterodactylus/sone/core/PreferencesTest.java [new file with mode: 0644]
src/test/java/net/pterodactylus/sone/core/SoneChangeDetectorTest.java [new file with mode: 0644]
src/test/java/net/pterodactylus/sone/core/SoneDownloaderTest.java [new file with mode: 0644]
src/test/java/net/pterodactylus/sone/core/SoneInserterTest.java [new file with mode: 0644]
src/test/java/net/pterodactylus/sone/core/SoneModificationDetectorTest.java [new file with mode: 0644]
src/test/java/net/pterodactylus/sone/core/SoneParserTest.java [new file with mode: 0644]
src/test/java/net/pterodactylus/sone/core/SoneRescuerTest.java [new file with mode: 0644]
src/test/java/net/pterodactylus/sone/core/SoneUriTest.java [new file with mode: 0644]
src/test/java/net/pterodactylus/sone/core/UpdateCheckerTest.java [new file with mode: 0644]
src/test/java/net/pterodactylus/sone/core/WebOfTrustUpdaterTest.java [new file with mode: 0644]
src/test/java/net/pterodactylus/sone/data/ProfileTest.java [new file with mode: 0644]
src/test/java/net/pterodactylus/sone/data/impl/AbstractSoneBuilderTest.java [new file with mode: 0644]
src/test/java/net/pterodactylus/sone/data/impl/ImageImplTest.java [new file with mode: 0644]
src/test/java/net/pterodactylus/sone/database/memory/ConfigurationLoaderTest.java [new file with mode: 0644]
src/test/java/net/pterodactylus/sone/database/memory/MemoryBookmarkDatabaseTest.java [new file with mode: 0644]
src/test/java/net/pterodactylus/sone/database/memory/MemoryDatabaseTest.java
src/test/java/net/pterodactylus/sone/fcp/FcpInterfaceTest.java [new file with mode: 0644]
src/test/java/net/pterodactylus/sone/fcp/LockSoneCommandTest.java
src/test/java/net/pterodactylus/sone/fcp/UnlockSoneCommandTest.java
src/test/java/net/pterodactylus/sone/freenet/KeyTest.java [new file with mode: 0644]
src/test/java/net/pterodactylus/sone/freenet/wot/DefaultIdentityTest.java [new file with mode: 0644]
src/test/java/net/pterodactylus/sone/freenet/wot/DefaultOwnIdentityTest.java [new file with mode: 0644]
src/test/java/net/pterodactylus/sone/freenet/wot/Identities.java [new file with mode: 0644]
src/test/java/net/pterodactylus/sone/freenet/wot/IdentityChangeDetectorTest.java [new file with mode: 0644]
src/test/java/net/pterodactylus/sone/freenet/wot/IdentityChangeEventSenderTest.java [new file with mode: 0644]
src/test/java/net/pterodactylus/sone/freenet/wot/IdentityLoaderTest.java [new file with mode: 0644]
src/test/java/net/pterodactylus/sone/freenet/wot/IdentityManagerTest.java [new file with mode: 0644]
src/test/java/net/pterodactylus/sone/freenet/wot/event/IdentityEventTest.java [new file with mode: 0644]
src/test/java/net/pterodactylus/sone/freenet/wot/event/OwnIdentityEventTest.java [new file with mode: 0644]
src/test/java/net/pterodactylus/sone/template/AlbumAccessorTest.java [new file with mode: 0644]
src/test/java/net/pterodactylus/sone/template/CollectionAccessorTest.java [new file with mode: 0644]
src/test/java/net/pterodactylus/sone/template/CssClassNameFilterTest.java [new file with mode: 0644]
src/test/java/net/pterodactylus/sone/template/GetPagePluginTest.java [new file with mode: 0644]
src/test/java/net/pterodactylus/sone/template/HttpRequestAccessorTest.java [new file with mode: 0644]
src/test/java/net/pterodactylus/sone/template/IdentityAccessorTest.java [new file with mode: 0644]
src/test/java/net/pterodactylus/sone/text/SoneTextParserTest.java
src/test/java/net/pterodactylus/sone/utils/DefaultOptionTest.java [new file with mode: 0644]
src/test/java/net/pterodactylus/sone/utils/IntegerRangePredicateTest.java [new file with mode: 0644]
src/test/java/net/pterodactylus/sone/utils/NumberParsersTest.java [new file with mode: 0644]
src/test/java/net/pterodactylus/sone/utils/OptionalsTest.java [new file with mode: 0644]
src/test/java/net/pterodactylus/sone/web/ajax/BookmarkAjaxPageTest.java
src/test/resources/net/pterodactylus/sone/core/sone-inserter-faulty-manifest.txt [new file with mode: 0644]
src/test/resources/net/pterodactylus/sone/core/sone-inserter-invalid-manifest.txt [new file with mode: 0644]
src/test/resources/net/pterodactylus/sone/core/sone-inserter-manifest.txt [new file with mode: 0644]
src/test/resources/net/pterodactylus/sone/core/sone-parser-missing-client-name.xml [new file with mode: 0644]
src/test/resources/net/pterodactylus/sone/core/sone-parser-missing-client-version.xml [new file with mode: 0644]
src/test/resources/net/pterodactylus/sone/core/sone-parser-missing-protocol-version.xml [new file with mode: 0644]
src/test/resources/net/pterodactylus/sone/core/sone-parser-negative-protocol-version.xml [new file with mode: 0644]
src/test/resources/net/pterodactylus/sone/core/sone-parser-no-payload.xml [new file with mode: 0644]
src/test/resources/net/pterodactylus/sone/core/sone-parser-no-profile.xml [new file with mode: 0644]
src/test/resources/net/pterodactylus/sone/core/sone-parser-no-time.xml [new file with mode: 0644]
src/test/resources/net/pterodactylus/sone/core/sone-parser-not-xml.xml [new file with mode: 0644]
src/test/resources/net/pterodactylus/sone/core/sone-parser-profile-duplicate-field-name.xml [new file with mode: 0644]
src/test/resources/net/pterodactylus/sone/core/sone-parser-profile-empty-field-name.xml [new file with mode: 0644]
src/test/resources/net/pterodactylus/sone/core/sone-parser-profile-missing-field-name.xml [new file with mode: 0644]
src/test/resources/net/pterodactylus/sone/core/sone-parser-time-not-numeric.xml [new file with mode: 0644]
src/test/resources/net/pterodactylus/sone/core/sone-parser-too-large-protocol-version.xml [new file with mode: 0644]
src/test/resources/net/pterodactylus/sone/core/sone-parser-with-client-info.xml [new file with mode: 0644]
src/test/resources/net/pterodactylus/sone/core/sone-parser-with-image.xml [new file with mode: 0644]
src/test/resources/net/pterodactylus/sone/core/sone-parser-with-invalid-image-height.xml [new file with mode: 0644]
src/test/resources/net/pterodactylus/sone/core/sone-parser-with-invalid-image-width.xml [new file with mode: 0644]
src/test/resources/net/pterodactylus/sone/core/sone-parser-with-invalid-parent-album-id.xml [new file with mode: 0644]
src/test/resources/net/pterodactylus/sone/core/sone-parser-with-invalid-post-reply-time.xml [new file with mode: 0644]
src/test/resources/net/pterodactylus/sone/core/sone-parser-with-invalid-post-time.xml [new file with mode: 0644]
src/test/resources/net/pterodactylus/sone/core/sone-parser-with-invalid-recipient.xml [new file with mode: 0644]
src/test/resources/net/pterodactylus/sone/core/sone-parser-with-liked-post-ids.xml [new file with mode: 0644]
src/test/resources/net/pterodactylus/sone/core/sone-parser-with-liked-post-reply-ids.xml [new file with mode: 0644]
src/test/resources/net/pterodactylus/sone/core/sone-parser-with-multiple-albums.xml [new file with mode: 0644]
src/test/resources/net/pterodactylus/sone/core/sone-parser-with-profile.xml [new file with mode: 0644]
src/test/resources/net/pterodactylus/sone/core/sone-parser-with-recipient.xml [new file with mode: 0644]
src/test/resources/net/pterodactylus/sone/core/sone-parser-with-valid-post-reply-time.xml [new file with mode: 0644]
src/test/resources/net/pterodactylus/sone/core/sone-parser-with-valid-post-time.xml [new file with mode: 0644]
src/test/resources/net/pterodactylus/sone/core/sone-parser-with-zero-time.xml [new file with mode: 0644]
src/test/resources/net/pterodactylus/sone/core/sone-parser-without-album-id.xml [new file with mode: 0644]
src/test/resources/net/pterodactylus/sone/core/sone-parser-without-album-title.xml [new file with mode: 0644]
src/test/resources/net/pterodactylus/sone/core/sone-parser-without-albums.xml [new file with mode: 0644]
src/test/resources/net/pterodactylus/sone/core/sone-parser-without-fields.xml [new file with mode: 0644]
src/test/resources/net/pterodactylus/sone/core/sone-parser-without-image-height.xml [new file with mode: 0644]
src/test/resources/net/pterodactylus/sone/core/sone-parser-without-image-id.xml [new file with mode: 0644]
src/test/resources/net/pterodactylus/sone/core/sone-parser-without-image-key.xml [new file with mode: 0644]
src/test/resources/net/pterodactylus/sone/core/sone-parser-without-image-time.xml [new file with mode: 0644]
src/test/resources/net/pterodactylus/sone/core/sone-parser-without-image-title.xml [new file with mode: 0644]
src/test/resources/net/pterodactylus/sone/core/sone-parser-without-image-width.xml [new file with mode: 0644]
src/test/resources/net/pterodactylus/sone/core/sone-parser-without-images.xml [new file with mode: 0644]
src/test/resources/net/pterodactylus/sone/core/sone-parser-without-liked-post-ids.xml [new file with mode: 0644]
src/test/resources/net/pterodactylus/sone/core/sone-parser-without-liked-post-reply-ids.xml [new file with mode: 0644]
src/test/resources/net/pterodactylus/sone/core/sone-parser-without-post-id.xml [new file with mode: 0644]
src/test/resources/net/pterodactylus/sone/core/sone-parser-without-post-reply-id.xml [new file with mode: 0644]
src/test/resources/net/pterodactylus/sone/core/sone-parser-without-post-reply-post-id.xml [new file with mode: 0644]
src/test/resources/net/pterodactylus/sone/core/sone-parser-without-post-reply-text.xml [new file with mode: 0644]
src/test/resources/net/pterodactylus/sone/core/sone-parser-without-post-reply-time.xml [new file with mode: 0644]
src/test/resources/net/pterodactylus/sone/core/sone-parser-without-post-text.xml [new file with mode: 0644]
src/test/resources/net/pterodactylus/sone/core/sone-parser-without-post-time.xml [new file with mode: 0644]

diff --git a/pom.xml b/pom.xml
index e96a971..139d3c1 100644 (file)
--- a/pom.xml
+++ b/pom.xml
@@ -2,7 +2,7 @@
        <modelVersion>4.0.0</modelVersion>
        <groupId>net.pterodactylus</groupId>
        <artifactId>sone</artifactId>
-       <version>0.8.9</version>
+       <version>0.9-rc1</version>
        <dependencies>
                <dependency>
                        <groupId>net.pterodactylus</groupId>
                        <scope>test</scope>
                </dependency>
                <dependency>
+                       <groupId>org.hamcrest</groupId>
+                       <artifactId>hamcrest-all</artifactId>
+                       <version>1.3</version>
+               </dependency>
+               <dependency>
                        <groupId>org.freenetproject</groupId>
                        <artifactId>fred</artifactId>
-                       <version>0.7.5.1405</version>
+                       <version>0.7.5.1467.99.3</version>
                        <scope>provided</scope>
                </dependency>
                <dependency>
                        <groupId>org.freenetproject</groupId>
                        <artifactId>freenet-ext</artifactId>
-                       <version>26</version>
+                       <version>29</version>
                        <scope>provided</scope>
                </dependency>
                <dependency>
@@ -41,7 +46,7 @@
                <dependency>
                        <groupId>com.google.guava</groupId>
                        <artifactId>guava</artifactId>
-                       <version>14.0-rc1</version>
+                       <version>14.0.1</version>
                </dependency>
                <dependency>
                        <groupId>commons-lang</groupId>
                                        <footer>© 2010–2013 David ‘Bombe’ Roden</footer>
                                </configuration>
                        </plugin>
+                       <plugin>
+                               <groupId>org.jacoco</groupId>
+                               <artifactId>jacoco-maven-plugin</artifactId>
+                               <version>0.7.1.201405082137</version>
+                               <executions>
+                                       <execution>
+                                               <id>default-prepare-agent</id>
+                                               <goals>
+                                                       <goal>prepare-agent</goal>
+                                               </goals>
+                                       </execution>
+                                       <execution>
+                                               <id>default-report</id>
+                                               <phase>prepare-package</phase>
+                                               <goals>
+                                                       <goal>report</goal>
+                                               </goals>
+                                       </execution>
+                                       <execution>
+                                               <id>default-check</id>
+                                               <goals>
+                                                       <goal>check</goal>
+                                               </goals>
+                                               <configuration>
+                                                       <rules>
+                                                               <!--  implmentation is needed only for Maven 2  -->
+                                                               <rule implementation="org.jacoco.maven.RuleConfiguration">
+                                                                       <element>BUNDLE</element>
+                                                                       <limits>
+                                                                               <!--  implmentation is needed only for Maven 2  -->
+                                                                               <limit implementation="org.jacoco.report.check.Limit">
+                                                                                       <counter>COMPLEXITY</counter>
+                                                                                       <value>COVEREDRATIO</value>
+                                                                                       <minimum>0.60</minimum>
+                                                                               </limit>
+                                                                       </limits>
+                                                               </rule>
+                                                       </rules>
+                                               </configuration>
+                                       </execution>
+                               </executions>
+                       </plugin>
                </plugins>
        </build>
 </project>
diff --git a/src/main/java/net/pterodactylus/sone/core/ConfigurationSoneParser.java b/src/main/java/net/pterodactylus/sone/core/ConfigurationSoneParser.java
new file mode 100644 (file)
index 0000000..a29856b
--- /dev/null
@@ -0,0 +1,312 @@
+package net.pterodactylus.sone.core;
+
+import static java.util.Collections.unmodifiableMap;
+
+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 javax.annotation.Nullable;
+
+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.Profile;
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.database.AlbumBuilderFactory;
+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.util.config.Configuration;
+
+/**
+ * Parses a {@link Sone}’s data from a {@link Configuration}.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class ConfigurationSoneParser {
+
+       private final Configuration configuration;
+       private final Sone sone;
+       private final String sonePrefix;
+       private final Map<String, Album> albums = new HashMap<String, Album>();
+       private final List<Album> topLevelAlbums = new ArrayList<Album>();
+       private final Map<String, Image> images = new HashMap<String, Image>();
+
+       public ConfigurationSoneParser(Configuration configuration, Sone sone) {
+               this.configuration = configuration;
+               this.sone = sone;
+               sonePrefix = "Sone/" + sone.getId();
+       }
+
+       public Profile parseProfile() {
+               Profile profile = new Profile(sone);
+               profile.setFirstName(getString("/Profile/FirstName", null));
+               profile.setMiddleName(getString("/Profile/MiddleName", null));
+               profile.setLastName(getString("/Profile/LastName", null));
+               profile.setBirthDay(getInt("/Profile/BirthDay", null));
+               profile.setBirthMonth(getInt("/Profile/BirthMonth", null));
+               profile.setBirthYear(getInt("/Profile/BirthYear", null));
+
+               /* load profile fields. */
+               int fieldCount = 0;
+               while (true) {
+                       String fieldPrefix = "/Profile/Fields/" + fieldCount++;
+                       String fieldName = getString(fieldPrefix + "/Name", null);
+                       if (fieldName == null) {
+                               break;
+                       }
+                       String fieldValue = getString(fieldPrefix + "/Value", "");
+                       profile.addField(fieldName).setValue(fieldValue);
+               }
+
+               return profile;
+       }
+
+       private String getString(String nodeName, @Nullable String defaultValue) {
+               return configuration.getStringValue(sonePrefix + nodeName)
+                               .getValue(defaultValue);
+       }
+
+       private Integer getInt(String nodeName, @Nullable Integer defaultValue) {
+               return configuration.getIntValue(sonePrefix + nodeName)
+                               .getValue(defaultValue);
+       }
+
+       private Long getLong(String nodeName, @Nullable Long defaultValue) {
+               return configuration.getLongValue(sonePrefix + nodeName)
+                               .getValue(defaultValue);
+       }
+
+       public Set<Post> parsePosts(PostBuilderFactory postBuilderFactory)
+       throws InvalidPostFound {
+               Set<Post> posts = new HashSet<Post>();
+               while (true) {
+                       String postPrefix = "/Posts/" + posts.size();
+                       String postId = getString(postPrefix + "/ID", null);
+                       if (postId == null) {
+                               break;
+                       }
+                       long postTime = getLong(postPrefix + "/Time", 0L);
+                       String postText = getString(postPrefix + "/Text", null);
+                       if (postAttributesAreInvalid(postTime, postText)) {
+                               throw new InvalidPostFound();
+                       }
+                       PostBuilder postBuilder = postBuilderFactory.newPostBuilder()
+                                       .withId(postId)
+                                       .from(sone.getId())
+                                       .withTime(postTime)
+                                       .withText(postText);
+                       String postRecipientId =
+                                       getString(postPrefix + "/Recipient", null);
+                       if (postRecipientIsValid(postRecipientId)) {
+                               postBuilder.to(postRecipientId);
+                       }
+                       posts.add(postBuilder.build());
+               }
+               return posts;
+       }
+
+       private boolean postAttributesAreInvalid(long postTime, String postText) {
+               return (postTime == 0) || (postText == null);
+       }
+
+       private boolean postRecipientIsValid(String postRecipientId) {
+               return (postRecipientId != null) && (postRecipientId.length() == 43);
+       }
+
+       public Set<PostReply> parsePostReplies(
+                       PostReplyBuilderFactory postReplyBuilderFactory) {
+               Set<PostReply> replies = new HashSet<PostReply>();
+               while (true) {
+                       String replyPrefix = "/Replies/" + replies.size();
+                       String replyId = getString(replyPrefix + "/ID", null);
+                       if (replyId == null) {
+                               break;
+                       }
+                       String postId = getString(replyPrefix + "/Post/ID", null);
+                       long replyTime = getLong(replyPrefix + "/Time", 0L);
+                       String replyText = getString(replyPrefix + "/Text", null);
+                       if ((postId == null) || (replyTime == 0) || (replyText == null)) {
+                               throw new InvalidPostReplyFound();
+                       }
+                       PostReplyBuilder postReplyBuilder = postReplyBuilderFactory
+                                       .newPostReplyBuilder()
+                                       .withId(replyId)
+                                       .from(sone.getId())
+                                       .to(postId)
+                                       .withTime(replyTime)
+                                       .withText(replyText);
+                       replies.add(postReplyBuilder.build());
+               }
+               return replies;
+       }
+
+       public Set<String> parseLikedPostIds() {
+               Set<String> likedPostIds = new HashSet<String>();
+               while (true) {
+                       String likedPostId =
+                                       getString("/Likes/Post/" + likedPostIds.size() + "/ID",
+                                                       null);
+                       if (likedPostId == null) {
+                               break;
+                       }
+                       likedPostIds.add(likedPostId);
+               }
+               return likedPostIds;
+       }
+
+       public Set<String> parseLikedPostReplyIds() {
+               Set<String> likedPostReplyIds = new HashSet<String>();
+               while (true) {
+                       String likedReplyId = getString(
+                                       "/Likes/Reply/" + likedPostReplyIds.size() + "/ID", null);
+                       if (likedReplyId == null) {
+                               break;
+                       }
+                       likedPostReplyIds.add(likedReplyId);
+               }
+               return likedPostReplyIds;
+       }
+
+       public Set<String> parseFriends() {
+               Set<String> friends = new HashSet<String>();
+               while (true) {
+                       String friendId =
+                                       getString("/Friends/" + friends.size() + "/ID", null);
+                       if (friendId == null) {
+                               break;
+                       }
+                       friends.add(friendId);
+               }
+               return friends;
+       }
+
+       public List<Album> parseTopLevelAlbums(
+                       AlbumBuilderFactory albumBuilderFactory) {
+               int albumCounter = 0;
+               while (true) {
+                       String albumPrefix = "/Albums/" + albumCounter++;
+                       String albumId = getString(albumPrefix + "/ID", null);
+                       if (albumId == null) {
+                               break;
+                       }
+                       String albumTitle = getString(albumPrefix + "/Title", null);
+                       String albumDescription =
+                                       getString(albumPrefix + "/Description", null);
+                       String albumParentId = getString(albumPrefix + "/Parent", null);
+                       String albumImageId =
+                                       getString(albumPrefix + "/AlbumImage", null);
+                       if ((albumTitle == null) || (albumDescription == null)) {
+                               throw new InvalidAlbumFound();
+                       }
+                       Album album = albumBuilderFactory.newAlbumBuilder()
+                                       .withId(albumId)
+                                       .by(sone)
+                                       .build()
+                                       .modify()
+                                       .setTitle(albumTitle)
+                                       .setDescription(albumDescription)
+                                       .setAlbumImage(albumImageId)
+                                       .update();
+                       if (albumParentId != null) {
+                               Album parentAlbum = albums.get(albumParentId);
+                               if (parentAlbum == null) {
+                                       throw new InvalidParentAlbumFound(albumParentId);
+                               }
+                               parentAlbum.addAlbum(album);
+                       } else {
+                               topLevelAlbums.add(album);
+                       }
+                       albums.put(albumId, album);
+               }
+               return topLevelAlbums;
+       }
+
+       public Map<String, Album> getAlbums() {
+               return unmodifiableMap(albums);
+       }
+
+       public void parseImages(ImageBuilderFactory imageBuilderFactory) {
+               int imageCounter = 0;
+               while (true) {
+                       String imagePrefix = "/Images/" + imageCounter++;
+                       String imageId = getString(imagePrefix + "/ID", null);
+                       if (imageId == null) {
+                               break;
+                       }
+                       String albumId = getString(imagePrefix + "/Album", null);
+                       String key = getString(imagePrefix + "/Key", null);
+                       String title = getString(imagePrefix + "/Title", null);
+                       String description =
+                                       getString(imagePrefix + "/Description", null);
+                       Long creationTime = getLong(imagePrefix + "/CreationTime", null);
+                       Integer width = getInt(imagePrefix + "/Width", null);
+                       Integer height = getInt(imagePrefix + "/Height", null);
+                       if (albumAttributesAreInvalid(albumId, key, title, description,
+                                       creationTime,
+                                       width, height)) {
+                               throw new InvalidImageFound();
+                       }
+                       Album album = albums.get(albumId);
+                       if (album == null) {
+                               throw new InvalidParentAlbumFound(albumId);
+                       }
+                       Image image = imageBuilderFactory.newImageBuilder()
+                                       .withId(imageId)
+                                       .build()
+                                       .modify()
+                                       .setSone(sone)
+                                       .setCreationTime(creationTime)
+                                       .setKey(key)
+                                       .setTitle(title)
+                                       .setDescription(description)
+                                       .setWidth(width)
+                                       .setHeight(height)
+                                       .update();
+                       album.addImage(image);
+                       images.put(image.getId(), image);
+               }
+       }
+
+       public Map<String, Image> getImages() {
+               return images;
+       }
+
+       private boolean albumAttributesAreInvalid(String albumId, String key,
+                       String title, String description, Long creationTime,
+                       Integer width, Integer height) {
+               return (albumId == null) || (key == null) || (title == null) || (
+                               description == null) || (creationTime == null) || (width
+                               == null) || (height == null);
+       }
+
+       public static class InvalidPostFound extends RuntimeException { }
+
+       public static class InvalidPostReplyFound extends RuntimeException { }
+
+       public static class InvalidAlbumFound extends RuntimeException { }
+
+       public static class InvalidParentAlbumFound extends RuntimeException {
+
+               private final String albumParentId;
+
+               public InvalidParentAlbumFound(String albumParentId) {
+                       this.albumParentId = albumParentId;
+               }
+
+               public String getAlbumParentId() {
+                       return albumParentId;
+               }
+
+       }
+
+       public static class InvalidImageFound extends RuntimeException { }
+
+}
index ecb5857..1153c0f 100644 (file)
 
 package net.pterodactylus.sone.core;
 
+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 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 java.net.MalformedURLException;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashMap;
@@ -36,9 +40,13 @@ import java.util.concurrent.TimeUnit;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 
-import net.pterodactylus.sone.core.Options.DefaultOption;
-import net.pterodactylus.sone.core.Options.Option;
-import net.pterodactylus.sone.core.Options.OptionWatcher;
+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.SoneChangeDetector.PostProcessor;
+import net.pterodactylus.sone.core.SoneChangeDetector.PostReplyProcessor;
 import net.pterodactylus.sone.core.event.ImageInsertFinishedEvent;
 import net.pterodactylus.sone.core.event.MarkPostKnownEvent;
 import net.pterodactylus.sone.core.event.MarkPostReplyKnownEvent;
@@ -62,17 +70,17 @@ import net.pterodactylus.sone.data.Reply;
 import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.data.Sone.ShowCustomAvatars;
 import net.pterodactylus.sone.data.Sone.SoneStatus;
-import net.pterodactylus.sone.data.SoneImpl;
 import net.pterodactylus.sone.data.TemporaryImage;
+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.PostProvider;
 import net.pterodactylus.sone.database.PostReplyBuilder;
 import net.pterodactylus.sone.database.PostReplyProvider;
+import net.pterodactylus.sone.database.SoneBuilder;
 import net.pterodactylus.sone.database.SoneProvider;
-import net.pterodactylus.sone.fcp.FcpInterface;
-import net.pterodactylus.sone.fcp.FcpInterface.FullAccessRequired;
 import net.pterodactylus.sone.freenet.wot.Identity;
 import net.pterodactylus.sone.freenet.wot.IdentityManager;
 import net.pterodactylus.sone.freenet.wot.OwnIdentity;
@@ -82,52 +90,45 @@ 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 net.pterodactylus.sone.main.SonePlugin;
-import net.pterodactylus.sone.utils.IntegerRangePredicate;
 import net.pterodactylus.util.config.Configuration;
 import net.pterodactylus.util.config.ConfigurationException;
-import net.pterodactylus.util.logging.Logging;
-import net.pterodactylus.util.number.Numbers;
 import net.pterodactylus.util.service.AbstractService;
 import net.pterodactylus.util.thread.NamedThreadFactory;
 
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Function;
 import com.google.common.base.Optional;
-import com.google.common.base.Predicate;
-import com.google.common.base.Predicates;
 import com.google.common.collect.FluentIterable;
 import com.google.common.collect.HashMultimap;
-import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Multimap;
 import com.google.common.collect.Multimaps;
 import com.google.common.eventbus.EventBus;
 import com.google.common.eventbus.Subscribe;
 import com.google.inject.Inject;
-
-import freenet.keys.FreenetURI;
+import com.google.inject.Singleton;
 
 /**
  * The Sone core.
  *
  * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
  */
+@Singleton
 public class Core extends AbstractService implements SoneProvider, PostProvider, PostReplyProvider {
 
        /** The logger. */
-       private static final Logger logger = Logging.getLogger(Core.class);
+       private static final Logger logger = getLogger("Sone.Core");
 
        /** The start time. */
        private final long startupTime = System.currentTimeMillis();
 
-       /** The options. */
-       private final Options options = new Options();
-
        /** The preferences. */
-       private final Preferences preferences = new Preferences(options);
+       private final Preferences preferences;
 
        /** The event bus. */
        private final EventBus eventBus;
 
        /** The configuration. */
-       private Configuration configuration;
+       private final Configuration configuration;
 
        /** Whether we’re currently saving the configuration. */
        private boolean storingConfiguration = false;
@@ -153,9 +154,6 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
        /** The trust updater. */
        private final WebOfTrustUpdater webOfTrustUpdater;
 
-       /** The FCP interface. */
-       private volatile FcpInterface fcpInterface;
-
        /** The times Sones were followed. */
        private final Map<String, Long> soneFollowingTimes = new HashMap<String, Long>();
 
@@ -171,20 +169,12 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
        /* synchronize access on this on sones. */
        private final Map<Sone, SoneRescuer> soneRescuers = new HashMap<Sone, SoneRescuer>();
 
-       /** All Sones. */
-       /* synchronize access on this on itself. */
-       private final Map<String, Sone> sones = new HashMap<String, Sone>();
-
        /** All known Sones. */
        private final Set<String> knownSones = new HashSet<String>();
 
        /** The post database. */
        private final Database database;
 
-       /** All bookmarked posts. */
-       /* synchronize access on itself. */
-       private final Set<String> bookmarkedPosts = new HashSet<String>();
-
        /** Trusted identities, sorted by own identities. */
        private final Multimap<OwnIdentity, Identity> trustedIdentities = Multimaps.synchronizedSetMultimap(HashMultimap.<OwnIdentity, Identity>create());
 
@@ -219,12 +209,28 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
                this.configuration = configuration;
                this.freenetInterface = freenetInterface;
                this.identityManager = identityManager;
-               this.soneDownloader = new SoneDownloader(this, freenetInterface);
-               this.imageInserter = new ImageInserter(freenetInterface);
+               this.soneDownloader = new SoneDownloaderImpl(this, freenetInterface);
+               this.imageInserter = new ImageInserter(freenetInterface, freenetInterface.new InsertTokenSupplier());
                this.updateChecker = new UpdateChecker(eventBus, freenetInterface);
                this.webOfTrustUpdater = webOfTrustUpdater;
                this.eventBus = eventBus;
                this.database = database;
+               preferences = new Preferences(eventBus);
+       }
+
+       @VisibleForTesting
+       protected Core(Configuration configuration, FreenetInterface freenetInterface, IdentityManager identityManager, SoneDownloader soneDownloader, ImageInserter imageInserter, UpdateChecker updateChecker, WebOfTrustUpdater webOfTrustUpdater, EventBus eventBus, Database database) {
+               super("Sone Core");
+               this.configuration = configuration;
+               this.freenetInterface = freenetInterface;
+               this.identityManager = identityManager;
+               this.soneDownloader = soneDownloader;
+               this.imageInserter = imageInserter;
+               this.updateChecker = updateChecker;
+               this.webOfTrustUpdater = webOfTrustUpdater;
+               this.eventBus = eventBus;
+               this.database = database;
+               preferences = new Preferences(eventBus);
        }
 
        //
@@ -241,18 +247,6 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
        }
 
        /**
-        * Sets the configuration to use. This will automatically save the current
-        * configuration to the given configuration.
-        *
-        * @param configuration
-        *            The new configuration to use
-        */
-       public void setConfiguration(Configuration configuration) {
-               this.configuration = configuration;
-               touchConfiguration();
-       }
-
-       /**
         * Returns the options used by the core.
         *
         * @return The options of the core
@@ -280,16 +274,6 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
        }
 
        /**
-        * Sets the FCP interface to use.
-        *
-        * @param fcpInterface
-        *            The FCP interface to use
-        */
-       public void setFcpInterface(FcpInterface fcpInterface) {
-               this.fcpInterface = fcpInterface;
-       }
-
-       /**
         * Returns the Sone rescuer for the given local Sone.
         *
         * @param sone
@@ -299,7 +283,7 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
        public SoneRescuer getSoneRescuer(Sone sone) {
                checkNotNull(sone, "sone must not be null");
                checkArgument(sone.isLocal(), "sone must be local");
-               synchronized (sones) {
+               synchronized (soneRescuers) {
                        SoneRescuer soneRescuer = soneRescuers.get(sone);
                        if (soneRescuer == null) {
                                soneRescuer = new SoneRescuer(this, soneDownloader, sone);
@@ -323,14 +307,21 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
                }
        }
 
+       public SoneBuilder soneBuilder() {
+               return database.newSoneBuilder();
+       }
+
        /**
         * {@inheritDocs}
         */
        @Override
        public Collection<Sone> getSones() {
-               synchronized (sones) {
-                       return ImmutableSet.copyOf(sones.values());
-               }
+               return database.getSones();
+       }
+
+       @Override
+       public Function<String, Optional<Sone>> soneLoader() {
+               return database.soneLoader();
        }
 
        /**
@@ -344,9 +335,7 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
         */
        @Override
        public Optional<Sone> getSone(String id) {
-               synchronized (sones) {
-                       return Optional.fromNullable(sones.get(id));
-               }
+               return database.getSone(id);
        }
 
        /**
@@ -354,15 +343,7 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
         */
        @Override
        public Collection<Sone> getLocalSones() {
-               synchronized (sones) {
-                       return FluentIterable.from(sones.values()).filter(new Predicate<Sone>() {
-
-                               @Override
-                               public boolean apply(Sone sone) {
-                                       return sone.isLocal();
-                               }
-                       }).toSet();
-               }
+               return database.getLocalSones();
        }
 
        /**
@@ -370,24 +351,14 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
         *
         * @param id
         *            The ID of the Sone
-        * @param create
-        *            {@code true} to create a new Sone if none exists,
-        *            {@code false} to return null if none exists
         * @return The Sone with the given ID, or {@code null}
         */
-       public Sone getLocalSone(String id, boolean create) {
-               synchronized (sones) {
-                       Sone sone = sones.get(id);
-                       if ((sone == null) && create) {
-                               sone = new SoneImpl(id, true);
-                               sones.put(id, sone);
-                       }
-                       if ((sone != null) && !sone.isLocal()) {
-                               sone = new SoneImpl(id, true);
-                               sones.put(id, sone);
-                       }
-                       return sone;
+       public Sone getLocalSone(String id) {
+               Optional<Sone> sone = database.getSone(id);
+               if (sone.isPresent() && sone.get().isLocal()) {
+                       return sone.get();
                }
+               return null;
        }
 
        /**
@@ -395,36 +366,19 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
         */
        @Override
        public Collection<Sone> getRemoteSones() {
-               synchronized (sones) {
-                       return FluentIterable.from(sones.values()).filter(new Predicate<Sone>() {
-
-                               @Override
-                               public boolean apply(Sone sone) {
-                                       return !sone.isLocal();
-                               }
-                       }).toSet();
-               }
+               return database.getRemoteSones();
        }
 
        /**
         * Returns the remote Sone with the given ID.
         *
+        *
         * @param id
         *            The ID of the remote Sone to get
-        * @param create
-        *            {@code true} to always create a Sone, {@code false} to return
-        *            {@code null} if no Sone with the given ID exists
         * @return The Sone with the given ID
         */
-       public Sone getRemoteSone(String id, boolean create) {
-               synchronized (sones) {
-                       Sone sone = sones.get(id);
-                       if ((sone == null) && create && (id != null) && (id.length() == 43)) {
-                               sone = new SoneImpl(id, false);
-                               sones.put(id, sone);
-                       }
-                       return sone;
-               }
+       public Sone getRemoteSone(String id) {
+               return database.getSone(id).orNull();
        }
 
        /**
@@ -436,7 +390,7 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
         *         {@code false} otherwise
         */
        public boolean isModifiedSone(Sone sone) {
-               return (soneInserters.containsKey(sone)) ? soneInserters.get(sone).isModified() : false;
+               return soneInserters.containsKey(sone) && soneInserters.get(sone).isModified();
        }
 
        /**
@@ -454,22 +408,6 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
        }
 
        /**
-        * Returns whether the target Sone is trusted by the origin Sone.
-        *
-        * @param origin
-        *            The origin Sone
-        * @param target
-        *            The target Sone
-        * @return {@code true} if the target Sone is trusted by the origin Sone
-        */
-       public boolean isSoneTrusted(Sone origin, Sone target) {
-               checkNotNull(origin, "origin must not be null");
-               checkNotNull(target, "target must not be null");
-               checkArgument(origin.getIdentity() instanceof OwnIdentity, "origin’s identity must be an OwnIdentity");
-               return trustedIdentities.containsEntry(origin.getIdentity(), target.getIdentity());
-       }
-
-       /**
         * Returns a post builder.
         *
         * @return A new post builder
@@ -571,21 +509,7 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
         *         otherwise
         */
        public boolean isBookmarked(Post post) {
-               return isPostBookmarked(post.getId());
-       }
-
-       /**
-        * Returns whether the post with the given ID is bookmarked.
-        *
-        * @param id
-        *            The ID of the post to check
-        * @return {@code true} if the post with the given ID is bookmarked,
-        *         {@code false} otherwise
-        */
-       public boolean isPostBookmarked(String id) {
-               synchronized (bookmarkedPosts) {
-                       return bookmarkedPosts.contains(id);
-               }
+               return database.isPostBookmarked(post);
        }
 
        /**
@@ -594,28 +518,11 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
         * @return All bookmarked posts
         */
        public Set<Post> getBookmarkedPosts() {
-               Set<Post> posts = new HashSet<Post>();
-               synchronized (bookmarkedPosts) {
-                       for (String bookmarkedPostId : bookmarkedPosts) {
-                               Optional<Post> post = getPost(bookmarkedPostId);
-                               if (post.isPresent()) {
-                                       posts.add(post.get());
-                               }
-                       }
-               }
-               return posts;
+               return database.getBookmarkedPosts();
        }
 
-       /**
-        * Returns the album with the given ID, creating a new album if no album
-        * with the given ID can be found.
-        *
-        * @param albumId
-        *            The ID of the album
-        * @return The album with the given ID
-        */
-       public Album getAlbum(String albumId) {
-               return getAlbum(albumId, true);
+       public AlbumBuilder albumBuilder() {
+               return database.newAlbumBuilder();
        }
 
        /**
@@ -624,23 +531,15 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
         *
         * @param albumId
         *            The ID of the album
-        * @param create
-        *            {@code true} to create a new album if none exists for the
-        *            given ID
         * @return The album with the given ID, or {@code null} if no album with the
-        *         given ID exists and {@code create} is {@code false}
+        *         given ID exists
         */
-       public Album getAlbum(String albumId, boolean create) {
-               Optional<Album> album = database.getAlbum(albumId);
-               if (album.isPresent()) {
-                       return album.get();
-               }
-               if (!create) {
-                       return null;
-               }
-               Album newAlbum = database.newAlbumBuilder().withId(albumId).build();
-               database.storeAlbum(newAlbum);
-               return newAlbum;
+       public Album getAlbum(String albumId) {
+               return database.getAlbum(albumId).orNull();
+       }
+
+       public ImageBuilder imageBuilder() {
+               return database.newImageBuilder();
        }
 
        /**
@@ -741,26 +640,19 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
                        return null;
                }
                logger.info(String.format("Adding Sone from OwnIdentity: %s", ownIdentity));
-               synchronized (sones) {
-                       final Sone sone;
-                       try {
-                               sone = getLocalSone(ownIdentity.getId(), true).setIdentity(ownIdentity).setInsertUri(new FreenetURI(ownIdentity.getInsertUri())).setRequestUri(new FreenetURI(ownIdentity.getRequestUri()));
-                       } catch (MalformedURLException mue1) {
-                               logger.log(Level.SEVERE, String.format("Could not convert the Identity’s URIs to Freenet URIs: %s, %s", ownIdentity.getInsertUri(), ownIdentity.getRequestUri()), mue1);
-                               return null;
-                       }
-                       sone.setLatestEdition(Numbers.safeParseLong(ownIdentity.getProperty("Sone.LatestEdition"), (long) 0));
-                       sone.setClient(new Client("Sone", SonePlugin.VERSION.toString()));
-                       sone.setKnown(true);
-                       /* TODO - load posts ’n stuff */
-                       sones.put(ownIdentity.getId(), sone);
-                       final SoneInserter soneInserter = new SoneInserter(this, eventBus, freenetInterface, sone);
+               Sone sone = database.newSoneBuilder().local().from(ownIdentity).build();
+               sone.setLatestEdition(fromNullable(tryParse(ownIdentity.getProperty("Sone.LatestEdition"))).or(0L));
+               sone.setClient(new Client("Sone", SonePlugin.VERSION.toString()));
+               sone.setKnown(true);
+               SoneInserter soneInserter = new SoneInserter(this, eventBus, freenetInterface, ownIdentity.getId());
+               eventBus.register(soneInserter);
+               synchronized (soneInserters) {
                        soneInserters.put(sone, soneInserter);
-                       sone.setStatus(SoneStatus.idle);
-                       loadSone(sone);
-                       soneInserter.start();
-                       return sone;
                }
+               loadSone(sone);
+               sone.setStatus(SoneStatus.idle);
+               soneInserter.start();
+               return sone;
        }
 
        /**
@@ -776,12 +668,6 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
                        return null;
                }
                Sone sone = addLocalSone(ownIdentity);
-               sone.getOptions().addBooleanOption("AutoFollow", new DefaultOption<Boolean>(false));
-               sone.getOptions().addBooleanOption("EnableSoneInsertNotifications", new DefaultOption<Boolean>(false));
-               sone.getOptions().addBooleanOption("ShowNotification/NewSones", new DefaultOption<Boolean>(true));
-               sone.getOptions().addBooleanOption("ShowNotification/NewPosts", new DefaultOption<Boolean>(true));
-               sone.getOptions().addBooleanOption("ShowNotification/NewReplies", new DefaultOption<Boolean>(true));
-               sone.getOptions().addEnumOption("ShowCustomAvatars", new DefaultOption<ShowCustomAvatars>(ShowCustomAvatars.NEVER));
 
                followSone(sone, "nwa8lHa271k2QvJ8aa0Ov7IHAV-DFOCFgmDt3X6BpCI");
                touchConfiguration();
@@ -800,41 +686,33 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
                        logger.log(Level.WARNING, "Given Identity is null!");
                        return null;
                }
-               synchronized (sones) {
-                       final Sone sone = getRemoteSone(identity.getId(), true);
-                       if (sone.isLocal()) {
-                               return sone;
+               final Long latestEdition = tryParse(fromNullable(
+                               identity.getProperty("Sone.LatestEdition")).or("0"));
+               Optional<Sone> existingSone = getSone(identity.getId());
+               if (existingSone.isPresent() && existingSone.get().isLocal()) {
+                       return existingSone.get();
+               }
+               boolean newSone = !existingSone.isPresent();
+               Sone sone = !newSone ? existingSone.get() : database.newSoneBuilder().from(identity).build();
+               sone.setLatestEdition(latestEdition);
+               if (newSone) {
+                       synchronized (knownSones) {
+                               newSone = !knownSones.contains(sone.getId());
                        }
-                       sone.setIdentity(identity);
-                       boolean newSone = sone.getRequestUri() == null;
-                       sone.setRequestUri(SoneUri.create(identity.getRequestUri()));
-                       sone.setLatestEdition(Numbers.safeParseLong(identity.getProperty("Sone.LatestEdition"), (long) 0));
+                       sone.setKnown(!newSone);
                        if (newSone) {
-                               synchronized (knownSones) {
-                                       newSone = !knownSones.contains(sone.getId());
-                               }
-                               sone.setKnown(!newSone);
-                               if (newSone) {
-                                       eventBus.post(new NewSoneFoundEvent(sone));
-                                       for (Sone localSone : getLocalSones()) {
-                                               if (localSone.getOptions().getBooleanOption("AutoFollow").get()) {
-                                                       followSone(localSone, sone.getId());
-                                               }
+                               eventBus.post(new NewSoneFoundEvent(sone));
+                               for (Sone localSone : getLocalSones()) {
+                                       if (localSone.getOptions().isAutoFollow()) {
+                                               followSone(localSone, sone.getId());
                                        }
                                }
                        }
-                       soneDownloader.addSone(sone);
-                       soneDownloaders.execute(new Runnable() {
-
-                               @Override
-                               @SuppressWarnings("synthetic-access")
-                               public void run() {
-                                       soneDownloader.fetchSone(sone, sone.getRequestUri());
-                               }
-
-                       });
-                       return sone;
                }
+               database.storeSone(sone);
+               soneDownloader.addSone(sone);
+               soneDownloaders.execute(soneDownloader.fetchSoneWithUriAction(sone));
+               return sone;
        }
 
        /**
@@ -848,7 +726,7 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
        public void followSone(Sone sone, String soneId) {
                checkNotNull(sone, "sone must not be null");
                checkNotNull(soneId, "soneId must not be null");
-               sone.addFriend(soneId);
+               database.addFriend(sone, soneId);
                synchronized (soneFollowingTimes) {
                        if (!soneFollowingTimes.containsKey(soneId)) {
                                long now = System.currentTimeMillis();
@@ -883,7 +761,7 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
        public void unfollowSone(Sone sone, String soneId) {
                checkNotNull(sone, "sone must not be null");
                checkNotNull(soneId, "soneId must not be null");
-               sone.removeFriend(soneId);
+               database.removeFriend(sone, soneId);
                boolean unfollowedSoneStillFollowed = false;
                for (Sone localSone : getLocalSones()) {
                        unfollowedSoneStillFollowed |= localSone.hasFriend(soneId);
@@ -986,75 +864,67 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
         *            {@code true} if the stored Sone should be updated regardless
         *            of the age of the given Sone
         */
-       public void updateSone(Sone sone, boolean soneRescueMode) {
+       public void updateSone(final Sone sone, boolean soneRescueMode) {
                Optional<Sone> storedSone = getSone(sone.getId());
                if (storedSone.isPresent()) {
                        if (!soneRescueMode && !(sone.getTime() > storedSone.get().getTime())) {
                                logger.log(Level.FINE, String.format("Downloaded Sone %s is not newer than stored Sone %s.", sone, storedSone));
                                return;
                        }
-                       /* find removed posts. */
-                       Collection<Post> existingPosts = database.getPosts(sone.getId());
-                       for (Post oldPost : existingPosts) {
-                               if (!sone.getPosts().contains(oldPost)) {
-                                       eventBus.post(new PostRemovedEvent(oldPost));
-                               }
+                       List<Object> events =
+                                       collectEventsForChangesInSone(storedSone.get(), sone);
+                       database.storeSone(sone);
+                       for (Object event : events) {
+                               eventBus.post(event);
                        }
-                       /* find new posts. */
-                       for (Post newPost : sone.getPosts()) {
-                               if (existingPosts.contains(newPost)) {
-                                       continue;
-                               }
-                               if (newPost.getTime() < getSoneFollowingTime(sone)) {
-                                       newPost.setKnown(true);
-                               } else if (!newPost.isKnown()) {
-                                       eventBus.post(new NewPostFoundEvent(newPost));
-                               }
-                       }
-                       /* store posts. */
-                       database.storePosts(sone, sone.getPosts());
-                       if (!soneRescueMode) {
-                               for (PostReply reply : storedSone.get().getReplies()) {
-                                       if (!sone.getReplies().contains(reply)) {
-                                               eventBus.post(new PostReplyRemovedEvent(reply));
-                                       }
-                               }
+                       sone.setOptions(storedSone.get().getOptions());
+                       sone.setKnown(storedSone.get().isKnown());
+                       sone.setStatus((sone.getTime() == 0) ? SoneStatus.unknown : SoneStatus.idle);
+                       if (sone.isLocal()) {
+                               touchConfiguration();
                        }
-                       Set<PostReply> storedReplies = storedSone.get().getReplies();
-                       for (PostReply reply : sone.getReplies()) {
-                               if (storedReplies.contains(reply)) {
-                                       continue;
-                               }
-                               if (reply.getTime() < getSoneFollowingTime(sone)) {
-                                       reply.setKnown(true);
-                               } else if (!reply.isKnown()) {
-                                       eventBus.post(new NewPostReplyFoundEvent(reply));
+               }
+       }
+
+       private List<Object> collectEventsForChangesInSone(Sone oldSone,
+                       final Sone newSone) {
+               final List<Object> events = new ArrayList<Object>();
+               SoneChangeDetector soneChangeDetector = new SoneChangeDetector(
+                               oldSone);
+               soneChangeDetector.onNewPosts(new PostProcessor() {
+                       @Override
+                       public void processPost(Post post) {
+                               if (post.getTime() < getSoneFollowingTime(newSone)) {
+                                       post.setKnown(true);
+                               } else if (!post.isKnown()) {
+                                       events.add(new NewPostFoundEvent(post));
                                }
                        }
-                       database.storePostReplies(sone, sone.getReplies());
-                       for (Album album : storedSone.get().getRootAlbum().getAlbums()) {
-                               database.removeAlbum(album);
-                               for (Image image : album.getImages()) {
-                                       database.removeImage(image);
-                               }
+               });
+               soneChangeDetector.onRemovedPosts(new PostProcessor() {
+                       @Override
+                       public void processPost(Post post) {
+                               events.add(new PostRemovedEvent(post));
                        }
-                       for (Album album : sone.getRootAlbum().getAlbums()) {
-                               database.storeAlbum(album);
-                               for (Image image : album.getImages()) {
-                                       database.storeImage(image);
+               });
+               soneChangeDetector.onNewPostReplies(new PostReplyProcessor() {
+                       @Override
+                       public void processPostReply(PostReply postReply) {
+                               if (postReply.getTime() < getSoneFollowingTime(newSone)) {
+                                       postReply.setKnown(true);
+                               } else if (!postReply.isKnown()) {
+                                       events.add(new NewPostReplyFoundEvent(postReply));
                                }
                        }
-                       synchronized (sones) {
-                               sone.setOptions(storedSone.get().getOptions());
-                               sone.setKnown(storedSone.get().isKnown());
-                               sone.setStatus((sone.getTime() == 0) ? SoneStatus.unknown : SoneStatus.idle);
-                               if (sone.isLocal()) {
-                                       soneInserters.get(storedSone.get()).setSone(sone);
-                                       touchConfiguration();
-                               }
-                               sones.put(sone.getId(), sone);
+               });
+               soneChangeDetector.onRemovedPostReplies(new PostReplyProcessor() {
+                       @Override
+                       public void processPostReply(PostReply postReply) {
+                               events.add(new PostReplyRemovedEvent(postReply));
                        }
-               }
+               });
+               soneChangeDetector.detectChanges(newSone);
+               return events;
        }
 
        /**
@@ -1070,15 +940,13 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
                        logger.log(Level.WARNING, String.format("Tried to delete Sone of non-own identity: %s", sone));
                        return;
                }
-               synchronized (sones) {
-                       if (!getLocalSones().contains(sone)) {
-                               logger.log(Level.WARNING, String.format("Tried to delete non-local Sone: %s", sone));
-                               return;
-                       }
-                       sones.remove(sone.getId());
-                       SoneInserter soneInserter = soneInserters.remove(sone);
-                       soneInserter.stop();
+               if (!getLocalSones().contains(sone)) {
+                       logger.log(Level.WARNING, String.format("Tried to delete non-local Sone: %s", sone));
+                       return;
                }
+               SoneInserter soneInserter = soneInserters.remove(sone);
+               soneInserter.stop();
+               database.removeSone(sone);
                webOfTrustUpdater.removeContext((OwnIdentity) sone.getIdentity(), "Sone");
                webOfTrustUpdater.removeProperty((OwnIdentity) sone.getIdentity(), "Sone.LatestEdition");
                try {
@@ -1120,14 +988,6 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
                }
                logger.info(String.format("Loading local Sone: %s", sone));
 
-               /* initialize options. */
-               sone.getOptions().addBooleanOption("AutoFollow", new DefaultOption<Boolean>(false));
-               sone.getOptions().addBooleanOption("EnableSoneInsertNotifications", new DefaultOption<Boolean>(false));
-               sone.getOptions().addBooleanOption("ShowNotification/NewSones", new DefaultOption<Boolean>(true));
-               sone.getOptions().addBooleanOption("ShowNotification/NewPosts", new DefaultOption<Boolean>(true));
-               sone.getOptions().addBooleanOption("ShowNotification/NewReplies", new DefaultOption<Boolean>(true));
-               sone.getOptions().addEnumOption("ShowCustomAvatars", new DefaultOption<ShowCustomAvatars>(ShowCustomAvatars.NEVER));
-
                /* load Sone. */
                String sonePrefix = "Sone/" + sone.getId();
                Long soneTime = configuration.getLongValue(sonePrefix + "/Time").getValue(null);
@@ -1138,169 +998,77 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
                String lastInsertFingerprint = configuration.getStringValue(sonePrefix + "/LastInsertFingerprint").getValue("");
 
                /* load profile. */
-               Profile profile = new Profile(sone);
-               profile.setFirstName(configuration.getStringValue(sonePrefix + "/Profile/FirstName").getValue(null));
-               profile.setMiddleName(configuration.getStringValue(sonePrefix + "/Profile/MiddleName").getValue(null));
-               profile.setLastName(configuration.getStringValue(sonePrefix + "/Profile/LastName").getValue(null));
-               profile.setBirthDay(configuration.getIntValue(sonePrefix + "/Profile/BirthDay").getValue(null));
-               profile.setBirthMonth(configuration.getIntValue(sonePrefix + "/Profile/BirthMonth").getValue(null));
-               profile.setBirthYear(configuration.getIntValue(sonePrefix + "/Profile/BirthYear").getValue(null));
-
-               /* load profile fields. */
-               while (true) {
-                       String fieldPrefix = sonePrefix + "/Profile/Fields/" + profile.getFields().size();
-                       String fieldName = configuration.getStringValue(fieldPrefix + "/Name").getValue(null);
-                       if (fieldName == null) {
-                               break;
-                       }
-                       String fieldValue = configuration.getStringValue(fieldPrefix + "/Value").getValue("");
-                       profile.addField(fieldName).setValue(fieldValue);
-               }
+               ConfigurationSoneParser configurationSoneParser = new ConfigurationSoneParser(configuration, sone);
+               Profile profile = configurationSoneParser.parseProfile();
 
                /* load posts. */
-               Set<Post> posts = new HashSet<Post>();
-               while (true) {
-                       String postPrefix = sonePrefix + "/Posts/" + posts.size();
-                       String postId = configuration.getStringValue(postPrefix + "/ID").getValue(null);
-                       if (postId == null) {
-                               break;
-                       }
-                       String postRecipientId = configuration.getStringValue(postPrefix + "/Recipient").getValue(null);
-                       long postTime = configuration.getLongValue(postPrefix + "/Time").getValue((long) 0);
-                       String postText = configuration.getStringValue(postPrefix + "/Text").getValue(null);
-                       if ((postTime == 0) || (postText == null)) {
-                               logger.log(Level.WARNING, "Invalid post found, aborting load!");
-                               return;
-                       }
-                       PostBuilder postBuilder = postBuilder().withId(postId).from(sone.getId()).withTime(postTime).withText(postText);
-                       if ((postRecipientId != null) && (postRecipientId.length() == 43)) {
-                               postBuilder.to(postRecipientId);
-                       }
-                       posts.add(postBuilder.build());
+               Collection<Post> posts;
+               try {
+                       posts = configurationSoneParser.parsePosts(database);
+               } catch (InvalidPostFound ipf) {
+                       logger.log(Level.WARNING, "Invalid post found, aborting load!");
+                       return;
                }
 
                /* load replies. */
-               Set<PostReply> replies = new HashSet<PostReply>();
-               while (true) {
-                       String replyPrefix = sonePrefix + "/Replies/" + replies.size();
-                       String replyId = configuration.getStringValue(replyPrefix + "/ID").getValue(null);
-                       if (replyId == null) {
-                               break;
-                       }
-                       String postId = configuration.getStringValue(replyPrefix + "/Post/ID").getValue(null);
-                       long replyTime = configuration.getLongValue(replyPrefix + "/Time").getValue((long) 0);
-                       String replyText = configuration.getStringValue(replyPrefix + "/Text").getValue(null);
-                       if ((postId == null) || (replyTime == 0) || (replyText == null)) {
-                               logger.log(Level.WARNING, "Invalid reply found, aborting load!");
-                               return;
-                       }
-                       PostReplyBuilder postReplyBuilder = postReplyBuilder().withId(replyId).from(sone.getId()).to(postId).withTime(replyTime).withText(replyText);
-                       replies.add(postReplyBuilder.build());
+               Collection<PostReply> replies;
+               try {
+                       replies = configurationSoneParser.parsePostReplies(database);
+               } catch (InvalidPostReplyFound iprf) {
+                       logger.log(Level.WARNING, "Invalid reply found, aborting load!");
+                       return;
                }
 
                /* load post likes. */
-               Set<String> likedPostIds = new HashSet<String>();
-               while (true) {
-                       String likedPostId = configuration.getStringValue(sonePrefix + "/Likes/Post/" + likedPostIds.size() + "/ID").getValue(null);
-                       if (likedPostId == null) {
-                               break;
-                       }
-                       likedPostIds.add(likedPostId);
-               }
+               Set<String> likedPostIds =
+                               configurationSoneParser.parseLikedPostIds();
 
                /* load reply likes. */
-               Set<String> likedReplyIds = new HashSet<String>();
-               while (true) {
-                       String likedReplyId = configuration.getStringValue(sonePrefix + "/Likes/Reply/" + likedReplyIds.size() + "/ID").getValue(null);
-                       if (likedReplyId == null) {
-                               break;
-                       }
-                       likedReplyIds.add(likedReplyId);
-               }
-
-               /* load friends. */
-               Set<String> friends = new HashSet<String>();
-               while (true) {
-                       String friendId = configuration.getStringValue(sonePrefix + "/Friends/" + friends.size() + "/ID").getValue(null);
-                       if (friendId == null) {
-                               break;
-                       }
-                       friends.add(friendId);
-               }
+               Set<String> likedReplyIds =
+                               configurationSoneParser.parseLikedPostReplyIds();
 
                /* load albums. */
-               List<Album> topLevelAlbums = new ArrayList<Album>();
-               int albumCounter = 0;
-               while (true) {
-                       String albumPrefix = sonePrefix + "/Albums/" + albumCounter++;
-                       String albumId = configuration.getStringValue(albumPrefix + "/ID").getValue(null);
-                       if (albumId == null) {
-                               break;
-                       }
-                       String albumTitle = configuration.getStringValue(albumPrefix + "/Title").getValue(null);
-                       String albumDescription = configuration.getStringValue(albumPrefix + "/Description").getValue(null);
-                       String albumParentId = configuration.getStringValue(albumPrefix + "/Parent").getValue(null);
-                       String albumImageId = configuration.getStringValue(albumPrefix + "/AlbumImage").getValue(null);
-                       if ((albumTitle == null) || (albumDescription == null)) {
-                               logger.log(Level.WARNING, "Invalid album found, aborting load!");
-                               return;
-                       }
-                       Album album = getAlbum(albumId).setSone(sone).modify().setTitle(albumTitle).setDescription(albumDescription).setAlbumImage(albumImageId).update();
-                       if (albumParentId != null) {
-                               Album parentAlbum = getAlbum(albumParentId, false);
-                               if (parentAlbum == null) {
-                                       logger.log(Level.WARNING, String.format("Invalid parent album ID: %s", albumParentId));
-                                       return;
-                               }
-                               parentAlbum.addAlbum(album);
-                       } else {
-                               if (!topLevelAlbums.contains(album)) {
-                                       topLevelAlbums.add(album);
-                               }
-                       }
+               List<Album> topLevelAlbums;
+               try {
+                       topLevelAlbums =
+                                       configurationSoneParser.parseTopLevelAlbums(database);
+               } catch (InvalidAlbumFound iaf) {
+                       logger.log(Level.WARNING, "Invalid album found, aborting load!");
+                       return;
+               } catch (InvalidParentAlbumFound ipaf) {
+                       logger.log(Level.WARNING, format("Invalid parent album ID: %s",
+                                       ipaf.getAlbumParentId()));
+                       return;
                }
 
                /* load images. */
-               int imageCounter = 0;
-               while (true) {
-                       String imagePrefix = sonePrefix + "/Images/" + imageCounter++;
-                       String imageId = configuration.getStringValue(imagePrefix + "/ID").getValue(null);
-                       if (imageId == null) {
-                               break;
-                       }
-                       String albumId = configuration.getStringValue(imagePrefix + "/Album").getValue(null);
-                       String key = configuration.getStringValue(imagePrefix + "/Key").getValue(null);
-                       String title = configuration.getStringValue(imagePrefix + "/Title").getValue(null);
-                       String description = configuration.getStringValue(imagePrefix + "/Description").getValue(null);
-                       Long creationTime = configuration.getLongValue(imagePrefix + "/CreationTime").getValue(null);
-                       Integer width = configuration.getIntValue(imagePrefix + "/Width").getValue(null);
-                       Integer height = configuration.getIntValue(imagePrefix + "/Height").getValue(null);
-                       if ((albumId == null) || (key == null) || (title == null) || (description == null) || (creationTime == null) || (width == null) || (height == null)) {
-                               logger.log(Level.WARNING, "Invalid image found, aborting load!");
-                               return;
-                       }
-                       Album album = getAlbum(albumId, false);
-                       if (album == null) {
-                               logger.log(Level.WARNING, "Invalid album image encountered, aborting load!");
-                               return;
-                       }
-                       Image image = getImage(imageId).modify().setSone(sone).setCreationTime(creationTime).setKey(key).setTitle(title).setDescription(description).setWidth(width).setHeight(height).update();
-                       album.addImage(image);
+               try {
+                       configurationSoneParser.parseImages(database);
+               } catch (InvalidImageFound iif) {
+                       logger.log(WARNING, "Invalid image found, aborting load!");
+                       return;
+               } catch (InvalidParentAlbumFound ipaf) {
+                       logger.log(Level.WARNING,
+                                       format("Invalid album image (%s) encountered, aborting load!",
+                                                       ipaf.getAlbumParentId()));
+                       return;
                }
 
                /* load avatar. */
                String avatarId = configuration.getStringValue(sonePrefix + "/Profile/Avatar").getValue(null);
                if (avatarId != null) {
-                       profile.setAvatar(getImage(avatarId, false));
+                       final Map<String, Image> images =
+                                       configurationSoneParser.getImages();
+                       profile.setAvatar(images.get(avatarId));
                }
 
                /* load options. */
-               sone.getOptions().getBooleanOption("AutoFollow").set(configuration.getBooleanValue(sonePrefix + "/Options/AutoFollow").getValue(null));
-               sone.getOptions().getBooleanOption("EnableSoneInsertNotifications").set(configuration.getBooleanValue(sonePrefix + "/Options/EnableSoneInsertNotifications").getValue(null));
-               sone.getOptions().getBooleanOption("ShowNotification/NewSones").set(configuration.getBooleanValue(sonePrefix + "/Options/ShowNotification/NewSones").getValue(null));
-               sone.getOptions().getBooleanOption("ShowNotification/NewPosts").set(configuration.getBooleanValue(sonePrefix + "/Options/ShowNotification/NewPosts").getValue(null));
-               sone.getOptions().getBooleanOption("ShowNotification/NewReplies").set(configuration.getBooleanValue(sonePrefix + "/Options/ShowNotification/NewReplies").getValue(null));
-               sone.getOptions().<ShowCustomAvatars> getEnumOption("ShowCustomAvatars").set(ShowCustomAvatars.valueOf(configuration.getStringValue(sonePrefix + "/Options/ShowCustomAvatars").getValue(ShowCustomAvatars.NEVER.name())));
+               sone.getOptions().setAutoFollow(configuration.getBooleanValue(sonePrefix + "/Options/AutoFollow").getValue(null));
+               sone.getOptions().setSoneInsertNotificationEnabled(configuration.getBooleanValue(sonePrefix + "/Options/EnableSoneInsertNotifications").getValue(null));
+               sone.getOptions().setShowNewSoneNotifications(configuration.getBooleanValue(sonePrefix + "/Options/ShowNotification/NewSones").getValue(null));
+               sone.getOptions().setShowNewPostNotifications(configuration.getBooleanValue(sonePrefix + "/Options/ShowNotification/NewPosts").getValue(null));
+               sone.getOptions().setShowNewReplyNotifications(configuration.getBooleanValue(sonePrefix + "/Options/ShowNotification/NewReplies").getValue(null));
+               sone.getOptions().setShowCustomAvatars(ShowCustomAvatars.valueOf(configuration.getStringValue(sonePrefix + "/Options/ShowCustomAvatars").getValue(ShowCustomAvatars.NEVER.name())));
 
                /* if we’re still here, Sone was loaded successfully. */
                synchronized (sone) {
@@ -1310,27 +1078,20 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
                        sone.setReplies(replies);
                        sone.setLikePostIds(likedPostIds);
                        sone.setLikeReplyIds(likedReplyIds);
-                       for (String friendId : friends) {
-                               followSone(sone, friendId);
-                       }
                        for (Album album : sone.getRootAlbum().getAlbums()) {
                                sone.getRootAlbum().removeAlbum(album);
                        }
                        for (Album album : topLevelAlbums) {
                                sone.getRootAlbum().addAlbum(album);
                        }
-                       soneInserters.get(sone).setLastInsertFingerprint(lastInsertFingerprint);
-               }
-               synchronized (knownSones) {
-                       for (String friend : friends) {
-                               knownSones.add(friend);
+                       database.storeSone(sone);
+                       synchronized (soneInserters) {
+                               soneInserters.get(sone).setLastInsertFingerprint(lastInsertFingerprint);
                        }
                }
-               database.storePosts(sone, posts);
                for (Post post : posts) {
                        post.setKnown(true);
                }
-               database.storePostReplies(sone, replies);
                for (PostReply reply : replies) {
                        reply.setKnown(true);
                }
@@ -1343,34 +1104,6 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
         *
         * @param sone
         *            The Sone that creates the post
-        * @param text
-        *            The text of the post
-        * @return The created post
-        */
-       public Post createPost(Sone sone, String text) {
-               return createPost(sone, System.currentTimeMillis(), text);
-       }
-
-       /**
-        * Creates a new post.
-        *
-        * @param sone
-        *            The Sone that creates the post
-        * @param time
-        *            The time of the post
-        * @param text
-        *            The text of the post
-        * @return The created post
-        */
-       public Post createPost(Sone sone, long time, String text) {
-               return createPost(sone, null, time, text);
-       }
-
-       /**
-        * Creates a new post.
-        *
-        * @param sone
-        *            The Sone that creates the post
         * @param recipient
         *            The recipient Sone, or {@code null} if this post does not have
         *            a recipient
@@ -1379,24 +1112,6 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
         * @return The created post
         */
        public Post createPost(Sone sone, Optional<Sone> recipient, String text) {
-               return createPost(sone, recipient, System.currentTimeMillis(), text);
-       }
-
-       /**
-        * Creates a new post.
-        *
-        * @param sone
-        *            The Sone that creates the post
-        * @param recipient
-        *            The recipient Sone, or {@code null} if this post does not have
-        *            a recipient
-        * @param time
-        *            The time of the post
-        * @param text
-        *            The text of the post
-        * @return The created post
-        */
-       public Post createPost(Sone sone, Optional<Sone> recipient, long time, String text) {
                checkNotNull(text, "text must not be null");
                checkArgument(text.trim().length() > 0, "text must not be empty");
                if (!sone.isLocal()) {
@@ -1404,7 +1119,7 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
                        return null;
                }
                PostBuilder postBuilder = database.newPostBuilder();
-               postBuilder.from(sone.getId()).randomId().withTime(time).withText(text.trim());
+               postBuilder.from(sone.getId()).randomId().currentTime().withText(text.trim());
                if (recipient.isPresent()) {
                        postBuilder.to(recipient.get().getId());
                }
@@ -1413,16 +1128,7 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
                eventBus.post(new NewPostFoundEvent(post));
                sone.addPost(post);
                touchConfiguration();
-               localElementTicker.schedule(new Runnable() {
-
-                       /**
-                        * {@inheritDoc}
-                        */
-                       @Override
-                       public void run() {
-                               markPostKnown(post);
-                       }
-               }, 10, TimeUnit.SECONDS);
+               localElementTicker.schedule(new MarkPostKnown(post), 10, TimeUnit.SECONDS);
                return post;
        }
 
@@ -1459,26 +1165,8 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
                }
        }
 
-       /**
-        * Bookmarks the given post.
-        *
-        * @param post
-        *            The post to bookmark
-        */
-       public void bookmark(Post post) {
-               bookmarkPost(post.getId());
-       }
-
-       /**
-        * Bookmarks the post with the given ID.
-        *
-        * @param id
-        *            The ID of the post to bookmark
-        */
-       public void bookmarkPost(String id) {
-               synchronized (bookmarkedPosts) {
-                       bookmarkedPosts.add(id);
-               }
+       public void bookmarkPost(Post post) {
+               database.bookmarkPost(post);
        }
 
        /**
@@ -1487,20 +1175,8 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
         * @param post
         *            The post to unbookmark
         */
-       public void unbookmark(Post post) {
-               unbookmarkPost(post.getId());
-       }
-
-       /**
-        * Removes the post with the given ID from the bookmarks.
-        *
-        * @param id
-        *            The ID of the post to unbookmark
-        */
-       public void unbookmarkPost(String id) {
-               synchronized (bookmarkedPosts) {
-                       bookmarkedPosts.remove(id);
-               }
+       public void unbookmarkPost(Post post) {
+               database.unbookmarkPost(post);
        }
 
        /**
@@ -1528,16 +1204,7 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
                eventBus.post(new NewPostReplyFoundEvent(reply));
                sone.addReply(reply);
                touchConfiguration();
-               localElementTicker.schedule(new Runnable() {
-
-                       /**
-                        * {@inheritDoc}
-                        */
-                       @Override
-                       public void run() {
-                               markReplyKnown(reply);
-                       }
-               }, 10, TimeUnit.SECONDS);
+               localElementTicker.schedule(new MarkReplyKnown(reply), 10, TimeUnit.SECONDS);
                return reply;
        }
 
@@ -1576,17 +1243,6 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
        }
 
        /**
-        * Creates a new top-level album for the given Sone.
-        *
-        * @param sone
-        *            The Sone to create the album for
-        * @return The new album
-        */
-       public Album createAlbum(Sone sone) {
-               return createAlbum(sone, sone.getRootAlbum());
-       }
-
-       /**
         * Creates a new album for the given Sone.
         *
         * @param sone
@@ -1597,9 +1253,8 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
         * @return The new album
         */
        public Album createAlbum(Sone sone, Album parent) {
-               Album album = database.newAlbumBuilder().randomId().build();
+               Album album = database.newAlbumBuilder().randomId().by(sone).build();
                database.storeAlbum(album);
-               album.setSone(sone);
                parent.addAlbum(album);
                return album;
        }
@@ -1650,7 +1305,7 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
         * Deletes the given image. This method will also delete a matching
         * temporary image.
         *
-        * @see #deleteTemporaryImage(TemporaryImage)
+        * @see #deleteTemporaryImage(String)
         * @param image
         *            The image to delete
         */
@@ -1682,17 +1337,6 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
        }
 
        /**
-        * Deletes the given temporary image.
-        *
-        * @param temporaryImage
-        *            The temporary image to delete
-        */
-       public void deleteTemporaryImage(TemporaryImage temporaryImage) {
-               checkNotNull(temporaryImage, "temporaryImage must not be null");
-               deleteTemporaryImage(temporaryImage.getId());
-       }
-
-       /**
         * Deletes the temporary image with the given ID.
         *
         * @param imageId
@@ -1760,10 +1404,15 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
        @Override
        public void serviceStop() {
                localElementTicker.shutdownNow();
-               synchronized (sones) {
+               synchronized (soneInserters) {
                        for (Entry<Sone, SoneInserter> soneInserter : soneInserters.entrySet()) {
                                soneInserter.getValue().stop();
-                               saveSone(getLocalSone(soneInserter.getKey().getId(), false));
+                               saveSone(soneInserter.getKey());
+                       }
+               }
+               synchronized (soneRescuers) {
+                       for (SoneRescuer soneRescuer : soneRescuers.values()) {
+                               soneRescuer.stop();
                        }
                }
                saveConfiguration();
@@ -1858,13 +1507,6 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
                        }
                        configuration.getStringValue(sonePrefix + "/Likes/Reply/" + replyLikeCounter + "/ID").setValue(null);
 
-                       /* save friends. */
-                       int friendCounter = 0;
-                       for (String friendId : sone.getFriends()) {
-                               configuration.getStringValue(sonePrefix + "/Friends/" + friendCounter++ + "/ID").setValue(friendId);
-                       }
-                       configuration.getStringValue(sonePrefix + "/Friends/" + friendCounter + "/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();
 
@@ -1900,12 +1542,12 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
                        configuration.getStringValue(sonePrefix + "/Images/" + imageCounter + "/ID").setValue(null);
 
                        /* save options. */
-                       configuration.getBooleanValue(sonePrefix + "/Options/AutoFollow").setValue(sone.getOptions().getBooleanOption("AutoFollow").getReal());
-                       configuration.getBooleanValue(sonePrefix + "/Options/ShowNotification/NewSones").setValue(sone.getOptions().getBooleanOption("ShowNotification/NewSones").getReal());
-                       configuration.getBooleanValue(sonePrefix + "/Options/ShowNotification/NewPosts").setValue(sone.getOptions().getBooleanOption("ShowNotification/NewPosts").getReal());
-                       configuration.getBooleanValue(sonePrefix + "/Options/ShowNotification/NewReplies").setValue(sone.getOptions().getBooleanOption("ShowNotification/NewReplies").getReal());
-                       configuration.getBooleanValue(sonePrefix + "/Options/EnableSoneInsertNotifications").setValue(sone.getOptions().getBooleanOption("EnableSoneInsertNotifications").getReal());
-                       configuration.getStringValue(sonePrefix + "/Options/ShowCustomAvatars").setValue(sone.getOptions().<ShowCustomAvatars> getEnumOption("ShowCustomAvatars").get().name());
+                       configuration.getBooleanValue(sonePrefix + "/Options/AutoFollow").setValue(sone.getOptions().isAutoFollow());
+                       configuration.getBooleanValue(sonePrefix + "/Options/EnableSoneInsertNotifications").setValue(sone.getOptions().isSoneInsertNotificationEnabled());
+                       configuration.getBooleanValue(sonePrefix + "/Options/ShowNotification/NewSones").setValue(sone.getOptions().isShowNewSoneNotifications());
+                       configuration.getBooleanValue(sonePrefix + "/Options/ShowNotification/NewPosts").setValue(sone.getOptions().isShowNewPostNotifications());
+                       configuration.getBooleanValue(sonePrefix + "/Options/ShowNotification/NewReplies").setValue(sone.getOptions().isShowNewReplyNotifications());
+                       configuration.getStringValue(sonePrefix + "/Options/ShowCustomAvatars").setValue(sone.getOptions().getShowCustomAvatars().name());
 
                        configuration.save();
 
@@ -1931,18 +1573,7 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
 
                /* store the options first. */
                try {
-                       configuration.getIntValue("Option/ConfigurationVersion").setValue(0);
-                       configuration.getIntValue("Option/InsertionDelay").setValue(options.getIntegerOption("InsertionDelay").getReal());
-                       configuration.getIntValue("Option/PostsPerPage").setValue(options.getIntegerOption("PostsPerPage").getReal());
-                       configuration.getIntValue("Option/ImagesPerPage").setValue(options.getIntegerOption("ImagesPerPage").getReal());
-                       configuration.getIntValue("Option/CharactersPerPost").setValue(options.getIntegerOption("CharactersPerPost").getReal());
-                       configuration.getIntValue("Option/PostCutOffLength").setValue(options.getIntegerOption("PostCutOffLength").getReal());
-                       configuration.getBooleanValue("Option/RequireFullAccess").setValue(options.getBooleanOption("RequireFullAccess").getReal());
-                       configuration.getIntValue("Option/PositiveTrust").setValue(options.getIntegerOption("PositiveTrust").getReal());
-                       configuration.getIntValue("Option/NegativeTrust").setValue(options.getIntegerOption("NegativeTrust").getReal());
-                       configuration.getStringValue("Option/TrustComment").setValue(options.getStringOption("TrustComment").getReal());
-                       configuration.getBooleanValue("Option/ActivateFcpInterface").setValue(options.getBooleanOption("ActivateFcpInterface").getReal());
-                       configuration.getIntValue("Option/FcpFullAccessRequired").setValue(options.getIntegerOption("FcpFullAccessRequired").getReal());
+                       preferences.saveTo(configuration);
 
                        /* save known Sones. */
                        int soneCounter = 0;
@@ -1967,15 +1598,6 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
                        /* save known posts. */
                        database.save();
 
-                       /* save bookmarked posts. */
-                       int bookmarkedPostCounter = 0;
-                       synchronized (bookmarkedPosts) {
-                               for (String bookmarkedPostId : bookmarkedPosts) {
-                                       configuration.getStringValue("Bookmarks/Post/" + bookmarkedPostCounter++ + "/ID").setValue(bookmarkedPostId);
-                               }
-                       }
-                       configuration.getStringValue("Bookmarks/Post/" + bookmarkedPostCounter++ + "/ID").setValue(null);
-
                        /* now save it. */
                        configuration.save();
 
@@ -1994,52 +1616,7 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
         * Loads the configuration.
         */
        private void loadConfiguration() {
-               /* create options. */
-               options.addIntegerOption("InsertionDelay", new DefaultOption<Integer>(60, new IntegerRangePredicate(0, Integer.MAX_VALUE), new OptionWatcher<Integer>() {
-
-                       @Override
-                       public void optionChanged(Option<Integer> option, Integer oldValue, Integer newValue) {
-                               SoneInserter.setInsertionDelay(newValue);
-                       }
-
-               }));
-               options.addIntegerOption("PostsPerPage", new DefaultOption<Integer>(10, new IntegerRangePredicate(1, Integer.MAX_VALUE)));
-               options.addIntegerOption("ImagesPerPage", new DefaultOption<Integer>(9, new IntegerRangePredicate(1, Integer.MAX_VALUE)));
-               options.addIntegerOption("CharactersPerPost", new DefaultOption<Integer>(400, Predicates.<Integer> or(new IntegerRangePredicate(50, Integer.MAX_VALUE), Predicates.equalTo(-1))));
-               options.addIntegerOption("PostCutOffLength", new DefaultOption<Integer>(200, Predicates.<Integer> or(new IntegerRangePredicate(50, Integer.MAX_VALUE), Predicates.equalTo(-1))));
-               options.addBooleanOption("RequireFullAccess", new DefaultOption<Boolean>(false));
-               options.addIntegerOption("PositiveTrust", new DefaultOption<Integer>(75, new IntegerRangePredicate(0, 100)));
-               options.addIntegerOption("NegativeTrust", new DefaultOption<Integer>(-25, new IntegerRangePredicate(-100, 100)));
-               options.addStringOption("TrustComment", new DefaultOption<String>("Set from Sone Web Interface"));
-               options.addBooleanOption("ActivateFcpInterface", new DefaultOption<Boolean>(false, new OptionWatcher<Boolean>() {
-
-                       @Override
-                       @SuppressWarnings("synthetic-access")
-                       public void optionChanged(Option<Boolean> option, Boolean oldValue, Boolean newValue) {
-                               fcpInterface.setActive(newValue);
-                       }
-               }));
-               options.addIntegerOption("FcpFullAccessRequired", new DefaultOption<Integer>(2, new OptionWatcher<Integer>() {
-
-                       @Override
-                       @SuppressWarnings("synthetic-access")
-                       public void optionChanged(Option<Integer> option, Integer oldValue, Integer newValue) {
-                               fcpInterface.setFullAccessRequired(FullAccessRequired.values()[newValue]);
-                       }
-
-               }));
-
-               loadConfigurationValue("InsertionDelay");
-               loadConfigurationValue("PostsPerPage");
-               loadConfigurationValue("ImagesPerPage");
-               loadConfigurationValue("CharactersPerPost");
-               loadConfigurationValue("PostCutOffLength");
-               options.getBooleanOption("RequireFullAccess").set(configuration.getBooleanValue("Option/RequireFullAccess").getValue(null));
-               loadConfigurationValue("PositiveTrust");
-               loadConfigurationValue("NegativeTrust");
-               options.getStringOption("TrustComment").set(configuration.getStringValue("Option/TrustComment").getValue(null));
-               options.getBooleanOption("ActivateFcpInterface").set(configuration.getBooleanValue("Option/ActivateFcpInterface").getValue(null));
-               options.getIntegerOption("FcpFullAccessRequired").set(configuration.getIntValue("Option/FcpFullAccessRequired").getValue(null));
+               new PreferencesLoader(preferences).loadFrom(configuration);
 
                /* load known Sones. */
                int soneCounter = 0;
@@ -2066,34 +1643,6 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
                        }
                        ++soneCounter;
                }
-
-               /* load bookmarked posts. */
-               int bookmarkedPostCounter = 0;
-               while (true) {
-                       String bookmarkedPostId = configuration.getStringValue("Bookmarks/Post/" + bookmarkedPostCounter++ + "/ID").getValue(null);
-                       if (bookmarkedPostId == null) {
-                               break;
-                       }
-                       synchronized (bookmarkedPosts) {
-                               bookmarkedPosts.add(bookmarkedPostId);
-                       }
-               }
-
-       }
-
-       /**
-        * Loads an {@link Integer} configuration value for the option with the
-        * given name, logging validation failures.
-        *
-        * @param optionName
-        *            The name of the option to load
-        */
-       private void loadConfigurationValue(String optionName) {
-               try {
-                       options.getIntegerOption(optionName).set(configuration.getIntValue("Option/" + optionName).getValue(null));
-               } catch (IllegalArgumentException iae1) {
-                       logger.log(Level.WARNING, String.format("Invalid value for %s in configuration, using default.", optionName));
-               }
        }
 
        /**
@@ -2146,22 +1695,14 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
         */
        @Subscribe
        public void identityUpdated(IdentityUpdatedEvent identityUpdatedEvent) {
-               final Identity identity = identityUpdatedEvent.identity();
-               soneDownloaders.execute(new Runnable() {
-
-                       @Override
-                       @SuppressWarnings("synthetic-access")
-                       public void run() {
-                               Sone sone = getRemoteSone(identity.getId(), false);
-                               if (sone.isLocal()) {
-                                       return;
-                               }
-                               sone.setIdentity(identity);
-                               sone.setLatestEdition(Numbers.safeParseLong(identity.getProperty("Sone.LatestEdition"), sone.getLatestEdition()));
-                               soneDownloader.addSone(sone);
-                               soneDownloader.fetchSone(sone);
-                       }
-               });
+               Identity identity = identityUpdatedEvent.identity();
+               final Sone sone = getRemoteSone(identity.getId());
+               if (sone.isLocal()) {
+                       return;
+               }
+               sone.setLatestEdition(fromNullable(tryParse(identity.getProperty("Sone.LatestEdition"))).or(sone.getLatestEdition()));
+               soneDownloader.addSone(sone);
+               soneDownloaders.execute(soneDownloader.fetchSoneAction(sone));
        }
 
        /**
@@ -2175,35 +1716,20 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
                OwnIdentity ownIdentity = identityRemovedEvent.ownIdentity();
                Identity identity = identityRemovedEvent.identity();
                trustedIdentities.remove(ownIdentity, identity);
-               boolean foundIdentity = false;
                for (Entry<OwnIdentity, Collection<Identity>> trustedIdentity : trustedIdentities.asMap().entrySet()) {
                        if (trustedIdentity.getKey().equals(ownIdentity)) {
                                continue;
                        }
                        if (trustedIdentity.getValue().contains(identity)) {
-                               foundIdentity = true;
+                               return;
                        }
                }
-               if (foundIdentity) {
-                       /* some local identity still trusts this identity, don’t remove. */
-                       return;
-               }
                Optional<Sone> sone = getSone(identity.getId());
                if (!sone.isPresent()) {
                        /* TODO - we don’t have the Sone anymore. should this happen? */
                        return;
                }
-               database.removePosts(sone.get());
-               for (Post post : sone.get().getPosts()) {
-                       eventBus.post(new PostRemovedEvent(post));
-               }
-               database.removePostReplies(sone.get());
-               for (PostReply reply : sone.get().getReplies()) {
-                       eventBus.post(new PostReplyRemovedEvent(reply));
-               }
-               synchronized (sones) {
-                       sones.remove(identity.getId());
-               }
+               database.removeSone(sone.get());
                eventBus.post(new SoneRemovedEvent(sone.get()));
        }
 
@@ -2221,4 +1747,36 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
                touchConfiguration();
        }
 
+       @VisibleForTesting
+       class MarkPostKnown implements Runnable {
+
+               private final Post post;
+
+               public MarkPostKnown(Post post) {
+                       this.post = post;
+               }
+
+               @Override
+               public void run() {
+                       markPostKnown(post);
+               }
+
+       }
+
+       @VisibleForTesting
+       class MarkReplyKnown implements Runnable {
+
+               private final PostReply postReply;
+
+               public MarkReplyKnown(PostReply postReply) {
+                       this.postReply = postReply;
+               }
+
+               @Override
+               public void run() {
+                       markReplyKnown(postReply);
+               }
+
+       }
+
 }
index e92cf8e..e802ba2 100644 (file)
 
 package net.pterodactylus.sone.core;
 
+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.net.MalformedURLException;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
-import java.util.concurrent.TimeUnit;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 
@@ -32,17 +37,17 @@ 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.util.logging.Logging;
 
-import com.db4o.ObjectContainer;
+import com.google.common.base.Function;
 import com.google.common.eventbus.EventBus;
 import com.google.inject.Inject;
+import com.google.inject.Singleton;
 
 import freenet.client.ClientMetadata;
 import freenet.client.FetchException;
+import freenet.client.FetchException.FetchExceptionMode;
 import freenet.client.FetchResult;
 import freenet.client.HighLevelSimpleClient;
-import freenet.client.HighLevelSimpleClientImpl;
 import freenet.client.InsertBlock;
 import freenet.client.InsertContext;
 import freenet.client.InsertException;
@@ -55,19 +60,23 @@ import freenet.keys.FreenetURI;
 import freenet.keys.InsertableClientSSK;
 import freenet.keys.USK;
 import freenet.node.Node;
+import freenet.node.RequestClient;
 import freenet.node.RequestStarter;
 import freenet.support.api.Bucket;
+import freenet.support.api.RandomAccessBucket;
 import freenet.support.io.ArrayBucket;
+import freenet.support.io.ResumeFailedException;
 
 /**
  * Contains all necessary functionality for interacting with the Freenet node.
  *
  * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
  */
+@Singleton
 public class FreenetInterface {
 
        /** The logger. */
-       private static final Logger logger = Logging.getLogger(FreenetInterface.class);
+       private static final Logger logger = getLogger("Sone.FreenetInterface");
 
        /** The event bus. */
        private final EventBus eventBus;
@@ -84,6 +93,18 @@ public class FreenetInterface {
        /** The not-Sone-related USK callbacks. */
        private final Map<FreenetURI, USKCallback> uriUskCallbacks = Collections.synchronizedMap(new HashMap<FreenetURI, USKCallback>());
 
+       private final RequestClient imageInserts = new RequestClient() {
+               @Override
+               public boolean persistent() {
+                       return false;
+               }
+
+               @Override
+               public boolean realTimeFlag() {
+                       return true;
+               }
+       };
+
        /**
         * Creates a new Freenet interface.
         *
@@ -111,14 +132,13 @@ public class FreenetInterface {
         * @return The result of the fetch, or {@code null} if an error occured
         */
        public Fetched fetchUri(FreenetURI uri) {
-               FetchResult fetchResult = null;
                FreenetURI currentUri = new FreenetURI(uri);
                while (true) {
                        try {
-                               fetchResult = client.fetch(currentUri);
+                               FetchResult fetchResult = client.fetch(currentUri);
                                return new Fetched(currentUri, fetchResult);
                        } catch (FetchException fe1) {
-                               if (fe1.getMode() == FetchException.PERMANENT_REDIRECT) {
+                               if (fe1.getMode() == FetchExceptionMode.PERMANENT_REDIRECT) {
                                        currentUri = fe1.newURI;
                                        continue;
                                }
@@ -129,16 +149,6 @@ public class FreenetInterface {
        }
 
        /**
-        * Creates a key pair.
-        *
-        * @return The request key at index 0, the insert key at index 1
-        */
-       public String[] generateKeyPair() {
-               FreenetURI[] keyPair = client.generateKeyPair("");
-               return new String[] { keyPair[1].toString(), keyPair[0].toString() };
-       }
-
-       /**
         * Inserts the image data of the given {@link TemporaryImage} and returns
         * the given insert token that can be used to add listeners or cancel the
         * insert.
@@ -157,11 +167,12 @@ public class FreenetInterface {
                InsertableClientSSK key = InsertableClientSSK.createRandom(node.random, "");
                FreenetURI targetUri = key.getInsertURI().setDocName(filenameHint);
                InsertContext insertContext = client.getInsertContext(true);
-               Bucket bucket = new ArrayBucket(temporaryImage.getImageData());
+               RandomAccessBucket bucket = new ArrayBucket(temporaryImage.getImageData());
+               insertToken.setBucket(bucket);
                ClientMetadata metadata = new ClientMetadata(temporaryImage.getMimeType());
                InsertBlock insertBlock = new InsertBlock(bucket, metadata, targetUri);
                try {
-                       ClientPutter clientPutter = client.insert(insertBlock, false, null, false, insertContext, insertToken, RequestStarter.INTERACTIVE_PRIORITY_CLASS);
+                       ClientPutter clientPutter = client.insert(insertBlock, null, false, insertContext, insertToken, RequestStarter.INTERACTIVE_PRIORITY_CLASS);
                        insertToken.setClientPutter(clientPutter);
                } catch (InsertException ie1) {
                        throw new SoneInsertException("Could not start image insert.", ie1);
@@ -189,51 +200,30 @@ public class FreenetInterface {
                }
        }
 
-       /**
-        * Registers the USK for the given Sone and notifies the given
-        * {@link SoneDownloader} if an update was found.
-        *
-        * @param sone
-        *            The Sone to watch
-        * @param soneDownloader
-        *            The Sone download to notify on updates
-        */
-       public void registerUsk(final Sone sone, final SoneDownloader soneDownloader) {
+       public void registerActiveUsk(FreenetURI requestUri,
+                       USKCallback uskCallback) {
                try {
-                       logger.log(Level.FINE, String.format("Registering Sone “%s” for USK updates at %s…", sone, sone.getRequestUri().setMetaString(new String[] { "sone.xml" })));
-                       USKCallback uskCallback = new USKCallback() {
-
-                               @Override
-                               @SuppressWarnings("synthetic-access")
-                               public void onFoundEdition(long edition, USK key, ObjectContainer objectContainer, ClientContext clientContext, boolean metadata, short codec, byte[] data, boolean newKnownGood, boolean newSlotToo) {
-                                       logger.log(Level.FINE, String.format("Found USK update for Sone “%s” at %s, new known good: %s, new slot too: %s.", sone, key, newKnownGood, newSlotToo));
-                                       if (edition > sone.getLatestEdition()) {
-                                               sone.setLatestEdition(edition);
-                                               new Thread(new Runnable() {
-
-                                                       @Override
-                                                       public void run() {
-                                                               soneDownloader.fetchSone(sone);
-                                                       }
-                                               }, "Sone Downloader").start();
-                                       }
-                               }
-
-                               @Override
-                               public short getPollingPriorityProgress() {
-                                       return RequestStarter.INTERACTIVE_PRIORITY_CLASS;
-                               }
+                       soneUskCallbacks.put(routingKey(requestUri), uskCallback);
+                       node.clientCore.uskManager.subscribe(create(requestUri),
+                                       uskCallback, true, (RequestClient) client);
+               } catch (MalformedURLException mue1) {
+                       logger.log(WARNING, format("Could not subscribe USK “%s”!",
+                                       requestUri), mue1);
+               }
+       }
 
-                               @Override
-                               public short getPollingPriorityNormal() {
-                                       return RequestStarter.INTERACTIVE_PRIORITY_CLASS;
-                               }
-                       };
-                       soneUskCallbacks.put(sone.getId(), uskCallback);
-                       boolean runBackgroundFetch = (System.currentTimeMillis() - sone.getTime()) < TimeUnit.DAYS.toMillis(7);
-                       node.clientCore.uskManager.subscribe(USK.create(sone.getRequestUri()), uskCallback, runBackgroundFetch, (HighLevelSimpleClientImpl) client);
+       public void registerPassiveUsk(FreenetURI requestUri,
+                       USKCallback uskCallback) {
+               try {
+                       soneUskCallbacks.put(routingKey(requestUri), uskCallback);
+                       node.clientCore
+                                       .uskManager
+                                       .subscribe(create(requestUri), uskCallback, false,
+                                                       (RequestClient) client);
                } catch (MalformedURLException mue1) {
-                       logger.log(Level.WARNING, String.format("Could not subscribe USK “%s”!", sone.getRequestUri()), mue1);
+                       logger.log(WARNING,
+                                       format("Could not subscribe USK “%s”!", requestUri),
+                                       mue1);
                }
        }
 
@@ -269,7 +259,7 @@ public class FreenetInterface {
                USKCallback uskCallback = new USKCallback() {
 
                        @Override
-                       public void onFoundEdition(long edition, USK key, ObjectContainer objectContainer, ClientContext clientContext, boolean metadata, short codec, byte[] data, boolean newKnownGood, boolean newSlotToo) {
+                       public void onFoundEdition(long edition, USK key, ClientContext clientContext, boolean metadata, short codec, byte[] data, boolean newKnownGood, boolean newSlotToo) {
                                callback.editionFound(key.getURI(), edition, newKnownGood, newSlotToo);
                        }
 
@@ -285,7 +275,7 @@ public class FreenetInterface {
 
                };
                try {
-                       node.clientCore.uskManager.subscribe(USK.create(uri), uskCallback, true, (HighLevelSimpleClientImpl) client);
+                       node.clientCore.uskManager.subscribe(USK.create(uri), uskCallback, true, (RequestClient) client);
                        uriUskCallbacks.put(uri, uskCallback);
                } catch (MalformedURLException mue1) {
                        logger.log(Level.WARNING, String.format("Could not subscribe to USK: %s", uri), mue1);
@@ -401,6 +391,7 @@ public class FreenetInterface {
 
                /** The client putter. */
                private ClientPutter clientPutter;
+               private Bucket bucket;
 
                /** The final URI. */
                private volatile FreenetURI resultingUri;
@@ -432,6 +423,10 @@ public class FreenetInterface {
                        eventBus.post(new ImageInsertStartedEvent(image));
                }
 
+               public void setBucket(Bucket bucket) {
+                       this.bucket = bucket;
+               }
+
                //
                // ACTIONS
                //
@@ -441,20 +436,23 @@ public class FreenetInterface {
                 */
                @SuppressWarnings("synthetic-access")
                public void cancel() {
-                       clientPutter.cancel(null, node.clientCore.clientContext);
+                       clientPutter.cancel(node.clientCore.clientContext);
                        eventBus.post(new ImageInsertAbortedEvent(image));
+                       bucket.free();
                }
 
                //
                // INTERFACE ClientPutCallback
                //
 
-               /**
-                * {@inheritDoc}
-                */
                @Override
-               public void onMajorProgress(ObjectContainer objectContainer) {
-                       /* ignore, we don’t care. */
+               public RequestClient getRequestClient() {
+                       return imageInserts;
+               }
+
+               @Override
+               public void onResume(ClientContext context) throws ResumeFailedException {
+                       /* ignore. */
                }
 
                /**
@@ -462,19 +460,20 @@ public class FreenetInterface {
                 */
                @Override
                @SuppressWarnings("synthetic-access")
-               public void onFailure(InsertException insertException, BaseClientPutter clientPutter, ObjectContainer objectContainer) {
+               public void onFailure(InsertException insertException, BaseClientPutter clientPutter) {
                        if ((insertException != null) && ("Cancelled by user".equals(insertException.getMessage()))) {
                                eventBus.post(new ImageInsertAbortedEvent(image));
                        } else {
                                eventBus.post(new ImageInsertFailedEvent(image, insertException));
                        }
+                       bucket.free();
                }
 
                /**
                 * {@inheritDoc}
                 */
                @Override
-               public void onFetchable(BaseClientPutter clientPutter, ObjectContainer objectContainer) {
+               public void onFetchable(BaseClientPutter clientPutter) {
                        /* ignore, we don’t care. */
                }
 
@@ -482,7 +481,7 @@ public class FreenetInterface {
                 * {@inheritDoc}
                 */
                @Override
-               public void onGeneratedMetadata(Bucket metadata, BaseClientPutter clientPutter, ObjectContainer objectContainer) {
+               public void onGeneratedMetadata(Bucket metadata, BaseClientPutter clientPutter) {
                        /* ignore, we don’t care. */
                }
 
@@ -490,7 +489,7 @@ public class FreenetInterface {
                 * {@inheritDoc}
                 */
                @Override
-               public void onGeneratedURI(FreenetURI generatedUri, BaseClientPutter clientPutter, ObjectContainer objectContainer) {
+               public void onGeneratedURI(FreenetURI generatedUri, BaseClientPutter clientPutter) {
                        resultingUri = generatedUri;
                }
 
@@ -499,8 +498,18 @@ public class FreenetInterface {
                 */
                @Override
                @SuppressWarnings("synthetic-access")
-               public void onSuccess(BaseClientPutter clientPutter, ObjectContainer objectContainer) {
+               public void onSuccess(BaseClientPutter clientPutter) {
                        eventBus.post(new ImageInsertFinishedEvent(image, resultingUri));
+                       bucket.free();
+               }
+
+       }
+
+       public class InsertTokenSupplier implements Function<Image, InsertToken> {
+
+               @Override
+               public InsertToken apply(Image image) {
+                       return new InsertToken(image);
                }
 
        }
index 791663f..25362a6 100644 (file)
@@ -19,6 +19,7 @@ package net.pterodactylus.sone.core;
 
 import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkNotNull;
+import static java.util.logging.Logger.getLogger;
 
 import java.util.Collections;
 import java.util.HashMap;
@@ -29,7 +30,8 @@ import java.util.logging.Logger;
 import net.pterodactylus.sone.core.FreenetInterface.InsertToken;
 import net.pterodactylus.sone.data.Image;
 import net.pterodactylus.sone.data.TemporaryImage;
-import net.pterodactylus.util.logging.Logging;
+
+import com.google.common.base.Function;
 
 /**
  * The image inserter is responsible for inserting images using
@@ -42,10 +44,11 @@ import net.pterodactylus.util.logging.Logging;
 public class ImageInserter {
 
        /** The logger. */
-       private static final Logger logger = Logging.getLogger(ImageInserter.class);
+       private static final Logger logger = getLogger("Sone.Image.Inserter");
 
        /** The freenet interface. */
        private final FreenetInterface freenetInterface;
+       private final Function<Image, InsertToken> insertTokenSupplier;
 
        /** The tokens of running inserts. */
        private final Map<String, InsertToken> insertTokens = Collections.synchronizedMap(new HashMap<String, InsertToken>());
@@ -55,9 +58,12 @@ public class ImageInserter {
         *
         * @param freenetInterface
         *            The freenet interface
+        * @param insertTokenSupplier
+        *            The supplier for insert tokens
         */
-       public ImageInserter(FreenetInterface freenetInterface) {
+       public ImageInserter(FreenetInterface freenetInterface, Function<Image, InsertToken> insertTokenSupplier) {
                this.freenetInterface = freenetInterface;
+               this.insertTokenSupplier = insertTokenSupplier;
        }
 
        /**
@@ -73,7 +79,7 @@ public class ImageInserter {
                checkNotNull(image, "image must not be null");
                checkArgument(image.getId().equals(temporaryImage.getId()), "image IDs must match");
                try {
-                       InsertToken insertToken = freenetInterface.new InsertToken(image);
+                       InsertToken insertToken = insertTokenSupplier.apply(image);
                        insertTokens.put(image.getId(), insertToken);
                        freenetInterface.insertImage(temporaryImage, image, insertToken);
                } catch (SoneException se1) {
index c8e3589..9e79fca 100644 (file)
@@ -21,6 +21,8 @@ import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
 
+import net.pterodactylus.sone.utils.Option;
+
 import com.google.common.base.Predicate;
 
 /**
@@ -30,211 +32,6 @@ import com.google.common.base.Predicate;
  */
 public class Options {
 
-       /**
-        * Contains current and default value of an option.
-        *
-        * @param <T>
-        *            The type of the option
-        * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
-        */
-       public static interface Option<T> {
-
-               /**
-                * Returns the default value of the option.
-                *
-                * @return The default value of the option
-                */
-               public T getDefault();
-
-               /**
-                * Returns the current value of the option. If the current value is not
-                * set (usually {@code null}), the default value is returned.
-                *
-                * @return The current value of the option
-                */
-               public T get();
-
-               /**
-                * Returns the real value of the option. This will also return an unset
-                * value (usually {@code null})!
-                *
-                * @return The real value of the option
-                */
-               public T getReal();
-
-               /**
-                * Validates the given value. Note that {@code null} is always a valid
-                * value!
-                *
-                * @param value
-                *            The value to validate
-                * @return {@code true} if this option does not have a validator, or the
-                *         validator validates this object, {@code false} otherwise
-                */
-               public boolean validate(T value);
-
-               /**
-                * Sets the current value of the option.
-                *
-                * @param value
-                *            The new value of the option
-                * @throws IllegalArgumentException
-                *             if the value is not valid for this option
-                */
-               public void set(T value) throws IllegalArgumentException;
-
-       }
-
-       /**
-        * Interface for objects that want to be notified when an option changes its
-        * value.
-        *
-        * @param <T>
-        *            The type of the option
-        * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
-        */
-       public static interface OptionWatcher<T> {
-
-               /**
-                * Notifies an object that an option has been changed.
-                *
-                * @param option
-                *            The option that has changed
-                * @param oldValue
-                *            The old value of the option
-                * @param newValue
-                *            The new value of the option
-                */
-               public void optionChanged(Option<T> option, T oldValue, T newValue);
-
-       }
-
-       /**
-        * Basic implementation of an {@link Option} that notifies an
-        * {@link OptionWatcher} if the value changes.
-        *
-        * @param <T>
-        *            The type of the option
-        * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
-        */
-       public static 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;
-
-               /** The option watcher. */
-               private final OptionWatcher<T> optionWatcher;
-
-               /**
-                * Creates a new default option.
-                *
-                * @param defaultValue
-                *            The default value of the option
-                */
-               public DefaultOption(T defaultValue) {
-                       this(defaultValue, (OptionWatcher<T>) 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, validator, null);
-               }
-
-               /**
-                * Creates a new default option.
-                *
-                * @param defaultValue
-                *            The default value of the option
-                * @param optionWatchers
-                *            The option watchers (may be {@code null})
-                */
-               public DefaultOption(T defaultValue, OptionWatcher<T> optionWatchers) {
-                       this(defaultValue, null, optionWatchers);
-               }
-
-               /**
-                * Creates a new default option.
-                *
-                * @param defaultValue
-                *            The default value of the option
-                * @param validator
-                *            The validator for value validation (may be {@code null})
-                * @param optionWatcher
-                *            The option watcher (may be {@code null})
-                */
-               public DefaultOption(T defaultValue, Predicate<T> validator, OptionWatcher<T> optionWatcher) {
-                       this.defaultValue = defaultValue;
-                       this.validator = validator;
-                       this.optionWatcher = optionWatcher;
-               }
-
-               /**
-                * {@inheritDoc}
-                */
-               @Override
-               public T getDefault() {
-                       return defaultValue;
-               }
-
-               /**
-                * {@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.");
-                       }
-                       T oldValue = this.value;
-                       this.value = value;
-                       if (!get().equals(oldValue)) {
-                               if (optionWatcher != null) {
-                                       optionWatcher.optionChanged(this, oldValue, get());
-                               }
-                       }
-               }
-
-       }
-
        /** Holds all {@link Boolean} {@link Option}s. */
        private final Map<String, Option<Boolean>> booleanOptions = Collections.synchronizedMap(new HashMap<String, Option<Boolean>>());
 
index 16d9453..56bfa73 100644 (file)
 
 package net.pterodactylus.sone.core;
 
+import static com.google.common.base.Predicates.equalTo;
+import static java.lang.Integer.MAX_VALUE;
+import static net.pterodactylus.sone.fcp.FcpInterface.FullAccessRequired.ALWAYS;
+import static net.pterodactylus.sone.utils.IntegerRangePredicate.range;
+
+import net.pterodactylus.sone.core.event.InsertionDelayChangedEvent;
 import net.pterodactylus.sone.fcp.FcpInterface;
 import net.pterodactylus.sone.fcp.FcpInterface.FullAccessRequired;
+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.sone.utils.Option;
+import net.pterodactylus.util.config.Configuration;
+import net.pterodactylus.util.config.ConfigurationException;
+
+import com.google.common.base.Predicates;
+import com.google.common.eventbus.EventBus;
 
 /**
  * Convenience interface for external classes that want to access the core’s
@@ -28,17 +44,33 @@ import net.pterodactylus.sone.fcp.FcpInterface.FullAccessRequired;
  */
 public class Preferences {
 
-       /** The wrapped options. */
-       private final Options options;
-
-       /**
-        * Creates a new preferences object wrapped around the given options.
-        *
-        * @param options
-        *            The options to wrap
-        */
-       public Preferences(Options options) {
-               this.options = options;
+       private final EventBus eventBus;
+       private final Option<Integer> insertionDelay =
+                       new DefaultOption<Integer>(60, range(0, MAX_VALUE));
+       private final Option<Integer> postsPerPage =
+                       new DefaultOption<Integer>(10, range(1, MAX_VALUE));
+       private final Option<Integer> imagesPerPage =
+                       new DefaultOption<Integer>(9, range(1, MAX_VALUE));
+       private final Option<Integer> charactersPerPost =
+                       new DefaultOption<Integer>(400, Predicates.<Integer>or(
+                                       range(50, MAX_VALUE), equalTo(-1)));
+       private final Option<Integer> postCutOffLength =
+                       new DefaultOption<Integer>(200, range(50, MAX_VALUE));
+       private final Option<Boolean> requireFullAccess =
+                       new DefaultOption<Boolean>(false);
+       private final Option<Integer> positiveTrust =
+                       new DefaultOption<Integer>(75, range(0, 100));
+       private final Option<Integer> negativeTrust =
+                       new DefaultOption<Integer>(-25, range(-100, 100));
+       private final Option<String> trustComment =
+                       new DefaultOption<String>("Set from Sone Web Interface");
+       private final Option<Boolean> activateFcpInterface =
+                       new DefaultOption<Boolean>(false);
+       private final Option<FullAccessRequired> fcpFullAccessRequired =
+                       new DefaultOption<FullAccessRequired>(ALWAYS);
+
+       public Preferences(EventBus eventBus) {
+               this.eventBus = eventBus;
        }
 
        /**
@@ -47,7 +79,7 @@ public class Preferences {
         * @return The insertion delay
         */
        public int getInsertionDelay() {
-               return options.getIntegerOption("InsertionDelay").get();
+               return insertionDelay.get();
        }
 
        /**
@@ -59,7 +91,7 @@ public class Preferences {
         *         {@code false} otherwise
         */
        public boolean validateInsertionDelay(Integer insertionDelay) {
-               return options.getIntegerOption("InsertionDelay").validate(insertionDelay);
+               return this.insertionDelay.validate(insertionDelay);
        }
 
        /**
@@ -71,7 +103,8 @@ public class Preferences {
         * @return This preferences
         */
        public Preferences setInsertionDelay(Integer insertionDelay) {
-               options.getIntegerOption("InsertionDelay").set(insertionDelay);
+               this.insertionDelay.set(insertionDelay);
+               eventBus.post(new InsertionDelayChangedEvent(getInsertionDelay()));
                return this;
        }
 
@@ -81,7 +114,7 @@ public class Preferences {
         * @return The number of posts to show per page
         */
        public int getPostsPerPage() {
-               return options.getIntegerOption("PostsPerPage").get();
+               return postsPerPage.get();
        }
 
        /**
@@ -93,7 +126,7 @@ public class Preferences {
         *         {@code false} otherwise
         */
        public boolean validatePostsPerPage(Integer postsPerPage) {
-               return options.getIntegerOption("PostsPerPage").validate(postsPerPage);
+               return this.postsPerPage.validate(postsPerPage);
        }
 
        /**
@@ -104,7 +137,7 @@ public class Preferences {
         * @return This preferences object
         */
        public Preferences setPostsPerPage(Integer postsPerPage) {
-               options.getIntegerOption("PostsPerPage").set(postsPerPage);
+               this.postsPerPage.set(postsPerPage);
                return this;
        }
 
@@ -114,7 +147,7 @@ public class Preferences {
         * @return The number of images to show per page
         */
        public int getImagesPerPage() {
-               return options.getIntegerOption("ImagesPerPage").get();
+               return imagesPerPage.get();
        }
 
        /**
@@ -126,7 +159,7 @@ public class Preferences {
         *         {@code false} otherwise
         */
        public boolean validateImagesPerPage(Integer imagesPerPage) {
-               return options.getIntegerOption("ImagesPerPage").validate(imagesPerPage);
+               return this.imagesPerPage.validate(imagesPerPage);
        }
 
        /**
@@ -137,7 +170,7 @@ public class Preferences {
         * @return This preferences object
         */
        public Preferences setImagesPerPage(Integer imagesPerPage) {
-               options.getIntegerOption("ImagesPerPage").set(imagesPerPage);
+               this.imagesPerPage.set(imagesPerPage);
                return this;
        }
 
@@ -148,7 +181,7 @@ public class Preferences {
         * @return The numbers of characters per post
         */
        public int getCharactersPerPost() {
-               return options.getIntegerOption("CharactersPerPost").get();
+               return charactersPerPost.get();
        }
 
        /**
@@ -160,7 +193,7 @@ public class Preferences {
         *         {@code false} otherwise
         */
        public boolean validateCharactersPerPost(Integer charactersPerPost) {
-               return options.getIntegerOption("CharactersPerPost").validate(charactersPerPost);
+               return this.charactersPerPost.validate(charactersPerPost);
        }
 
        /**
@@ -172,7 +205,7 @@ public class Preferences {
         * @return This preferences objects
         */
        public Preferences setCharactersPerPost(Integer charactersPerPost) {
-               options.getIntegerOption("CharactersPerPost").set(charactersPerPost);
+               this.charactersPerPost.set(charactersPerPost);
                return this;
        }
 
@@ -182,7 +215,7 @@ public class Preferences {
         * @return The number of characters of the snippet
         */
        public int getPostCutOffLength() {
-               return options.getIntegerOption("PostCutOffLength").get();
+               return postCutOffLength.get();
        }
 
        /**
@@ -194,7 +227,7 @@ public class Preferences {
         *         valid, {@code false} otherwise
         */
        public boolean validatePostCutOffLength(Integer postCutOffLength) {
-               return options.getIntegerOption("PostCutOffLength").validate(postCutOffLength);
+               return this.postCutOffLength.validate(postCutOffLength);
        }
 
        /**
@@ -205,7 +238,7 @@ public class Preferences {
         * @return This preferences
         */
        public Preferences setPostCutOffLength(Integer postCutOffLength) {
-               options.getIntegerOption("PostCutOffLength").set(postCutOffLength);
+               this.postCutOffLength.set(postCutOffLength);
                return this;
        }
 
@@ -216,7 +249,7 @@ public class Preferences {
         *         otherwise
         */
        public boolean isRequireFullAccess() {
-               return options.getBooleanOption("RequireFullAccess").get();
+               return requireFullAccess.get();
        }
 
        /**
@@ -227,7 +260,7 @@ public class Preferences {
         *            otherwise
         */
        public void setRequireFullAccess(Boolean requireFullAccess) {
-               options.getBooleanOption("RequireFullAccess").set(requireFullAccess);
+               this.requireFullAccess.set(requireFullAccess);
        }
 
        /**
@@ -236,7 +269,7 @@ public class Preferences {
         * @return The positive trust
         */
        public int getPositiveTrust() {
-               return options.getIntegerOption("PositiveTrust").get();
+               return positiveTrust.get();
        }
 
        /**
@@ -248,7 +281,7 @@ public class Preferences {
         *         otherwise
         */
        public boolean validatePositiveTrust(Integer positiveTrust) {
-               return options.getIntegerOption("PositiveTrust").validate(positiveTrust);
+               return this.positiveTrust.validate(positiveTrust);
        }
 
        /**
@@ -260,7 +293,7 @@ public class Preferences {
         * @return This preferences
         */
        public Preferences setPositiveTrust(Integer positiveTrust) {
-               options.getIntegerOption("PositiveTrust").set(positiveTrust);
+               this.positiveTrust.set(positiveTrust);
                return this;
        }
 
@@ -270,7 +303,7 @@ public class Preferences {
         * @return The negative trust
         */
        public int getNegativeTrust() {
-               return options.getIntegerOption("NegativeTrust").get();
+               return negativeTrust.get();
        }
 
        /**
@@ -282,7 +315,7 @@ public class Preferences {
         *         otherwise
         */
        public boolean validateNegativeTrust(Integer negativeTrust) {
-               return options.getIntegerOption("NegativeTrust").validate(negativeTrust);
+               return this.negativeTrust.validate(negativeTrust);
        }
 
        /**
@@ -294,7 +327,7 @@ public class Preferences {
         * @return The preferences
         */
        public Preferences setNegativeTrust(Integer negativeTrust) {
-               options.getIntegerOption("NegativeTrust").set(negativeTrust);
+               this.negativeTrust.set(negativeTrust);
                return this;
        }
 
@@ -305,7 +338,7 @@ public class Preferences {
         * @return The trust comment
         */
        public String getTrustComment() {
-               return options.getStringOption("TrustComment").get();
+               return trustComment.get();
        }
 
        /**
@@ -317,7 +350,7 @@ public class Preferences {
         * @return This preferences
         */
        public Preferences setTrustComment(String trustComment) {
-               options.getStringOption("TrustComment").set(trustComment);
+               this.trustComment.set(trustComment);
                return this;
        }
 
@@ -330,7 +363,7 @@ public class Preferences {
         *         {@code false} otherwise
         */
        public boolean isFcpInterfaceActive() {
-               return options.getBooleanOption("ActivateFcpInterface").get();
+               return activateFcpInterface.get();
        }
 
        /**
@@ -343,8 +376,13 @@ public class Preferences {
         *            to deactivate the FCP interface
         * @return This preferences object
         */
-       public Preferences setFcpInterfaceActive(boolean fcpInterfaceActive) {
-               options.getBooleanOption("ActivateFcpInterface").set(fcpInterfaceActive);
+       public Preferences setFcpInterfaceActive(Boolean fcpInterfaceActive) {
+               this.activateFcpInterface.set(fcpInterfaceActive);
+               if (isFcpInterfaceActive()) {
+                       eventBus.post(new FcpInterfaceActivatedEvent());
+               } else {
+                       eventBus.post(new FcpInterfaceDeactivatedEvent());
+               }
                return this;
        }
 
@@ -356,7 +394,7 @@ public class Preferences {
         *         is required
         */
        public FullAccessRequired getFcpFullAccessRequired() {
-               return FullAccessRequired.values()[options.getIntegerOption("FcpFullAccessRequired").get()];
+               return fcpFullAccessRequired.get();
        }
 
        /**
@@ -367,9 +405,30 @@ public class Preferences {
         *            The action level
         * @return This preferences
         */
-       public Preferences setFcpFullAccessRequired(FullAccessRequired fcpFullAccessRequired) {
-               options.getIntegerOption("FcpFullAccessRequired").set((fcpFullAccessRequired != null) ? fcpFullAccessRequired.ordinal() : null);
+       public Preferences setFcpFullAccessRequired(
+                       FullAccessRequired fcpFullAccessRequired) {
+               this.fcpFullAccessRequired.set(fcpFullAccessRequired);
+               eventBus.post(new FullAccessRequiredChanged(getFcpFullAccessRequired()));
                return this;
        }
 
+       public void saveTo(Configuration configuration) throws ConfigurationException {
+               configuration.getIntValue("Option/ConfigurationVersion").setValue(0);
+               configuration.getIntValue("Option/InsertionDelay").setValue(insertionDelay.getReal());
+               configuration.getIntValue("Option/PostsPerPage").setValue(postsPerPage.getReal());
+               configuration.getIntValue("Option/ImagesPerPage").setValue(imagesPerPage.getReal());
+               configuration.getIntValue("Option/CharactersPerPost").setValue(charactersPerPost.getReal());
+               configuration.getIntValue("Option/PostCutOffLength").setValue(postCutOffLength.getReal());
+               configuration.getBooleanValue("Option/RequireFullAccess").setValue(requireFullAccess.getReal());
+               configuration.getIntValue("Option/PositiveTrust").setValue(positiveTrust.getReal());
+               configuration.getIntValue("Option/NegativeTrust").setValue(negativeTrust.getReal());
+               configuration.getStringValue("Option/TrustComment").setValue(trustComment.getReal());
+               configuration.getBooleanValue("Option/ActivateFcpInterface").setValue(activateFcpInterface.getReal());
+               configuration.getIntValue("Option/FcpFullAccessRequired").setValue(toInt(fcpFullAccessRequired.getReal()));
+       }
+
+       private Integer toInt(FullAccessRequired fullAccessRequired) {
+               return (fullAccessRequired == null) ? null : fullAccessRequired.ordinal();
+       }
+
 }
diff --git a/src/main/java/net/pterodactylus/sone/core/PreferencesLoader.java b/src/main/java/net/pterodactylus/sone/core/PreferencesLoader.java
new file mode 100644 (file)
index 0000000..0ae13ba
--- /dev/null
@@ -0,0 +1,105 @@
+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.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+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.setInsertionDelay(configuration.getIntValue(
+                               "Option/InsertionDelay").getValue(null));
+       }
+
+       private void loadPostsPerPage(Configuration configuration) {
+               preferences.setPostsPerPage(
+                               configuration.getIntValue("Option/PostsPerPage")
+                                               .getValue(null));
+       }
+
+       private void loadImagesPerPage(Configuration configuration) {
+               preferences.setImagesPerPage(
+                               configuration.getIntValue("Option/ImagesPerPage")
+                                               .getValue(null));
+       }
+
+       private void loadCharactersPerPost(Configuration configuration) {
+               preferences.setCharactersPerPost(
+                               configuration.getIntValue("Option/CharactersPerPost")
+                                               .getValue(null));
+       }
+
+       private void loadPostCutOffLength(Configuration configuration) {
+               try {
+                       preferences.setPostCutOffLength(
+                                       configuration.getIntValue("Option/PostCutOffLength")
+                                                       .getValue(null));
+               } catch (IllegalArgumentException iae1) {
+                       /* previous versions allowed -1, ignore and use default. */
+               }
+       }
+
+       private void loadRequireFullAccess(Configuration configuration) {
+               preferences.setRequireFullAccess(
+                               configuration.getBooleanValue("Option/RequireFullAccess")
+                                               .getValue(null));
+       }
+
+       private void loadPositiveTrust(Configuration configuration) {
+               preferences.setPositiveTrust(
+                               configuration.getIntValue("Option/PositiveTrust")
+                                               .getValue(null));
+       }
+
+       private void loadNegativeTrust(Configuration configuration) {
+               preferences.setNegativeTrust(
+                               configuration.getIntValue("Option/NegativeTrust")
+                                               .getValue(null));
+       }
+
+       private void loadTrustComment(Configuration configuration) {
+               preferences.setTrustComment(
+                               configuration.getStringValue("Option/TrustComment")
+                                               .getValue(null));
+       }
+
+       private void loadFcpInterfaceActive(Configuration configuration) {
+               preferences.setFcpInterfaceActive(configuration.getBooleanValue(
+                               "Option/ActivateFcpInterface").getValue(null));
+       }
+
+       private void loadFcpFullAccessRequired(Configuration configuration) {
+               Integer fullAccessRequiredInteger = configuration
+                               .getIntValue("Option/FcpFullAccessRequired").getValue(null);
+               preferences.setFcpFullAccessRequired(
+                               (fullAccessRequiredInteger == null) ? null :
+                                               FullAccessRequired.values()[fullAccessRequiredInteger]);
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/sone/core/SoneChangeDetector.java b/src/main/java/net/pterodactylus/sone/core/SoneChangeDetector.java
new file mode 100644 (file)
index 0000000..efcdc6e
--- /dev/null
@@ -0,0 +1,114 @@
+package net.pterodactylus.sone.core;
+
+import static com.google.common.base.Optional.absent;
+import static com.google.common.base.Optional.fromNullable;
+import static com.google.common.collect.FluentIterable.from;
+
+import java.util.Collection;
+
+import net.pterodactylus.sone.data.Post;
+import net.pterodactylus.sone.data.PostReply;
+import net.pterodactylus.sone.data.Sone;
+
+import com.google.common.base.Optional;
+import com.google.common.base.Predicate;
+import com.google.common.collect.FluentIterable;
+
+/**
+ * Compares the contents of two {@link Sone}s and fires events for new and
+ * removed elements.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class SoneChangeDetector {
+
+       private final Sone oldSone;
+       private Optional<PostProcessor> newPostProcessor = absent();
+       private Optional<PostProcessor> removedPostProcessor = absent();
+       private Optional<PostReplyProcessor> newPostReplyProcessor = absent();
+       private Optional<PostReplyProcessor> removedPostReplyProcessor = absent();
+
+       public SoneChangeDetector(Sone oldSone) {
+               this.oldSone = oldSone;
+       }
+
+       public void onNewPosts(PostProcessor newPostProcessor) {
+               this.newPostProcessor = fromNullable(newPostProcessor);
+       }
+
+       public void onRemovedPosts(PostProcessor removedPostProcessor) {
+               this.removedPostProcessor = fromNullable(removedPostProcessor);
+       }
+
+       public void onNewPostReplies(PostReplyProcessor newPostReplyProcessor) {
+               this.newPostReplyProcessor = fromNullable(newPostReplyProcessor);
+       }
+
+       public void onRemovedPostReplies(
+                       PostReplyProcessor removedPostReplyProcessor) {
+               this.removedPostReplyProcessor = fromNullable(removedPostReplyProcessor);
+       }
+
+       public void detectChanges(Sone newSone) {
+               processPosts(from(newSone.getPosts()).filter(
+                               notContainedIn(oldSone.getPosts())), newPostProcessor);
+               processPosts(from(oldSone.getPosts()).filter(
+                               notContainedIn(newSone.getPosts())), removedPostProcessor);
+               processPostReplies(from(newSone.getReplies()).filter(
+                               notContainedIn(oldSone.getReplies())), newPostReplyProcessor);
+               processPostReplies(from(oldSone.getReplies()).filter(
+                               notContainedIn(newSone.getReplies())), removedPostReplyProcessor);
+       }
+
+       private void processPostReplies(FluentIterable<PostReply> postReplies,
+                       Optional<PostReplyProcessor> postReplyProcessor) {
+               for (PostReply postReply : postReplies) {
+                       notifyPostReplyProcessor(postReplyProcessor, postReply);
+               }
+       }
+
+       private void notifyPostReplyProcessor(
+                       Optional<PostReplyProcessor> postReplyProcessor,
+                       PostReply postReply) {
+               if (postReplyProcessor.isPresent()) {
+                       postReplyProcessor.get()
+                                       .processPostReply(postReply);
+               }
+       }
+
+       private void processPosts(FluentIterable<Post> posts,
+                       Optional<PostProcessor> newPostProcessor) {
+               for (Post post : posts) {
+                       notifyPostProcessor(newPostProcessor, post);
+               }
+       }
+
+       private void notifyPostProcessor(Optional<PostProcessor> postProcessor,
+                       Post newPost) {
+               if (postProcessor.isPresent()) {
+                       postProcessor.get().processPost(newPost);
+               }
+       }
+
+       private <T> Predicate<T> notContainedIn(final Collection<T> posts) {
+               return new Predicate<T>() {
+                       @Override
+                       public boolean apply(T element) {
+                               return !posts.contains(element);
+                       }
+               };
+       }
+
+       public interface PostProcessor {
+
+               void processPost(Post post);
+
+       }
+
+       public interface PostReplyProcessor {
+
+               void processPostReply(PostReply postReply);
+
+       }
+
+}
index 53eef16..be0be02 100644 (file)
-/*
- * Sone - SoneDownloader.java - Copyright © 2010–2013 David Roden
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General 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 java.io.InputStream;
-import java.net.MalformedURLException;
-import java.util.ArrayList;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-
-import net.pterodactylus.sone.core.FreenetInterface.Fetched;
-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.Sone;
-import net.pterodactylus.sone.data.Sone.SoneStatus;
-import net.pterodactylus.sone.data.SoneImpl;
-import net.pterodactylus.sone.database.PostBuilder;
-import net.pterodactylus.sone.database.PostReplyBuilder;
-import net.pterodactylus.util.io.Closer;
-import net.pterodactylus.util.logging.Logging;
-import net.pterodactylus.util.number.Numbers;
-import net.pterodactylus.util.service.AbstractService;
-import net.pterodactylus.util.xml.SimpleXML;
-import net.pterodactylus.util.xml.XML;
+import net.pterodactylus.util.service.Service;
 
-import org.w3c.dom.Document;
-
-import freenet.client.FetchResult;
 import freenet.keys.FreenetURI;
-import freenet.support.api.Bucket;
 
 /**
- * The Sone downloader is responsible for download Sones as they are updated.
+ * Downloads and parses Sone and {@link Core#updateSone(Sone) updates the
+ * core}.
  *
  * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
  */
-public class SoneDownloader extends AbstractService {
-
-       /** The logger. */
-       private static final Logger logger = Logging.getLogger(SoneDownloader.class);
-
-       /** The maximum protocol version. */
-       private static final int MAX_PROTOCOL_VERSION = 0;
-
-       /** The core. */
-       private final Core core;
-
-       /** The Freenet interface. */
-       private final FreenetInterface freenetInterface;
-
-       /** The sones to update. */
-       private final Set<Sone> sones = new HashSet<Sone>();
-
-       /**
-        * Creates a new Sone downloader.
-        *
-        * @param core
-        *            The core
-        * @param freenetInterface
-        *            The Freenet interface
-        */
-       public SoneDownloader(Core core, FreenetInterface freenetInterface) {
-               super("Sone Downloader", false);
-               this.core = core;
-               this.freenetInterface = freenetInterface;
-       }
-
-       //
-       // ACTIONS
-       //
-
-       /**
-        * Adds the given Sone to the set of Sones that will be watched for updates.
-        *
-        * @param sone
-        *            The Sone to add
-        */
-       public void addSone(Sone sone) {
-               if (!sones.add(sone)) {
-                       freenetInterface.unregisterUsk(sone);
-               }
-               freenetInterface.registerUsk(sone, this);
-       }
-
-       /**
-        * Removes the given Sone from the downloader.
-        *
-        * @param sone
-        *            The Sone to stop watching
-        */
-       public void removeSone(Sone sone) {
-               if (sones.remove(sone)) {
-                       freenetInterface.unregisterUsk(sone);
-               }
-       }
-
-       /**
-        * Fetches the updated Sone. This method is a callback method for
-        * {@link FreenetInterface#registerUsk(Sone, SoneDownloader)}.
-        *
-        * @param sone
-        *            The Sone to fetch
-        */
-       public void fetchSone(Sone sone) {
-               fetchSone(sone, sone.getRequestUri().sskForUSK());
-       }
-
-       /**
-        * Fetches the updated Sone. This method can be used to fetch a Sone from a
-        * specific URI.
-        *
-        * @param sone
-        *            The Sone to fetch
-        * @param soneUri
-        *            The URI to fetch the Sone from
-        */
-       public void fetchSone(Sone sone, FreenetURI soneUri) {
-               fetchSone(sone, soneUri, false);
-       }
-
-       /**
-        * Fetches the Sone from the given URI.
-        *
-        * @param sone
-        *            The Sone to fetch
-        * @param soneUri
-        *            The URI of the Sone to fetch
-        * @param fetchOnly
-        *            {@code true} to only fetch and parse the Sone, {@code false}
-        *            to {@link Core#updateSone(Sone) update} it in the core
-        * @return The downloaded Sone, or {@code null} if the Sone could not be
-        *         downloaded
-        */
-       public Sone fetchSone(Sone sone, FreenetURI soneUri, boolean fetchOnly) {
-               logger.log(Level.FINE, String.format("Starting fetch for Sone “%s” from %s…", sone, soneUri));
-               FreenetURI requestUri = soneUri.setMetaString(new String[] { "sone.xml" });
-               sone.setStatus(SoneStatus.downloading);
-               try {
-                       Fetched fetchResults = freenetInterface.fetchUri(requestUri);
-                       if (fetchResults == null) {
-                               /* TODO - mark Sone as bad. */
-                               return null;
-                       }
-                       logger.log(Level.FINEST, String.format("Got %d bytes back.", fetchResults.getFetchResult().size()));
-                       Sone parsedSone = parseSone(sone, fetchResults.getFetchResult(), fetchResults.getFreenetUri());
-                       if (parsedSone != null) {
-                               if (!fetchOnly) {
-                                       parsedSone.setStatus((parsedSone.getTime() == 0) ? SoneStatus.unknown : SoneStatus.idle);
-                                       core.updateSone(parsedSone);
-                                       addSone(parsedSone);
-                               }
-                       }
-                       return parsedSone;
-               } finally {
-                       sone.setStatus((sone.getTime() == 0) ? SoneStatus.unknown : SoneStatus.idle);
-               }
-       }
-
-       /**
-        * Parses a Sone from a fetch result.
-        *
-        * @param originalSone
-        *            The sone to parse, or {@code null} if the Sone is yet unknown
-        * @param fetchResult
-        *            The fetch result
-        * @param requestUri
-        *            The requested URI
-        * @return The parsed Sone, or {@code null} if the Sone could not be parsed
-        */
-       public 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));
-               Bucket soneBucket = fetchResult.asBucket();
-               InputStream soneInputStream = null;
-               try {
-                       soneInputStream = soneBucket.getInputStream();
-                       Sone parsedSone = parseSone(originalSone, soneInputStream);
-                       if (parsedSone != null) {
-                               parsedSone.setLatestEdition(requestUri.getEdition());
-                               if (requestUri.getKeyType().equals("USK")) {
-                                       parsedSone.setRequestUri(requestUri.setMetaString(new String[0]));
-                               } else {
-                                       parsedSone.setRequestUri(requestUri.setKeyType("USK").setDocName("Sone").setMetaString(new String[0]));
-                               }
-                       }
-                       return parsedSone;
-               } catch (Exception e1) {
-                       logger.log(Level.WARNING, String.format("Could not parse Sone from %s!", requestUri), e1);
-               } finally {
-                       Closer.close(soneInputStream);
-                       soneBucket.free();
-               }
-               return null;
-       }
-
-       /**
-        * Parses a Sone from the given input stream and creates a new Sone from the
-        * parsed data.
-        *
-        * @param originalSone
-        *            The Sone to update
-        * @param soneInputStream
-        *            The input stream to parse the Sone from
-        * @return The parsed Sone
-        * @throws SoneException
-        *             if a parse error occurs, or the protocol is invalid
-        */
-       public Sone parseSone(Sone originalSone, InputStream soneInputStream) throws SoneException {
-               /* TODO - impose a size limit? */
-
-               Document document;
-               /* XML parsing is not thread-safe. */
-               synchronized (this) {
-                       document = XML.transformToDocument(soneInputStream);
-               }
-               if (document == null) {
-                       /* TODO - mark Sone as bad. */
-                       logger.log(Level.WARNING, String.format("Could not parse XML for Sone %s!", originalSone));
-                       return null;
-               }
-
-               Sone sone = new SoneImpl(originalSone.getId(), originalSone.isLocal()).setIdentity(originalSone.getIdentity());
-
-               SimpleXML soneXml;
-               try {
-                       soneXml = SimpleXML.fromDocument(document);
-               } catch (NullPointerException npe1) {
-                       /* for some reason, invalid XML can cause NPEs. */
-                       logger.log(Level.WARNING, String.format("XML for Sone %s can not be parsed!", sone), npe1);
-                       return null;
-               }
-
-               Integer protocolVersion = null;
-               String soneProtocolVersion = soneXml.getValue("protocol-version", null);
-               if (soneProtocolVersion != null) {
-                       protocolVersion = Numbers.safeParseInteger(soneProtocolVersion);
-               }
-               if (protocolVersion == null) {
-                       logger.log(Level.INFO, "No protocol version found, assuming 0.");
-                       protocolVersion = 0;
-               }
-
-               if (protocolVersion < 0) {
-                       logger.log(Level.WARNING, String.format("Invalid protocol version: %d! Not parsing Sone.", protocolVersion));
-                       return null;
-               }
-
-               /* check for valid versions. */
-               if (protocolVersion > MAX_PROTOCOL_VERSION) {
-                       logger.log(Level.WARNING, String.format("Unknown protocol version: %d! Not parsing Sone.", protocolVersion));
-                       return null;
-               }
-
-               String soneTime = soneXml.getValue("time", null);
-               if (soneTime == null) {
-                       /* TODO - mark Sone as bad. */
-                       logger.log(Level.WARNING, String.format("Downloaded time for Sone %s was null!", sone));
-                       return null;
-               }
-               try {
-                       sone.setTime(Long.parseLong(soneTime));
-               } catch (NumberFormatException nfe1) {
-                       /* TODO - mark Sone as bad. */
-                       logger.log(Level.WARNING, String.format("Downloaded Sone %s with invalid time: %s", sone, soneTime));
-                       return null;
-               }
-
-               SimpleXML clientXml = soneXml.getNode("client");
-               if (clientXml != null) {
-                       String clientName = clientXml.getValue("name", null);
-                       String clientVersion = clientXml.getValue("version", null);
-                       if ((clientName == null) || (clientVersion == null)) {
-                               logger.log(Level.WARNING, String.format("Download Sone %s with client XML but missing name or version!", sone));
-                               return null;
-                       }
-                       sone.setClient(new Client(clientName, clientVersion));
-               }
-
-               String soneRequestUri = soneXml.getValue("request-uri", null);
-               if (soneRequestUri != null) {
-                       try {
-                               sone.setRequestUri(new FreenetURI(soneRequestUri));
-                       } catch (MalformedURLException mue1) {
-                               /* TODO - mark Sone as bad. */
-                               logger.log(Level.WARNING, String.format("Downloaded Sone %s has invalid request URI: %s", sone, soneRequestUri), mue1);
-                               return null;
-                       }
-               }
-
-               if (originalSone.getInsertUri() != null) {
-                       sone.setInsertUri(originalSone.getInsertUri());
-               }
-
-               SimpleXML profileXml = soneXml.getNode("profile");
-               if (profileXml == null) {
-                       /* TODO - mark Sone as bad. */
-                       logger.log(Level.WARNING, String.format("Downloaded Sone %s has no profile!", sone));
-                       return null;
-               }
-
-               /* parse profile. */
-               String profileFirstName = profileXml.getValue("first-name", null);
-               String profileMiddleName = profileXml.getValue("middle-name", null);
-               String profileLastName = profileXml.getValue("last-name", null);
-               Integer profileBirthDay = Numbers.safeParseInteger(profileXml.getValue("birth-day", null));
-               Integer profileBirthMonth = Numbers.safeParseInteger(profileXml.getValue("birth-month", null));
-               Integer profileBirthYear = Numbers.safeParseInteger(profileXml.getValue("birth-year", null));
-               Profile profile = new Profile(sone).setFirstName(profileFirstName).setMiddleName(profileMiddleName).setLastName(profileLastName);
-               profile.setBirthDay(profileBirthDay).setBirthMonth(profileBirthMonth).setBirthYear(profileBirthYear);
-               /* avatar is processed after images are loaded. */
-               String avatarId = profileXml.getValue("avatar", null);
-
-               /* parse profile fields. */
-               SimpleXML profileFieldsXml = profileXml.getNode("fields");
-               if (profileFieldsXml != null) {
-                       for (SimpleXML fieldXml : profileFieldsXml.getNodes("field")) {
-                               String fieldName = fieldXml.getValue("field-name", null);
-                               String fieldValue = fieldXml.getValue("field-value", "");
-                               if (fieldName == null) {
-                                       logger.log(Level.WARNING, String.format("Downloaded profile field for Sone %s with missing data! Name: %s, Value: %s", sone, fieldName, fieldValue));
-                                       return null;
-                               }
-                               try {
-                                       profile.addField(fieldName).setValue(fieldValue);
-                               } catch (IllegalArgumentException iae1) {
-                                       logger.log(Level.WARNING, String.format("Duplicate field: %s", fieldName), iae1);
-                                       return null;
-                               }
-                       }
-               }
-
-               /* parse posts. */
-               SimpleXML postsXml = soneXml.getNode("posts");
-               Set<Post> posts = new HashSet<Post>();
-               if (postsXml == null) {
-                       /* TODO - mark Sone as bad. */
-                       logger.log(Level.WARNING, String.format("Downloaded Sone %s has no posts!", sone));
-               } else {
-                       for (SimpleXML postXml : postsXml.getNodes("post")) {
-                               String postId = postXml.getValue("id", null);
-                               String postRecipientId = postXml.getValue("recipient", null);
-                               String postTime = postXml.getValue("time", null);
-                               String postText = postXml.getValue("text", null);
-                               if ((postId == null) || (postTime == null) || (postText == null)) {
-                                       /* TODO - mark Sone as bad. */
-                                       logger.log(Level.WARNING, String.format("Downloaded post for Sone %s with missing data! ID: %s, Time: %s, Text: %s", sone, postId, postTime, postText));
-                                       return null;
-                               }
-                               try {
-                                       PostBuilder postBuilder = core.postBuilder();
-                                       /* TODO - parse time correctly. */
-                                       postBuilder.withId(postId).from(sone.getId()).withTime(Long.parseLong(postTime)).withText(postText);
-                                       if ((postRecipientId != null) && (postRecipientId.length() == 43)) {
-                                               postBuilder.to(postRecipientId);
-                                       }
-                                       posts.add(postBuilder.build());
-                               } catch (NumberFormatException nfe1) {
-                                       /* TODO - mark Sone as bad. */
-                                       logger.log(Level.WARNING, String.format("Downloaded post for Sone %s with invalid time: %s", sone, postTime));
-                                       return null;
-                               }
-                       }
-               }
-
-               /* parse replies. */
-               SimpleXML repliesXml = soneXml.getNode("replies");
-               Set<PostReply> replies = new HashSet<PostReply>();
-               if (repliesXml == null) {
-                       /* TODO - mark Sone as bad. */
-                       logger.log(Level.WARNING, String.format("Downloaded Sone %s has no replies!", sone));
-               } else {
-                       for (SimpleXML replyXml : repliesXml.getNodes("reply")) {
-                               String replyId = replyXml.getValue("id", null);
-                               String replyPostId = replyXml.getValue("post-id", null);
-                               String replyTime = replyXml.getValue("time", null);
-                               String replyText = replyXml.getValue("text", null);
-                               if ((replyId == null) || (replyPostId == null) || (replyTime == null) || (replyText == null)) {
-                                       /* TODO - mark Sone as bad. */
-                                       logger.log(Level.WARNING, String.format("Downloaded reply for Sone %s with missing data! ID: %s, Post: %s, Time: %s, Text: %s", sone, replyId, replyPostId, replyTime, replyText));
-                                       return null;
-                               }
-                               try {
-                                       PostReplyBuilder postReplyBuilder = core.postReplyBuilder();
-                                       /* TODO - parse time correctly. */
-                                       postReplyBuilder.withId(replyId).from(sone.getId()).to(replyPostId).withTime(Long.parseLong(replyTime)).withText(replyText);
-                                       replies.add(postReplyBuilder.build());
-                               } catch (NumberFormatException nfe1) {
-                                       /* TODO - mark Sone as bad. */
-                                       logger.log(Level.WARNING, String.format("Downloaded reply for Sone %s with invalid time: %s", sone, replyTime));
-                                       return null;
-                               }
-                       }
-               }
-
-               /* parse liked post IDs. */
-               SimpleXML likePostIdsXml = soneXml.getNode("post-likes");
-               Set<String> likedPostIds = new HashSet<String>();
-               if (likePostIdsXml == null) {
-                       /* TODO - mark Sone as bad. */
-                       logger.log(Level.WARNING, String.format("Downloaded Sone %s has no post likes!", sone));
-               } else {
-                       for (SimpleXML likedPostIdXml : likePostIdsXml.getNodes("post-like")) {
-                               String postId = likedPostIdXml.getValue();
-                               likedPostIds.add(postId);
-                       }
-               }
-
-               /* parse liked reply IDs. */
-               SimpleXML likeReplyIdsXml = soneXml.getNode("reply-likes");
-               Set<String> likedReplyIds = new HashSet<String>();
-               if (likeReplyIdsXml == null) {
-                       /* TODO - mark Sone as bad. */
-                       logger.log(Level.WARNING, String.format("Downloaded Sone %s has no reply likes!", sone));
-               } else {
-                       for (SimpleXML likedReplyIdXml : likeReplyIdsXml.getNodes("reply-like")) {
-                               String replyId = likedReplyIdXml.getValue();
-                               likedReplyIds.add(replyId);
-                       }
-               }
-
-               /* parse albums. */
-               SimpleXML albumsXml = soneXml.getNode("albums");
-               List<Album> topLevelAlbums = new ArrayList<Album>();
-               if (albumsXml != null) {
-                       for (SimpleXML albumXml : albumsXml.getNodes("album")) {
-                               String id = albumXml.getValue("id", null);
-                               String parentId = albumXml.getValue("parent", null);
-                               String title = albumXml.getValue("title", null);
-                               String description = albumXml.getValue("description", "");
-                               String albumImageId = albumXml.getValue("album-image", null);
-                               if ((id == null) || (title == null) || (description == null)) {
-                                       logger.log(Level.WARNING, String.format("Downloaded Sone %s contains invalid album!", sone));
-                                       return null;
-                               }
-                               Album parent = null;
-                               if (parentId != null) {
-                                       parent = core.getAlbum(parentId, false);
-                                       if (parent == null) {
-                                               logger.log(Level.WARNING, String.format("Downloaded Sone %s has album with invalid parent!", sone));
-                                               return null;
-                                       }
-                               }
-                               Album album = core.getAlbum(id).setSone(sone).modify().setTitle(title).setDescription(description).update();
-                               if (parent != null) {
-                                       parent.addAlbum(album);
-                               } else {
-                                       topLevelAlbums.add(album);
-                               }
-                               SimpleXML imagesXml = albumXml.getNode("images");
-                               if (imagesXml != null) {
-                                       for (SimpleXML imageXml : imagesXml.getNodes("image")) {
-                                               String imageId = imageXml.getValue("id", null);
-                                               String imageCreationTimeString = imageXml.getValue("creation-time", null);
-                                               String imageKey = imageXml.getValue("key", null);
-                                               String imageTitle = imageXml.getValue("title", null);
-                                               String imageDescription = imageXml.getValue("description", "");
-                                               String imageWidthString = imageXml.getValue("width", null);
-                                               String imageHeightString = imageXml.getValue("height", null);
-                                               if ((imageId == null) || (imageCreationTimeString == null) || (imageKey == null) || (imageTitle == null) || (imageWidthString == null) || (imageHeightString == null)) {
-                                                       logger.log(Level.WARNING, String.format("Downloaded Sone %s contains invalid images!", sone));
-                                                       return null;
-                                               }
-                                               long creationTime = Numbers.safeParseLong(imageCreationTimeString, 0L);
-                                               int imageWidth = Numbers.safeParseInteger(imageWidthString, 0);
-                                               int imageHeight = Numbers.safeParseInteger(imageHeightString, 0);
-                                               if ((imageWidth < 1) || (imageHeight < 1)) {
-                                                       logger.log(Level.WARNING, String.format("Downloaded Sone %s contains image %s with invalid dimensions (%s, %s)!", sone, imageId, imageWidthString, imageHeightString));
-                                                       return null;
-                                               }
-                                               Image image = core.getImage(imageId).modify().setSone(sone).setKey(imageKey).setCreationTime(creationTime).update();
-                                               image = image.modify().setTitle(imageTitle).setDescription(imageDescription).update();
-                                               image = image.modify().setWidth(imageWidth).setHeight(imageHeight).update();
-                                               album.addImage(image);
-                                       }
-                               }
-                               album.modify().setAlbumImage(albumImageId).update();
-                       }
-               }
-
-               /* process avatar. */
-               if (avatarId != null) {
-                       profile.setAvatar(core.getImage(avatarId, false));
-               }
-
-               /* okay, apparently everything was parsed correctly. Now import. */
-               /* atomic setter operation on the Sone. */
-               synchronized (sone) {
-                       sone.setProfile(profile);
-                       sone.setPosts(posts);
-                       sone.setReplies(replies);
-                       sone.setLikePostIds(likedPostIds);
-                       sone.setLikeReplyIds(likedReplyIds);
-                       for (Album album : topLevelAlbums) {
-                               sone.getRootAlbum().addAlbum(album);
-                       }
-               }
-
-               return sone;
-       }
+public interface SoneDownloader extends Service {
 
-       //
-       // SERVICE METHODS
-       //
+       void addSone(Sone sone);
+       void fetchSone(Sone sone, FreenetURI soneUri);
+       Sone fetchSone(Sone sone, FreenetURI soneUri, boolean fetchOnly);
 
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       protected void serviceStop() {
-               for (Sone sone : sones) {
-                       freenetInterface.unregisterUsk(sone);
-               }
-       }
+       Runnable fetchSoneWithUriAction(Sone sone);
+       Runnable fetchSoneAction(Sone sone);
 
 }
diff --git a/src/main/java/net/pterodactylus/sone/core/SoneDownloaderImpl.java b/src/main/java/net/pterodactylus/sone/core/SoneDownloaderImpl.java
new file mode 100644 (file)
index 0000000..b36c2d3
--- /dev/null
@@ -0,0 +1,274 @@
+/*
+ * Sone - SoneDownloader.java - Copyright © 2010–2013 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General 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 freenet.support.io.Closer.close;
+import static java.lang.String.format;
+import static java.lang.System.currentTimeMillis;
+import static java.util.concurrent.TimeUnit.DAYS;
+import static java.util.logging.Logger.getLogger;
+
+import java.io.InputStream;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import net.pterodactylus.sone.core.FreenetInterface.Fetched;
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.data.Sone.SoneStatus;
+import net.pterodactylus.util.service.AbstractService;
+
+import freenet.client.FetchResult;
+import freenet.client.async.ClientContext;
+import freenet.client.async.USKCallback;
+import freenet.keys.FreenetURI;
+import freenet.keys.USK;
+import freenet.node.RequestStarter;
+import freenet.support.api.Bucket;
+import freenet.support.io.Closer;
+import com.db4o.ObjectContainer;
+
+import com.google.common.annotations.VisibleForTesting;
+
+/**
+ * The Sone downloader is responsible for download Sones as they are updated.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class SoneDownloaderImpl extends AbstractService implements SoneDownloader {
+
+       /** The logger. */
+       private static final Logger logger = getLogger("Sone.Downloader");
+
+       /** The maximum protocol version. */
+       private static final int MAX_PROTOCOL_VERSION = 0;
+
+       /** The core. */
+       private final Core core;
+       private final SoneParser soneParser;
+
+       /** The Freenet interface. */
+       private final FreenetInterface freenetInterface;
+
+       /** The sones to update. */
+       private final Set<Sone> sones = new HashSet<Sone>();
+
+       /**
+        * Creates a new Sone downloader.
+        *
+        * @param core
+        *              The core
+        * @param freenetInterface
+        *              The Freenet interface
+        */
+       public SoneDownloaderImpl(Core core, FreenetInterface freenetInterface) {
+               this(core, freenetInterface, new SoneParser(core));
+       }
+
+       /**
+        * Creates a new Sone downloader.
+        *
+        * @param core
+        *              The core
+        * @param freenetInterface
+        *              The Freenet interface
+        * @param soneParser
+        */
+       @VisibleForTesting
+       SoneDownloaderImpl(Core core, FreenetInterface freenetInterface, SoneParser soneParser) {
+               super("Sone Downloader", false);
+               this.core = core;
+               this.freenetInterface = freenetInterface;
+               this.soneParser = soneParser;
+       }
+
+       //
+       // ACTIONS
+       //
+
+       /**
+        * Adds the given Sone to the set of Sones that will be watched for updates.
+        *
+        * @param sone
+        *              The Sone to add
+        */
+       @Override
+       public void addSone(final Sone sone) {
+               if (!sones.add(sone)) {
+                       freenetInterface.unregisterUsk(sone);
+               }
+               final USKCallback uskCallback = new USKCallback() {
+
+                       @Override
+                       @SuppressWarnings("synthetic-access")
+                       public void onFoundEdition(long edition, USK key,
+                                       ClientContext clientContext, boolean metadata,
+                                       short codec, byte[] data, boolean newKnownGood,
+                                       boolean newSlotToo) {
+                               logger.log(Level.FINE, format(
+                                               "Found USK update for Sone “%s” at %s, new known good: %s, new slot too: %s.",
+                                               sone, key, newKnownGood, newSlotToo));
+                               if (edition > sone.getLatestEdition()) {
+                                       sone.setLatestEdition(edition);
+                                       new Thread(fetchSoneAction(sone),
+                                                       "Sone Downloader").start();
+                               }
+                       }
+
+                       @Override
+                       public short getPollingPriorityProgress() {
+                               return RequestStarter.INTERACTIVE_PRIORITY_CLASS;
+                       }
+
+                       @Override
+                       public short getPollingPriorityNormal() {
+                               return RequestStarter.INTERACTIVE_PRIORITY_CLASS;
+                       }
+               };
+               if (soneHasBeenActiveRecently(sone)) {
+                       freenetInterface.registerActiveUsk(sone.getRequestUri(),
+                                       uskCallback);
+               } else {
+                       freenetInterface.registerPassiveUsk(sone.getRequestUri(),
+                                       uskCallback);
+               }
+       }
+
+       private boolean soneHasBeenActiveRecently(Sone sone) {
+               return (currentTimeMillis() - sone.getTime()) < DAYS.toMillis(7);
+       }
+
+       private void fetchSone(Sone sone) {
+               fetchSone(sone, sone.getRequestUri().sskForUSK());
+       }
+
+       /**
+        * Fetches the updated Sone. This method can be used to fetch a Sone from a
+        * specific URI.
+        *
+        * @param sone
+        *              The Sone to fetch
+        * @param soneUri
+        *              The URI to fetch the Sone from
+        */
+       @Override
+       public void fetchSone(Sone sone, FreenetURI soneUri) {
+               fetchSone(sone, soneUri, false);
+       }
+
+       /**
+        * Fetches the Sone from the given URI.
+        *
+        * @param sone
+        *              The Sone to fetch
+        * @param soneUri
+        *              The URI of the Sone to fetch
+        * @param fetchOnly
+        *              {@code true} to only fetch and parse the Sone, {@code false}
+        *              to {@link Core#updateSone(Sone) update} it in the core
+        * @return The downloaded Sone, or {@code null} if the Sone could not be
+        *         downloaded
+        */
+       @Override
+       public Sone fetchSone(Sone sone, FreenetURI soneUri, boolean fetchOnly) {
+               logger.log(Level.FINE, String.format("Starting fetch for Sone “%s” from %s…", sone, soneUri));
+               FreenetURI requestUri = soneUri.setMetaString(new String[] { "sone.xml" });
+               sone.setStatus(SoneStatus.downloading);
+               try {
+                       Fetched fetchResults = freenetInterface.fetchUri(requestUri);
+                       if (fetchResults == null) {
+                               /* TODO - mark Sone as bad. */
+                               return null;
+                       }
+                       logger.log(Level.FINEST, String.format("Got %d bytes back.", fetchResults.getFetchResult().size()));
+                       Sone parsedSone = parseSone(sone, fetchResults.getFetchResult(), fetchResults.getFreenetUri());
+                       if (parsedSone != null) {
+                               if (!fetchOnly) {
+                                       parsedSone.setStatus((parsedSone.getTime() == 0) ? SoneStatus.unknown : SoneStatus.idle);
+                                       core.updateSone(parsedSone);
+                                       addSone(parsedSone);
+                               }
+                       }
+                       return parsedSone;
+               } finally {
+                       sone.setStatus((sone.getTime() == 0) ? SoneStatus.unknown : SoneStatus.idle);
+               }
+       }
+
+       /**
+        * Parses a Sone from a fetch result.
+        *
+        * @param originalSone
+        *              The sone to parse, or {@code null} if the Sone is yet unknown
+        * @param fetchResult
+        *              The fetch result
+        * @param requestUri
+        *              The requested URI
+        * @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));
+               Bucket soneBucket = fetchResult.asBucket();
+               InputStream soneInputStream = null;
+               try {
+                       soneInputStream = soneBucket.getInputStream();
+                       Sone parsedSone = soneParser.parseSone(originalSone,
+                                       soneInputStream);
+                       if (parsedSone != null) {
+                               parsedSone.setLatestEdition(requestUri.getEdition());
+                       }
+                       return parsedSone;
+               } catch (Exception e1) {
+                       logger.log(Level.WARNING, String.format("Could not parse Sone from %s!", requestUri), e1);
+               } finally {
+                       close(soneInputStream);
+                       close(soneBucket);
+               }
+               return null;
+       }
+
+       @Override
+       public Runnable fetchSoneWithUriAction(final Sone sone) {
+               return new Runnable() {
+                       @Override
+                       public void run() {
+                               fetchSone(sone, sone.getRequestUri());
+                       }
+               };
+       }
+
+       @Override
+       public Runnable fetchSoneAction(final Sone sone) {
+               return new Runnable() {
+                       @Override
+                       public void run() {
+                               fetchSone(sone);
+                       }
+               };
+       }
+
+       /** {@inheritDoc} */
+       @Override
+       protected void serviceStop() {
+               for (Sone sone : sones) {
+                       freenetInterface.unregisterUsk(sone);
+               }
+       }
+
+}
index f1af354..2b1f1de 100644 (file)
@@ -26,23 +26,6 @@ public class SoneException extends Exception {
 
        /**
         * Creates a new Sone exception.
-        */
-       public SoneException() {
-               super();
-       }
-
-       /**
-        * Creates a new Sone exception.
-        *
-        * @param message
-        *            The message of the exception
-        */
-       public SoneException(String message) {
-               super(message);
-       }
-
-       /**
-        * Creates a new Sone exception.
         *
         * @param cause
         *            The cause of the exception
index 5c3f7cd..f67f093 100644 (file)
@@ -26,33 +26,6 @@ public class SoneInsertException extends SoneException {
 
        /**
         * Creates a new Sone insert exception.
-        */
-       public SoneInsertException() {
-               super();
-       }
-
-       /**
-        * Creates a new Sone insert exception.
-        *
-        * @param message
-        *            The message of the exception
-        */
-       public SoneInsertException(String message) {
-               super(message);
-       }
-
-       /**
-        * Creates a new Sone insert exception.
-        *
-        * @param cause
-        *            The cause of the exception
-        */
-       public SoneInsertException(Throwable cause) {
-               super(cause);
-       }
-
-       /**
-        * Creates a new Sone insert exception.
         *
         * @param message
         *            The message of the exception
index a1bb903..86bf049 100644 (file)
 
 package net.pterodactylus.sone.core;
 
-import static com.google.common.base.Preconditions.checkArgument;
+import static java.lang.String.format;
+import static java.lang.System.currentTimeMillis;
+import static java.util.logging.Logger.getLogger;
 import static net.pterodactylus.sone.data.Album.NOT_EMPTY;
 
+import java.io.Closeable;
+import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.io.StringWriter;
 import java.nio.charset.Charset;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 
+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;
@@ -37,10 +45,8 @@ import net.pterodactylus.sone.data.Post;
 import net.pterodactylus.sone.data.Reply;
 import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.data.Sone.SoneStatus;
-import net.pterodactylus.sone.freenet.StringBucket;
 import net.pterodactylus.sone.main.SonePlugin;
 import net.pterodactylus.util.io.Closer;
-import net.pterodactylus.util.logging.Logging;
 import net.pterodactylus.util.service.AbstractService;
 import net.pterodactylus.util.template.HtmlFilter;
 import net.pterodactylus.util.template.ReflectionAccessor;
@@ -51,12 +57,19 @@ import net.pterodactylus.util.template.TemplateException;
 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.base.Optional;
 import com.google.common.collect.FluentIterable;
 import com.google.common.collect.Ordering;
 import com.google.common.eventbus.EventBus;
+import com.google.common.eventbus.Subscribe;
 
-import freenet.client.async.ManifestElement;
 import freenet.keys.FreenetURI;
+import freenet.support.api.Bucket;
+import freenet.support.api.ManifestElement;
+import freenet.support.api.RandomAccessBucket;
+import freenet.support.io.ArrayBucket;
 
 /**
  * A Sone inserter is responsible for inserting a Sone if it has changed.
@@ -66,10 +79,10 @@ import freenet.keys.FreenetURI;
 public class SoneInserter extends AbstractService {
 
        /** The logger. */
-       private static final Logger logger = Logging.getLogger(SoneInserter.class);
+       private static final Logger logger = getLogger("Sone.Inserter");
 
        /** The insertion delay (in seconds). */
-       private static volatile int insertionDelay = 60;
+       private static final AtomicInteger insertionDelay = new AtomicInteger(60);
 
        /** The template factory used to create the templates. */
        private static final TemplateContextFactory templateContextFactory = new TemplateContextFactory();
@@ -92,14 +105,9 @@ public class SoneInserter extends AbstractService {
        /** The Freenet interface. */
        private final FreenetInterface freenetInterface;
 
-       /** The Sone to insert. */
-       private volatile Sone sone;
-
-       /** Whether a modification has been detected. */
-       private volatile boolean modified = false;
-
-       /** The fingerprint of the last insert. */
-       private volatile String lastInsertFingerprint;
+       private final SoneModificationDetector soneModificationDetector;
+       private final long delay;
+       private final String soneId;
 
        /**
         * Creates a new Sone inserter.
@@ -110,32 +118,49 @@ public class SoneInserter extends AbstractService {
         *            The event bus
         * @param freenetInterface
         *            The freenet interface
-        * @param sone
-        *            The Sone to insert
+        * @param soneId
+        *            The ID of the Sone to insert
         */
-       public SoneInserter(Core core, EventBus eventBus, FreenetInterface freenetInterface, Sone sone) {
-               super("Sone Inserter for “" + sone.getName() + "”", false);
+       public SoneInserter(final Core core, EventBus eventBus, FreenetInterface freenetInterface, final String soneId) {
+               this(core, eventBus, freenetInterface, soneId, new SoneModificationDetector(new LockableFingerprintProvider() {
+                       @Override
+                       public boolean isLocked() {
+                               final Optional<Sone> sone = core.getSone(soneId);
+                               if (!sone.isPresent()) {
+                                       return false;
+                               }
+                               return core.isLocked(sone.get());
+                       }
+
+                       @Override
+                       public String getFingerprint() {
+                               final Optional<Sone> sone = core.getSone(soneId);
+                               if (!sone.isPresent()) {
+                                       return null;
+                               }
+                               return sone.get().getFingerprint();
+                       }
+               }, insertionDelay), 1000);
+       }
+
+       @VisibleForTesting
+       SoneInserter(Core core, EventBus eventBus, FreenetInterface freenetInterface, String soneId, SoneModificationDetector soneModificationDetector, long delay) {
+               super("Sone Inserter for “" + soneId + "”", false);
                this.core = core;
                this.eventBus = eventBus;
                this.freenetInterface = freenetInterface;
-               this.sone = sone;
+               this.soneId = soneId;
+               this.soneModificationDetector = soneModificationDetector;
+               this.delay = delay;
        }
 
        //
        // ACCESSORS
        //
 
-       /**
-        * Sets the Sone to insert.
-        *
-        * @param sone
-        *              The Sone to insert
-        * @return This Sone inserter
-        */
-       public SoneInserter setSone(Sone sone) {
-               checkArgument((this.sone == null) || sone.equals(this.sone), "Sone to insert can not be set to a different Sone");
-               this.sone = sone;
-               return this;
+       @VisibleForTesting
+       static AtomicInteger getInsertionDelay() {
+               return insertionDelay;
        }
 
        /**
@@ -145,8 +170,8 @@ public class SoneInserter extends AbstractService {
         * @param insertionDelay
         *            The insertion delay (in seconds)
         */
-       public static void setInsertionDelay(int insertionDelay) {
-               SoneInserter.insertionDelay = insertionDelay;
+       private static void setInsertionDelay(int insertionDelay) {
+               SoneInserter.insertionDelay.set(insertionDelay);
        }
 
        /**
@@ -155,7 +180,7 @@ public class SoneInserter extends AbstractService {
         * @return The fingerprint of the last insert
         */
        public String getLastInsertFingerprint() {
-               return lastInsertFingerprint;
+               return soneModificationDetector.getOriginalFingerprint();
        }
 
        /**
@@ -165,7 +190,7 @@ public class SoneInserter extends AbstractService {
         *            The fingerprint of the last insert
         */
        public void setLastInsertFingerprint(String lastInsertFingerprint) {
-               this.lastInsertFingerprint = lastInsertFingerprint;
+               soneModificationDetector.setFingerprint(lastInsertFingerprint);
        }
 
        /**
@@ -176,7 +201,7 @@ public class SoneInserter extends AbstractService {
         *         otherwise
         */
        public boolean isModified() {
-               return modified;
+               return soneModificationDetector.isModified();
        }
 
        //
@@ -188,59 +213,28 @@ public class SoneInserter extends AbstractService {
         */
        @Override
        protected void serviceRun() {
-               long lastModificationTime = 0;
-               String lastInsertedFingerprint = lastInsertFingerprint;
-               String lastFingerprint = "";
-               Sone sone;
                while (!shouldStop()) {
                        try {
-                               /* check every seconds. */
-                               sleep(1000);
-
-                               /* don’t insert locked Sones. */
-                               sone = this.sone;
-                               if (core.isLocked(sone)) {
-                                       /* trigger redetection when the Sone is unlocked. */
-                                       synchronized (sone) {
-                                               modified = !sone.getFingerprint().equals(lastInsertedFingerprint);
+                               /* check every second. */
+                               sleep(delay);
+
+                               if (soneModificationDetector.isEligibleForInsert()) {
+                                       Optional<Sone> soneOptional = core.getSone(soneId);
+                                       if (!soneOptional.isPresent()) {
+                                               logger.log(Level.WARNING, format("Sone %s has disappeared, exiting inserter.", soneId));
+                                               return;
                                        }
-                                       lastFingerprint = "";
-                                       lastModificationTime = 0;
-                                       continue;
-                               }
-
-                               InsertInformation insertInformation = null;
-                               synchronized (sone) {
-                                       String fingerprint = sone.getFingerprint();
-                                       if (!fingerprint.equals(lastFingerprint)) {
-                                               if (fingerprint.equals(lastInsertedFingerprint)) {
-                                                       modified = false;
-                                                       lastModificationTime = 0;
-                                                       logger.log(Level.FINE, String.format("Sone %s has been reverted to last insert state.", sone));
-                                               } else {
-                                                       lastModificationTime = System.currentTimeMillis();
-                                                       modified = true;
-                                                       logger.log(Level.FINE, String.format("Sone %s has been modified, waiting %d seconds before inserting.", sone.getName(), insertionDelay));
-                                               }
-                                               lastFingerprint = fingerprint;
-                                       }
-                                       if (modified && (lastModificationTime > 0) && ((System.currentTimeMillis() - lastModificationTime) > (insertionDelay * 1000))) {
-                                               lastInsertedFingerprint = fingerprint;
-                                               insertInformation = new InsertInformation(sone);
-                                       }
-                               }
-
-                               if (insertInformation != null) {
+                                       Sone sone = soneOptional.get();
+                                       InsertInformation insertInformation = new InsertInformation(sone);
                                        logger.log(Level.INFO, String.format("Inserting Sone “%s”…", sone.getName()));
 
                                        boolean success = false;
                                        try {
                                                sone.setStatus(SoneStatus.inserting);
-                                               long insertTime = System.currentTimeMillis();
-                                               insertInformation.setTime(insertTime);
+                                               long insertTime = currentTimeMillis();
                                                eventBus.post(new SoneInsertingEvent(sone));
-                                               FreenetURI finalUri = freenetInterface.insertDirectory(insertInformation.getInsertUri(), insertInformation.generateManifestEntries(), "index.html");
-                                               eventBus.post(new SoneInsertedEvent(sone, System.currentTimeMillis() - insertTime));
+                                               FreenetURI finalUri = freenetInterface.insertDirectory(sone.getInsertUri(), insertInformation.generateManifestEntries(), "index.html");
+                                               eventBus.post(new SoneInsertedEvent(sone, currentTimeMillis() - insertTime, insertInformation.getFingerprint()));
                                                /* at this point we might already be stopped. */
                                                if (shouldStop()) {
                                                        /* if so, bail out, don’t change anything. */
@@ -255,6 +249,7 @@ public class SoneInserter extends AbstractService {
                                                eventBus.post(new SoneInsertAbortedEvent(sone, se1));
                                                logger.log(Level.WARNING, String.format("Could not insert Sone “%s”!", sone.getName()), se1);
                                        } finally {
+                                               insertInformation.close();
                                                sone.setStatus(SoneStatus.idle);
                                        }
 
@@ -264,12 +259,10 @@ public class SoneInserter extends AbstractService {
                                         */
                                        if (success) {
                                                synchronized (sone) {
-                                                       if (lastInsertedFingerprint.equals(sone.getFingerprint())) {
+                                                       if (insertInformation.getFingerprint().equals(sone.getFingerprint())) {
                                                                logger.log(Level.FINE, String.format("Sone “%s” was not modified further, resetting counter…", sone));
-                                                               lastModificationTime = 0;
-                                                               lastInsertFingerprint = lastInsertedFingerprint;
+                                                               soneModificationDetector.setFingerprint(insertInformation.getFingerprint());
                                                                core.touchConfiguration();
-                                                               modified = false;
                                                        }
                                                }
                                        }
@@ -280,6 +273,11 @@ public class SoneInserter extends AbstractService {
                }
        }
 
+       @Subscribe
+       public void insertionDelayChanged(InsertionDelayChangedEvent insertionDelayChangedEvent) {
+               setInsertionDelay(insertionDelayChangedEvent.getInsertionDelay());
+       }
+
        /**
         * Container for information that are required to insert a Sone. This
         * container merely exists to copy all relevant data without holding a lock
@@ -287,10 +285,13 @@ public class SoneInserter extends AbstractService {
         *
         * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
         */
-       private class InsertInformation {
+       @VisibleForTesting
+       class InsertInformation implements Closeable {
 
                /** All properties of the Sone, copied for thread safety. */
                private final Map<String, Object> soneProperties = new HashMap<String, Object>();
+               private final String fingerprint;
+               private final ManifestCreator manifestCreator;
 
                /**
                 * Creates a new insert information container.
@@ -299,40 +300,28 @@ public class SoneInserter extends AbstractService {
                 *            The sone to insert
                 */
                public InsertInformation(Sone sone) {
+                       this.fingerprint = sone.getFingerprint();
+                       Map<String, Object> soneProperties = new HashMap<String, Object>();
                        soneProperties.put("id", sone.getId());
                        soneProperties.put("name", sone.getName());
-                       soneProperties.put("time", sone.getTime());
+                       soneProperties.put("time", currentTimeMillis());
                        soneProperties.put("requestUri", sone.getRequestUri());
-                       soneProperties.put("insertUri", sone.getInsertUri());
                        soneProperties.put("profile", sone.getProfile());
                        soneProperties.put("posts", Ordering.from(Post.TIME_COMPARATOR).sortedCopy(sone.getPosts()));
                        soneProperties.put("replies", Ordering.from(Reply.TIME_COMPARATOR).reverse().sortedCopy(sone.getReplies()));
                        soneProperties.put("likedPostIds", new HashSet<String>(sone.getLikedPostIds()));
                        soneProperties.put("likedReplyIds", new HashSet<String>(sone.getLikedReplyIds()));
                        soneProperties.put("albums", FluentIterable.from(sone.getRootAlbum().getAlbums()).transformAndConcat(Album.FLATTENER).filter(NOT_EMPTY).toList());
+                       manifestCreator = new ManifestCreator(core, soneProperties);
                }
 
                //
                // ACCESSORS
                //
 
-               /**
-                * Returns the insert URI of the Sone.
-                *
-                * @return The insert URI of the Sone
-                */
-               public FreenetURI getInsertUri() {
-                       return (FreenetURI) soneProperties.get("insertUri");
-               }
-
-               /**
-                * Sets the time of the Sone at the time of the insert.
-                *
-                * @param time
-                *            The time of the Sone
-                */
-               public void setTime(long time) {
-                       soneProperties.put("time", time);
+               @VisibleForTesting
+               String getFingerprint() {
+                       return fingerprint;
                }
 
                //
@@ -348,41 +337,56 @@ public class SoneInserter extends AbstractService {
                        HashMap<String, Object> manifestEntries = new HashMap<String, Object>();
 
                        /* first, create an index.html. */
-                       manifestEntries.put("index.html", createManifestElement("index.html", "text/html; charset=utf-8", "/templates/insert/index.html"));
+                       manifestEntries.put("index.html", manifestCreator.createManifestElement(
+                                       "index.html", "text/html; charset=utf-8",
+                                       "/templates/insert/index.html"));
 
                        /* now, store the sone. */
-                       manifestEntries.put("sone.xml", createManifestElement("sone.xml", "text/xml; charset=utf-8", "/templates/insert/sone.xml"));
+                       manifestEntries.put("sone.xml", manifestCreator.createManifestElement(
+                                       "sone.xml", "text/xml; charset=utf-8",
+                                       "/templates/insert/sone.xml"));
 
                        return manifestEntries;
                }
 
-               //
-               // PRIVATE METHODS
-               //
+               @Override
+               public void close() {
+                       manifestCreator.close();
+               }
 
-               /**
-                * Creates a new manifest element.
-                *
-                * @param name
-                *            The name of the file
-                * @param contentType
-                *            The content type of the file
-                * @param templateName
-                *            The name of the template to render
-                * @return The manifest element
-                */
-               @SuppressWarnings("synthetic-access")
-               private ManifestElement createManifestElement(String name, String contentType, String templateName) {
+       }
+
+       /**
+        * Creates manifest elements for an insert by rendering a template.
+        *
+        * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+        */
+       @VisibleForTesting
+       static class ManifestCreator implements Closeable {
+
+               private final Core core;
+               private final Map<String, Object> soneProperties;
+               private final Set<Bucket> buckets = new HashSet<Bucket>();
+
+               ManifestCreator(Core core, Map<String, Object> soneProperties) {
+                       this.core = core;
+                       this.soneProperties = soneProperties;
+               }
+
+               public ManifestElement createManifestElement(String name, String contentType, String templateName) {
                        InputStreamReader templateInputStreamReader = null;
+                       InputStream templateInputStream = null;
                        Template template;
                        try {
-                               templateInputStreamReader = new InputStreamReader(getClass().getResourceAsStream(templateName), utf8Charset);
+                               templateInputStream = getClass().getResourceAsStream(templateName);
+                               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);
                                return null;
                        } finally {
                                Closer.close(templateInputStreamReader);
+                               Closer.close(templateInputStream);
                        }
 
                        TemplateContext templateContext = templateContextFactory.createTemplateContext();
@@ -391,19 +395,22 @@ public class SoneInserter extends AbstractService {
                        templateContext.set("currentEdition", core.getUpdateChecker().getLatestEdition());
                        templateContext.set("version", SonePlugin.VERSION);
                        StringWriter writer = new StringWriter();
-                       StringBucket bucket = null;
                        try {
                                template.render(templateContext, writer);
-                               bucket = new StringBucket(writer.toString(), utf8Charset);
+                               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);
                                return null;
                        } finally {
                                Closer.close(writer);
-                               if (bucket != null) {
-                                       bucket.free();
-                               }
+                       }
+               }
+
+               public void close() {
+                       for (Bucket bucket : buckets) {
+                               bucket.free();
                        }
                }
 
diff --git a/src/main/java/net/pterodactylus/sone/core/SoneModificationDetector.java b/src/main/java/net/pterodactylus/sone/core/SoneModificationDetector.java
new file mode 100644 (file)
index 0000000..290fcbe
--- /dev/null
@@ -0,0 +1,95 @@
+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;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+import net.pterodactylus.sone.data.Sone;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Optional;
+import com.google.common.base.Ticker;
+
+/**
+ * Class that detects {@link Sone} modifications (as per their {@link
+ * Sone#getFingerprint() fingerprints} and determines when a modified Sone may
+ * be inserted.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+class SoneModificationDetector {
+
+       private final Ticker ticker;
+       private final LockableFingerprintProvider lockableFingerprintProvider;
+       private final AtomicInteger insertionDelay;
+       private Optional<Long> lastModificationTime;
+       private String originalFingerprint;
+       private String lastFingerprint;
+
+       SoneModificationDetector(LockableFingerprintProvider lockableFingerprintProvider, AtomicInteger insertionDelay) {
+               this(systemTicker(), lockableFingerprintProvider, insertionDelay);
+       }
+
+       @VisibleForTesting
+       SoneModificationDetector(Ticker ticker, LockableFingerprintProvider lockableFingerprintProvider, AtomicInteger insertionDelay) {
+               this.ticker = ticker;
+               this.lockableFingerprintProvider = lockableFingerprintProvider;
+               this.insertionDelay = insertionDelay;
+               lastFingerprint = originalFingerprint;
+       }
+
+       public boolean isEligibleForInsert() {
+               if (lockableFingerprintProvider.isLocked()) {
+                       lastModificationTime = absent();
+                       lastFingerprint = "";
+                       return false;
+               }
+               String fingerprint = lockableFingerprintProvider.getFingerprint();
+               if (originalFingerprint.equals(fingerprint)) {
+                       lastModificationTime = absent();
+                       lastFingerprint = fingerprint;
+                       return false;
+               }
+               if (!lastFingerprint.equals(fingerprint)) {
+                       lastModificationTime = of(ticker.read());
+                       lastFingerprint = fingerprint;
+                       return false;
+               }
+               return insertionDelayHasPassed();
+       }
+
+       public String getOriginalFingerprint() {
+               return originalFingerprint;
+       }
+
+       public void setFingerprint(String fingerprint) {
+               originalFingerprint = fingerprint;
+               lastFingerprint = originalFingerprint;
+               lastModificationTime = absent();
+       }
+
+       private boolean insertionDelayHasPassed() {
+               return NANOSECONDS.toSeconds(ticker.read() - lastModificationTime.get()) >= insertionDelay.get();
+       }
+
+       public boolean isModified() {
+               return !lockableFingerprintProvider.getFingerprint().equals(originalFingerprint);
+       }
+
+       /**
+        * Provider for a fingerprint and the information if a {@link Sone} is locked. This
+        * prevents us from having to lug a Sone object around.
+        *
+        * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+        */
+       static interface LockableFingerprintProvider {
+
+               boolean isLocked();
+               String getFingerprint();
+
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/sone/core/SoneParser.java b/src/main/java/net/pterodactylus/sone/core/SoneParser.java
new file mode 100644 (file)
index 0000000..ae600a6
--- /dev/null
@@ -0,0 +1,344 @@
+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 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 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.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 org.w3c.dom.Document;
+
+/**
+ * Parses a {@link Sone} from an XML {@link InputStream}.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class SoneParser {
+
+       private static final Logger logger = getLogger("Sone.Parser");
+       private static final int MAX_PROTOCOL_VERSION = 0;
+       private final Core core;
+
+       public SoneParser(Core core) {
+               this.core = core;
+       }
+
+       public Sone parseSone(Sone originalSone, InputStream soneInputStream) throws SoneException {
+               /* TODO - impose a size limit? */
+
+               Document document;
+               /* XML parsing is not thread-safe. */
+               synchronized (this) {
+                       document = XML.transformToDocument(soneInputStream);
+               }
+               if (document == null) {
+                       /* TODO - mark Sone as bad. */
+                       logger.log(Level.WARNING, String.format("Could not parse XML for Sone %s!", originalSone));
+                       return null;
+               }
+
+               SoneBuilder soneBuilder = core.soneBuilder().from(originalSone.getIdentity());
+               if (originalSone.isLocal()) {
+                       soneBuilder = soneBuilder.local();
+               }
+               Sone sone = soneBuilder.build();
+
+               SimpleXML soneXml;
+               try {
+                       soneXml = SimpleXML.fromDocument(document);
+               } catch (NullPointerException npe1) {
+                       /* for some reason, invalid XML can cause NPEs. */
+                       logger.log(Level.WARNING, String.format("XML for Sone %s can not be parsed!", sone), npe1);
+                       return null;
+               }
+
+               Integer protocolVersion = null;
+               String soneProtocolVersion = soneXml.getValue("protocol-version", null);
+               if (soneProtocolVersion != null) {
+                       protocolVersion = parseInt(soneProtocolVersion, null);
+               }
+               if (protocolVersion == null) {
+                       logger.log(Level.INFO, "No protocol version found, assuming 0.");
+                       protocolVersion = 0;
+               }
+
+               if (protocolVersion < 0) {
+                       logger.log(Level.WARNING, String.format("Invalid protocol version: %d! Not parsing Sone.", protocolVersion));
+                       return null;
+               }
+
+               /* check for valid versions. */
+               if (protocolVersion > MAX_PROTOCOL_VERSION) {
+                       logger.log(Level.WARNING, String.format("Unknown protocol version: %d! Not parsing Sone.", protocolVersion));
+                       return null;
+               }
+
+               String soneTime = soneXml.getValue("time", null);
+               if (soneTime == null) {
+                       /* TODO - mark Sone as bad. */
+                       logger.log(Level.WARNING, String.format("Downloaded time for Sone %s was null!", sone));
+                       return null;
+               }
+               try {
+                       sone.setTime(Long.parseLong(soneTime));
+               } catch (NumberFormatException nfe1) {
+                       /* TODO - mark Sone as bad. */
+                       logger.log(Level.WARNING, String.format("Downloaded Sone %s with invalid time: %s", sone, soneTime));
+                       return null;
+               }
+
+               SimpleXML clientXml = soneXml.getNode("client");
+               if (clientXml != null) {
+                       String clientName = clientXml.getValue("name", null);
+                       String clientVersion = clientXml.getValue("version", null);