+ touchConfiguration();
+ }
+
+ /**
+ * Sets the trust value of the given origin Sone for the target Sone.
+ *
+ * @param origin
+ * The origin Sone
+ * @param target
+ * The target Sone
+ * @param trustValue
+ * The trust value (from {@code -100} to {@code 100})
+ */
+ public void setTrust(Sone origin, Sone target, int trustValue) {
+ checkNotNull(origin, "origin must not be null");
+ checkArgument(origin.getIdentity() instanceof OwnIdentity, "origin must be a local Sone");
+ checkNotNull(target, "target must not be null");
+ checkArgument((trustValue >= -100) && (trustValue <= 100), "trustValue must be within [-100, 100]");
+ webOfTrustUpdater.setTrust((OwnIdentity) origin.getIdentity(), target.getIdentity(), trustValue, preferences.getTrustComment());
+ }
+
+ /**
+ * Removes any trust assignment for the given target Sone.
+ *
+ * @param origin
+ * The trust origin
+ * @param target
+ * The trust target
+ */
+ public void removeTrust(Sone origin, Sone target) {
+ checkNotNull(origin, "origin must not be null");
+ checkNotNull(target, "target must not be null");
+ checkArgument(origin.getIdentity() instanceof OwnIdentity, "origin must be a local Sone");
+ webOfTrustUpdater.setTrust((OwnIdentity) origin.getIdentity(), target.getIdentity(), null, null);
+ }
+
+ /**
+ * Assigns the configured positive trust value for the given target.
+ *
+ * @param origin
+ * The trust origin
+ * @param target
+ * The trust target
+ */
+ public void trustSone(Sone origin, Sone target) {
+ setTrust(origin, target, preferences.getPositiveTrust());
+ }
+
+ /**
+ * Assigns the configured negative trust value for the given target.
+ *
+ * @param origin
+ * The trust origin
+ * @param target
+ * The trust target
+ */
+ public void distrustSone(Sone origin, Sone target) {
+ setTrust(origin, target, preferences.getNegativeTrust());
+ }
+
+ /**
+ * Removes the trust assignment for the given target.
+ *
+ * @param origin
+ * The trust origin
+ * @param target
+ * The trust target
+ */
+ public void untrustSone(Sone origin, Sone target) {
+ removeTrust(origin, target);
+ }
+
+ /**
+ * Updates the stored Sone with the given Sone.
+ *
+ * @param sone
+ * The updated Sone
+ */
+ public void updateSone(Sone sone) {
+ updateSone(sone, false);
+ }
+
+ /**
+ * Updates the stored Sone with the given Sone. If {@code soneRescueMode} is
+ * {@code true}, an older Sone than the current Sone can be given to restore
+ * an old state.
+ *
+ * @param sone
+ * The Sone to update
+ * @param soneRescueMode
+ * {@code true} if the stored Sone should be updated regardless
+ * of the age of the given Sone
+ */
+ 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;
+ }
+ List<Object> events =
+ collectEventsForChangesInSone(storedSone.get(), sone);
+ database.storeSone(sone);
+ for (Object event : events) {
+ eventBus.post(event);
+ }
+ sone.setOptions(storedSone.get().getOptions());
+ sone.setKnown(storedSone.get().isKnown());
+ sone.setStatus((sone.getTime() == 0) ? SoneStatus.unknown : SoneStatus.idle);
+ if (sone.isLocal()) {
+ touchConfiguration();
+ }
+ }
+ }
+
+ 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));
+ }
+ }
+ });
+ soneChangeDetector.onRemovedPosts(new PostProcessor() {
+ @Override
+ public void processPost(Post post) {
+ events.add(new PostRemovedEvent(post));
+ }
+ });
+ 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));
+ }
+ }
+ });
+ soneChangeDetector.onRemovedPostReplies(new PostReplyProcessor() {
+ @Override
+ public void processPostReply(PostReply postReply) {
+ events.add(new PostReplyRemovedEvent(postReply));
+ }
+ });
+ soneChangeDetector.detectChanges(newSone);
+ return events;
+ }
+
+ /**
+ * Deletes the given Sone. This will remove the Sone from the
+ * {@link #getLocalSones() local Sones}, stop its {@link SoneInserter} and
+ * remove the context from its identity.
+ *
+ * @param sone
+ * The Sone to delete
+ */
+ public void deleteSone(Sone sone) {
+ if (!(sone.getIdentity() instanceof OwnIdentity)) {
+ logger.log(Level.WARNING, String.format("Tried to delete Sone of non-own identity: %s", sone));
+ return;
+ }
+ 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 {
+ configuration.getLongValue("Sone/" + sone.getId() + "/Time").setValue(null);
+ } catch (ConfigurationException ce1) {
+ logger.log(Level.WARNING, "Could not remove Sone from configuration!", ce1);
+ }
+ }
+
+ /**
+ * Marks the given Sone as known. If the Sone was not {@link Post#isKnown()
+ * known} before, a {@link MarkSoneKnownEvent} is fired.
+ *
+ * @param sone
+ * The Sone to mark as known
+ */
+ public void markSoneKnown(Sone sone) {
+ if (!sone.isKnown()) {
+ sone.setKnown(true);
+ synchronized (knownSones) {
+ knownSones.add(sone.getId());
+ }
+ eventBus.post(new MarkSoneKnownEvent(sone));
+ touchConfiguration();
+ }
+ }
+
+ /**
+ * Loads and updates the given Sone from the configuration. If any error is
+ * encountered, loading is aborted and the given Sone is not changed.
+ *
+ * @param sone
+ * The Sone to load and update
+ */
+ public void loadSone(Sone sone) {
+ if (!sone.isLocal()) {
+ logger.log(Level.FINE, String.format("Tried to load non-local Sone: %s", sone));
+ return;
+ }
+ logger.info(String.format("Loading local Sone: %s", sone));
+
+ /* load Sone. */
+ String sonePrefix = "Sone/" + sone.getId();
+ Long soneTime = configuration.getLongValue(sonePrefix + "/Time").getValue(null);
+ if (soneTime == null) {
+ logger.log(Level.INFO, "Could not load Sone because no Sone has been saved.");
+ return;
+ }
+ String lastInsertFingerprint = configuration.getStringValue(sonePrefix + "/LastInsertFingerprint").getValue("");
+
+ /* load profile. */
+ ConfigurationSoneParser configurationSoneParser = new ConfigurationSoneParser(configuration, sone);
+ Profile profile = configurationSoneParser.parseProfile();
+
+ /* load posts. */
+ Collection<Post> posts;
+ try {
+ posts = configurationSoneParser.parsePosts(database);
+ } catch (InvalidPostFound ipf) {
+ logger.log(Level.WARNING, "Invalid post found, aborting load!");
+ return;
+ }
+
+ /* load replies. */
+ 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 =
+ configurationSoneParser.parseLikedPostIds();
+
+ /* load reply likes. */
+ Set<String> likedReplyIds =
+ configurationSoneParser.parseLikedPostReplyIds();
+
+ /* load albums. */
+ 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. */
+ 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) {
+ final Map<String, Image> images =
+ configurationSoneParser.getImages();
+ profile.setAvatar(images.get(avatarId));
+ }
+
+ /* load options. */
+ sone.getOptions().setAutoFollow(configuration.getBooleanValue(sonePrefix + "/Options/AutoFollow").getValue(false));
+ sone.getOptions().setSoneInsertNotificationEnabled(configuration.getBooleanValue(sonePrefix + "/Options/EnableSoneInsertNotifications").getValue(false));
+ sone.getOptions().setShowNewSoneNotifications(configuration.getBooleanValue(sonePrefix + "/Options/ShowNotification/NewSones").getValue(true));
+ sone.getOptions().setShowNewPostNotifications(configuration.getBooleanValue(sonePrefix + "/Options/ShowNotification/NewPosts").getValue(true));
+ sone.getOptions().setShowNewReplyNotifications(configuration.getBooleanValue(sonePrefix + "/Options/ShowNotification/NewReplies").getValue(true));
+ 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) {
+ sone.setTime(soneTime);
+ sone.setProfile(profile);
+ sone.setPosts(posts);
+ sone.setReplies(replies);
+ sone.setLikePostIds(likedPostIds);
+ sone.setLikeReplyIds(likedReplyIds);
+ for (Album album : sone.getRootAlbum().getAlbums()) {
+ sone.getRootAlbum().removeAlbum(album);
+ }
+ for (Album album : topLevelAlbums) {
+ sone.getRootAlbum().addAlbum(album);
+ }
+ synchronized (soneInserters) {
+ soneInserters.get(sone).setLastInsertFingerprint(lastInsertFingerprint);
+ }
+ }
+ for (Post post : posts) {
+ post.setKnown(true);
+ }
+ for (PostReply reply : replies) {
+ reply.setKnown(true);
+ }
+
+ logger.info(String.format("Sone loaded successfully: %s", sone));
+ }
+
+ /**
+ * 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 text
+ * The text of the post
+ * @return The created post
+ */
+ public Post createPost(Sone sone, Optional<Sone> recipient, String text) {
+ checkNotNull(text, "text must not be null");
+ checkArgument(text.trim().length() > 0, "text must not be empty");
+ if (!sone.isLocal()) {
+ logger.log(Level.FINE, String.format("Tried to create post for non-local Sone: %s", sone));
+ return null;
+ }
+ PostBuilder postBuilder = database.newPostBuilder();
+ postBuilder.from(sone.getId()).randomId().currentTime().withText(text.trim());
+ if (recipient.isPresent()) {
+ postBuilder.to(recipient.get().getId());
+ }
+ final Post post = postBuilder.build();
+ database.storePost(post);
+ eventBus.post(new NewPostFoundEvent(post));
+ sone.addPost(post);
+ touchConfiguration();
+ localElementTicker.schedule(new MarkPostKnown(post), 10, TimeUnit.SECONDS);
+ return post;
+ }
+
+ /**
+ * Deletes the given post.
+ *
+ * @param post
+ * The post to delete
+ */
+ public void deletePost(Post post) {
+ if (!post.getSone().isLocal()) {
+ logger.log(Level.WARNING, String.format("Tried to delete post of non-local Sone: %s", post.getSone()));
+ return;
+ }
+ database.removePost(post);
+ eventBus.post(new PostRemovedEvent(post));
+ markPostKnown(post);
+ touchConfiguration();
+ }
+
+ /**
+ * Marks the given post as known, if it is currently not a known post
+ * (according to {@link Post#isKnown()}).
+ *
+ * @param post
+ * The post to mark as known
+ */
+ public void markPostKnown(Post post) {
+ post.setKnown(true);
+ eventBus.post(new MarkPostKnownEvent(post));
+ touchConfiguration();
+ for (PostReply reply : getReplies(post.getId())) {
+ markReplyKnown(reply);
+ }
+ }
+
+ public void bookmarkPost(Post post) {
+ database.bookmarkPost(post);
+ }
+
+ /**
+ * Removes the given post from the bookmarks.
+ *
+ * @param post
+ * The post to unbookmark
+ */
+ public void unbookmarkPost(Post post) {
+ database.unbookmarkPost(post);
+ }
+
+ /**
+ * Creates a new reply.
+ *
+ * @param sone
+ * The Sone that creates the reply
+ * @param post
+ * The post that this reply refers to
+ * @param text
+ * The text of the reply
+ * @return The created reply
+ */
+ public PostReply createReply(Sone sone, Post post, String text) {
+ checkNotNull(text, "text must not be null");
+ checkArgument(text.trim().length() > 0, "text must not be empty");
+ if (!sone.isLocal()) {
+ logger.log(Level.FINE, String.format("Tried to create reply for non-local Sone: %s", sone));
+ return null;
+ }
+ PostReplyBuilder postReplyBuilder = postReplyBuilder();
+ postReplyBuilder.randomId().from(sone.getId()).to(post.getId()).currentTime().withText(text.trim());
+ final PostReply reply = postReplyBuilder.build();
+ database.storePostReply(reply);
+ eventBus.post(new NewPostReplyFoundEvent(reply));
+ sone.addReply(reply);
+ touchConfiguration();
+ localElementTicker.schedule(new MarkReplyKnown(reply), 10, TimeUnit.SECONDS);
+ return reply;
+ }
+
+ /**
+ * Deletes the given reply.
+ *
+ * @param reply
+ * The reply to delete
+ */
+ public void deleteReply(PostReply reply) {
+ Sone sone = reply.getSone();
+ if (!sone.isLocal()) {
+ logger.log(Level.FINE, String.format("Tried to delete non-local reply: %s", reply));
+ return;
+ }
+ database.removePostReply(reply);
+ markReplyKnown(reply);
+ sone.removeReply(reply);
+ touchConfiguration();
+ }
+
+ /**
+ * Marks the given reply as known, if it is currently not a known reply
+ * (according to {@link Reply#isKnown()}).
+ *
+ * @param reply
+ * The reply to mark as known
+ */
+ public void markReplyKnown(PostReply reply) {
+ boolean previouslyKnown = reply.isKnown();
+ reply.setKnown(true);
+ eventBus.post(new MarkPostReplyKnownEvent(reply));
+ if (!previouslyKnown) {
+ touchConfiguration();
+ }
+ }
+
+ /**
+ * Creates a new album for the given Sone.
+ *
+ * @param sone
+ * The Sone to create the album for
+ * @param parent
+ * The parent of the album (may be {@code null} to create a
+ * top-level album)
+ * @return The new album
+ */
+ public Album createAlbum(Sone sone, Album parent) {
+ Album album = database.newAlbumBuilder().randomId().by(sone).build();
+ database.storeAlbum(album);
+ parent.addAlbum(album);
+ return album;
+ }
+
+ /**
+ * Deletes the given album. The owner of the album has to be a local Sone,
+ * and the album has to be {@link Album#isEmpty() empty} to be deleted.
+ *
+ * @param album
+ * The album to remove
+ */
+ public void deleteAlbum(Album album) {
+ checkNotNull(album, "album must not be null");
+ checkArgument(album.getSone().isLocal(), "album’s Sone must be a local Sone");
+ if (!album.isEmpty()) {
+ return;
+ }
+ album.getParent().removeAlbum(album);
+ database.removeAlbum(album);
+ touchConfiguration();
+ }
+
+ /**
+ * Creates a new image.
+ *
+ * @param sone
+ * The Sone creating the image
+ * @param album
+ * The album the image will be inserted into
+ * @param temporaryImage
+ * The temporary image to create the image from
+ * @return The newly created image
+ */
+ public Image createImage(Sone sone, Album album, TemporaryImage temporaryImage) {
+ checkNotNull(sone, "sone must not be null");
+ checkNotNull(album, "album must not be null");
+ checkNotNull(temporaryImage, "temporaryImage must not be null");
+ checkArgument(sone.isLocal(), "sone must be a local Sone");
+ checkArgument(sone.equals(album.getSone()), "album must belong to the given Sone");
+ Image image = database.newImageBuilder().withId(temporaryImage.getId()).build().modify().setSone(sone).setCreationTime(System.currentTimeMillis()).update();
+ album.addImage(image);
+ database.storeImage(image);
+ imageInserter.insertImage(temporaryImage, image);
+ return image;
+ }
+
+ /**
+ * Deletes the given image. This method will also delete a matching
+ * temporary image.
+ *
+ * @see #deleteTemporaryImage(String)
+ * @param image
+ * The image to delete
+ */
+ public void deleteImage(Image image) {
+ checkNotNull(image, "image must not be null");
+ checkArgument(image.getSone().isLocal(), "image must belong to a local Sone");
+ deleteTemporaryImage(image.getId());
+ image.getAlbum().removeImage(image);
+ database.removeImage(image);
+ touchConfiguration();
+ }
+
+ /**
+ * Creates a new temporary image.
+ *
+ * @param mimeType
+ * The MIME type of the temporary image
+ * @param imageData
+ * The encoded data of the image
+ * @return The temporary image
+ */
+ public TemporaryImage createTemporaryImage(String mimeType, byte[] imageData) {
+ TemporaryImage temporaryImage = new TemporaryImage();
+ temporaryImage.setMimeType(mimeType).setImageData(imageData);
+ synchronized (temporaryImages) {
+ temporaryImages.put(temporaryImage.getId(), temporaryImage);
+ }
+ return temporaryImage;
+ }
+
+ /**
+ * Deletes the temporary image with the given ID.
+ *
+ * @param imageId
+ * The ID of the temporary image to delete
+ */
+ public void deleteTemporaryImage(String imageId) {
+ checkNotNull(imageId, "imageId must not be null");
+ synchronized (temporaryImages) {
+ temporaryImages.remove(imageId);
+ }
+ Image image = getImage(imageId, false);
+ if (image != null) {
+ imageInserter.cancelImageInsert(image);
+ }
+ }
+
+ /**
+ * Notifies the core that the configuration, either of the core or of a
+ * single local Sone, has changed, and that the configuration should be
+ * saved.
+ */
+ public void touchConfiguration() {
+ lastConfigurationUpdate = System.currentTimeMillis();
+ }
+
+ //
+ // SERVICE METHODS
+ //
+
+ /**
+ * Starts the core.
+ */
+ @Override
+ public void serviceStart() {
+ loadConfiguration();
+ updateChecker.start();
+ identityManager.start();
+ webOfTrustUpdater.init();
+ webOfTrustUpdater.start();
+ database.start();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void serviceRun() {
+ long lastSaved = System.currentTimeMillis();
+ while (!shouldStop()) {
+ sleep(1000);
+ long now = System.currentTimeMillis();
+ if (shouldStop() || ((lastConfigurationUpdate > lastSaved) && ((now - lastConfigurationUpdate) > 5000))) {
+ for (Sone localSone : getLocalSones()) {
+ saveSone(localSone);
+ }
+ saveConfiguration();
+ lastSaved = now;
+ }
+ }
+ }
+
+ /**
+ * Stops the core.
+ */
+ @Override
+ public void serviceStop() {
+ localElementTicker.shutdownNow();
+ synchronized (soneInserters) {
+ for (Entry<Sone, SoneInserter> soneInserter : soneInserters.entrySet()) {
+ soneInserter.getValue().stop();
+ saveSone(soneInserter.getKey());
+ }
+ }
+ synchronized (soneRescuers) {
+ for (SoneRescuer soneRescuer : soneRescuers.values()) {
+ soneRescuer.stop();
+ }
+ }
+ saveConfiguration();
+ database.stop();
+ webOfTrustUpdater.stop();
+ updateChecker.stop();
+ soneDownloader.stop();
+ soneDownloaders.shutdown();
+ identityManager.stop();
+ }
+
+ //
+ // PRIVATE METHODS
+ //
+
+ /**
+ * Saves the given Sone. This will persist all local settings for the given
+ * Sone, such as the friends list and similar, private options.
+ *
+ * @param sone
+ * The Sone to save
+ */
+ private synchronized void saveSone(Sone sone) {
+ if (!sone.isLocal()) {
+ logger.log(Level.FINE, String.format("Tried to save non-local Sone: %s", sone));
+ return;
+ }
+ if (!(sone.getIdentity() instanceof OwnIdentity)) {
+ logger.log(Level.WARNING, String.format("Local Sone without OwnIdentity found, refusing to save: %s", sone));
+ return;
+ }
+
+ logger.log(Level.INFO, String.format("Saving Sone: %s", sone));
+ try {
+ /* save Sone into configuration. */
+ String sonePrefix = "Sone/" + sone.getId();
+ configuration.getLongValue(sonePrefix + "/Time").setValue(sone.getTime());
+ configuration.getStringValue(sonePrefix + "/LastInsertFingerprint").setValue(soneInserters.get(sone).getLastInsertFingerprint());
+
+ /* save profile. */
+ Profile profile = sone.getProfile();
+ configuration.getStringValue(sonePrefix + "/Profile/FirstName").setValue(profile.getFirstName());
+ configuration.getStringValue(sonePrefix + "/Profile/MiddleName").setValue(profile.getMiddleName());
+ configuration.getStringValue(sonePrefix + "/Profile/LastName").setValue(profile.getLastName());
+ configuration.getIntValue(sonePrefix + "/Profile/BirthDay").setValue(profile.getBirthDay());
+ configuration.getIntValue(sonePrefix + "/Profile/BirthMonth").setValue(profile.getBirthMonth());
+ configuration.getIntValue(sonePrefix + "/Profile/BirthYear").setValue(profile.getBirthYear());
+ configuration.getStringValue(sonePrefix + "/Profile/Avatar").setValue(profile.getAvatar());
+
+ /* save profile fields. */
+ int fieldCounter = 0;
+ for (Field profileField : profile.getFields()) {
+ String fieldPrefix = sonePrefix + "/Profile/Fields/" + fieldCounter++;
+ configuration.getStringValue(fieldPrefix + "/Name").setValue(profileField.getName());
+ configuration.getStringValue(fieldPrefix + "/Value").setValue(profileField.getValue());
+ }
+ configuration.getStringValue(sonePrefix + "/Profile/Fields/" + fieldCounter + "/Name").setValue(null);
+
+ /* save posts. */
+ int postCounter = 0;
+ for (Post post : sone.getPosts()) {
+ String postPrefix = sonePrefix + "/Posts/" + postCounter++;
+ configuration.getStringValue(postPrefix + "/ID").setValue(post.getId());
+ configuration.getStringValue(postPrefix + "/Recipient").setValue(post.getRecipientId().orNull());
+ configuration.getLongValue(postPrefix + "/Time").setValue(post.getTime());
+ configuration.getStringValue(postPrefix + "/Text").setValue(post.getText());
+ }
+ configuration.getStringValue(sonePrefix + "/Posts/" + postCounter + "/ID").setValue(null);
+
+ /* save replies. */
+ int replyCounter = 0;
+ for (PostReply reply : sone.getReplies()) {
+ String replyPrefix = sonePrefix + "/Replies/" + replyCounter++;
+ configuration.getStringValue(replyPrefix + "/ID").setValue(reply.getId());
+ configuration.getStringValue(replyPrefix + "/Post/ID").setValue(reply.getPostId());
+ configuration.getLongValue(replyPrefix + "/Time").setValue(reply.getTime());
+ configuration.getStringValue(replyPrefix + "/Text").setValue(reply.getText());
+ }
+ configuration.getStringValue(sonePrefix + "/Replies/" + replyCounter + "/ID").setValue(null);
+
+ /* save post likes. */
+ int postLikeCounter = 0;
+ for (String postId : sone.getLikedPostIds()) {
+ configuration.getStringValue(sonePrefix + "/Likes/Post/" + postLikeCounter++ + "/ID").setValue(postId);
+ }
+ configuration.getStringValue(sonePrefix + "/Likes/Post/" + postLikeCounter + "/ID").setValue(null);
+
+ /* save reply likes. */
+ int replyLikeCounter = 0;
+ for (String replyId : sone.getLikedReplyIds()) {
+ configuration.getStringValue(sonePrefix + "/Likes/Reply/" + replyLikeCounter++ + "/ID").setValue(replyId);
+ }
+ configuration.getStringValue(sonePrefix + "/Likes/Reply/" + replyLikeCounter + "/ID").setValue(null);
+
+ /* save albums. first, collect in a flat structure, top-level first. */
+ List<Album> albums = FluentIterable.from(sone.getRootAlbum().getAlbums()).transformAndConcat(Album.FLATTENER).toList();
+
+ int albumCounter = 0;
+ for (Album album : albums) {
+ String albumPrefix = sonePrefix + "/Albums/" + albumCounter++;
+ configuration.getStringValue(albumPrefix + "/ID").setValue(album.getId());
+ configuration.getStringValue(albumPrefix + "/Title").setValue(album.getTitle());
+ configuration.getStringValue(albumPrefix + "/Description").setValue(album.getDescription());
+ configuration.getStringValue(albumPrefix + "/Parent").setValue(album.getParent().equals(sone.getRootAlbum()) ? null : album.getParent().getId());
+ }
+ configuration.getStringValue(sonePrefix + "/Albums/" + albumCounter + "/ID").setValue(null);
+
+ /* save images. */
+ int imageCounter = 0;
+ for (Album album : albums) {
+ for (Image image : album.getImages()) {
+ if (!image.isInserted()) {
+ continue;
+ }
+ String imagePrefix = sonePrefix + "/Images/" + imageCounter++;
+ configuration.getStringValue(imagePrefix + "/ID").setValue(image.getId());
+ configuration.getStringValue(imagePrefix + "/Album").setValue(album.getId());
+ configuration.getStringValue(imagePrefix + "/Key").setValue(image.getKey());
+ configuration.getStringValue(imagePrefix + "/Title").setValue(image.getTitle());
+ configuration.getStringValue(imagePrefix + "/Description").setValue(image.getDescription());
+ configuration.getLongValue(imagePrefix + "/CreationTime").setValue(image.getCreationTime());
+ configuration.getIntValue(imagePrefix + "/Width").setValue(image.getWidth());
+ configuration.getIntValue(imagePrefix + "/Height").setValue(image.getHeight());
+ }
+ }
+ configuration.getStringValue(sonePrefix + "/Images/" + imageCounter + "/ID").setValue(null);
+
+ /* save options. */
+ 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();
+
+ webOfTrustUpdater.setProperty((OwnIdentity) sone.getIdentity(), "Sone.LatestEdition", String.valueOf(sone.getLatestEdition()));
+
+ logger.log(Level.INFO, String.format("Sone %s saved.", sone));
+ } catch (ConfigurationException ce1) {
+ logger.log(Level.WARNING, String.format("Could not save Sone: %s", sone), ce1);
+ }
+ }
+
+ /**
+ * Saves the current options.
+ */
+ private void saveConfiguration() {
+ synchronized (configuration) {
+ if (storingConfiguration) {
+ logger.log(Level.FINE, "Already storing configuration…");
+ return;
+ }
+ storingConfiguration = true;
+ }
+
+ /* store the options first. */
+ try {
+ preferences.saveTo(configuration);
+
+ /* save known Sones. */
+ int soneCounter = 0;
+ synchronized (knownSones) {
+ for (String knownSoneId : knownSones) {
+ configuration.getStringValue("KnownSone/" + soneCounter++ + "/ID").setValue(knownSoneId);
+ }
+ configuration.getStringValue("KnownSone/" + soneCounter + "/ID").setValue(null);
+ }
+
+ /* save Sone following times. */
+ soneCounter = 0;
+ synchronized (soneFollowingTimes) {
+ for (Entry<String, Long> soneFollowingTime : soneFollowingTimes.entrySet()) {
+ configuration.getStringValue("SoneFollowingTimes/" + soneCounter + "/Sone").setValue(soneFollowingTime.getKey());
+ configuration.getLongValue("SoneFollowingTimes/" + soneCounter + "/Time").setValue(soneFollowingTime.getValue());
+ ++soneCounter;
+ }
+ configuration.getStringValue("SoneFollowingTimes/" + soneCounter + "/Sone").setValue(null);
+ }
+
+ /* save known posts. */
+ database.save();
+
+ /* now save it. */
+ configuration.save();
+
+ } catch (ConfigurationException ce1) {
+ logger.log(Level.SEVERE, "Could not store configuration!", ce1);
+ } catch (DatabaseException de1) {
+ logger.log(Level.SEVERE, "Could not save database!", de1);
+ } finally {
+ synchronized (configuration) {
+ storingConfiguration = false;
+ }
+ }
+ }
+
+ /**
+ * Loads the configuration.
+ */
+ private void loadConfiguration() {
+ new PreferencesLoader(preferences).loadFrom(configuration);
+
+ /* load known Sones. */
+ int soneCounter = 0;
+ while (true) {
+ String knownSoneId = configuration.getStringValue("KnownSone/" + soneCounter++ + "/ID").getValue(null);
+ if (knownSoneId == null) {
+ break;
+ }
+ synchronized (knownSones) {
+ knownSones.add(knownSoneId);
+ }
+ }
+
+ /* load Sone following times. */
+ soneCounter = 0;
+ while (true) {
+ String soneId = configuration.getStringValue("SoneFollowingTimes/" + soneCounter + "/Sone").getValue(null);
+ if (soneId == null) {
+ break;
+ }
+ long time = configuration.getLongValue("SoneFollowingTimes/" + soneCounter + "/Time").getValue(Long.MAX_VALUE);
+ synchronized (soneFollowingTimes) {
+ soneFollowingTimes.put(soneId, time);
+ }
+ ++soneCounter;
+ }
+ }
+
+ /**
+ * Notifies the core that a new {@link OwnIdentity} was added.
+ *
+ * @param ownIdentityAddedEvent
+ * The event
+ */
+ @Subscribe
+ public void ownIdentityAdded(OwnIdentityAddedEvent ownIdentityAddedEvent) {
+ OwnIdentity ownIdentity = ownIdentityAddedEvent.ownIdentity();
+ logger.log(Level.FINEST, String.format("Adding OwnIdentity: %s", ownIdentity));
+ if (ownIdentity.hasContext("Sone")) {
+ addLocalSone(ownIdentity);
+ }
+ }
+
+ /**
+ * Notifies the core that an {@link OwnIdentity} was removed.
+ *
+ * @param ownIdentityRemovedEvent
+ * The event
+ */
+ @Subscribe
+ public void ownIdentityRemoved(OwnIdentityRemovedEvent ownIdentityRemovedEvent) {
+ OwnIdentity ownIdentity = ownIdentityRemovedEvent.ownIdentity();
+ logger.log(Level.FINEST, String.format("Removing OwnIdentity: %s", ownIdentity));
+ trustedIdentities.removeAll(ownIdentity);
+ }
+
+ /**
+ * Notifies the core that a new {@link Identity} was added.
+ *
+ * @param identityAddedEvent
+ * The event
+ */
+ @Subscribe
+ public void identityAdded(IdentityAddedEvent identityAddedEvent) {
+ Identity identity = identityAddedEvent.identity();
+ logger.log(Level.FINEST, String.format("Adding Identity: %s", identity));
+ trustedIdentities.put(identityAddedEvent.ownIdentity(), identity);
+ addRemoteSone(identity);
+ }
+
+ /**
+ * Notifies the core that an {@link Identity} was updated.
+ *
+ * @param identityUpdatedEvent
+ * The event
+ */
+ @Subscribe
+ public void identityUpdated(IdentityUpdatedEvent identityUpdatedEvent) {
+ 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));
+ }
+
+ /**
+ * Notifies the core that an {@link Identity} was removed.
+ *
+ * @param identityRemovedEvent
+ * The event
+ */
+ @Subscribe
+ public void identityRemoved(IdentityRemovedEvent identityRemovedEvent) {
+ OwnIdentity ownIdentity = identityRemovedEvent.ownIdentity();
+ Identity identity = identityRemovedEvent.identity();
+ trustedIdentities.remove(ownIdentity, identity);
+ for (Entry<OwnIdentity, Collection<Identity>> trustedIdentity : trustedIdentities.asMap().entrySet()) {
+ if (trustedIdentity.getKey().equals(ownIdentity)) {
+ continue;
+ }
+ if (trustedIdentity.getValue().contains(identity)) {
+ return;
+ }
+ }
+ Optional<Sone> sone = getSone(identity.getId());
+ if (!sone.isPresent()) {
+ /* TODO - we don’t have the Sone anymore. should this happen? */
+ return;
+ }
+ for (PostReply postReply : sone.get().getReplies()) {
+ eventBus.post(new PostReplyRemovedEvent(postReply));
+ }
+ for (Post post : sone.get().getPosts()) {
+ eventBus.post(new PostRemovedEvent(post));
+ }
+ eventBus.post(new SoneRemovedEvent(sone.get()));
+ database.removeSone(sone.get());
+ }
+
+ /**
+ * Deletes the temporary image.
+ *
+ * @param imageInsertFinishedEvent
+ * The event
+ */
+ @Subscribe
+ public void imageInsertFinished(ImageInsertFinishedEvent imageInsertFinishedEvent) {
+ logger.log(Level.WARNING, String.format("Image insert finished for %s: %s", imageInsertFinishedEvent.image(), imageInsertFinishedEvent.resultingUri()));
+ imageInsertFinishedEvent.image().modify().setKey(imageInsertFinishedEvent.resultingUri().toString()).update();
+ deleteTemporaryImage(imageInsertFinishedEvent.image().getId());
+ 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);
+ }
+