+ * 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(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));
+ }
+ }
+ /* 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));
+ }
+ }
+ }
+ 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));
+ }
+ }
+ database.storePostReplies(sone, sone.getReplies());
+ synchronized (albums) {
+ synchronized (images) {
+ for (Album album : storedSone.get().getAlbums()) {
+ albums.remove(album.getId());
+ for (Image image : album.getImages()) {
+ images.remove(image.getId());
+ }
+ }
+ for (Album album : sone.getAlbums()) {
+ albums.put(album.getId(), album);
+ for (Image image : album.getImages()) {
+ images.put(image.getId(), image);
+ }
+ }
+ }
+ }
+ synchronized (sones) {
+ sone.setOptions(storedSone.get().getOptions());
+ sones.put(sone.getId(), sone);
+ }
+ }
+ }
+
+ /**
+ * 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;
+ }
+ 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();
+ }
+ 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;
+ }
+
+ /* 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);
+ 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. */
+ 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);
+ }
+
+ /* 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());
+ }
+
+ /* 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());
+ }
+
+ /* 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);
+ }
+
+ /* 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);
+ }
+
+ /* 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).setTitle(albumTitle).setDescription(albumDescription).setAlbumImage(albumImageId);
+ 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);
+ }
+ }
+ }
+
+ /* 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).setSone(sone).setCreationTime(creationTime).setKey(key);
+ image.setTitle(title).setDescription(description).setWidth(width).setHeight(height);
+ album.addImage(image);
+ }
+
+ /* load avatar. */
+ String avatarId = configuration.getStringValue(sonePrefix + "/Profile/Avatar").getValue(null);
+ if (avatarId != null) {
+ profile.setAvatar(getImage(avatarId, false));
+ }
+
+ /* 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())));
+
+ /* 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 (String friendId : friends) {
+ followSone(sone, friendId);
+ }
+ sone.setAlbums(topLevelAlbums);
+ soneInserters.get(sone).setLastInsertFingerprint(lastInsertFingerprint);
+ }
+ synchronized (knownSones) {
+ for (String friend : friends) {
+ knownSones.add(friend);
+ }
+ }
+ database.storePosts(sone, posts);
+ for (Post post : posts) {
+ post.setKnown(true);
+ }
+ database.storePostReplies(sone, replies);
+ for (PostReply reply : replies) {
+ reply.setKnown(true);
+ }
+ }
+
+ /**
+ * Creates a new post.
+ *
+ * @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