+ 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);
+ }
+ database.storeSone(sone);
+ 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);