+ /**
+ * 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
+ * @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
+ * @param text
+ * The text of the post
+ * @return The created post
+ */
+ public Post createPost(Sone sone, 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, Sone recipient, long time, String text) {
+ if (!isLocalSone(sone)) {
+ logger.log(Level.FINE, "Tried to create post for non-local Sone: %s", sone);
+ return null;
+ }
+ final Post post = new Post(sone, time, text);
+ if (recipient != null) {
+ post.setRecipient(recipient);
+ }
+ synchronized (posts) {
+ posts.put(post.getId(), post);
+ }
+ coreListenerManager.fireNewPostFound(post);
+ sone.addPost(post);
+ touchConfiguration();
+ localElementTicker.registerEvent(System.currentTimeMillis() + 10 * 1000, new Runnable() {
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void run() {
+ markPostKnown(post);
+ }
+ }, "Mark " + post + " read.");
+ return post;
+ }
+
+ /**
+ * Deletes the given post.
+ *
+ * @param post
+ * The post to delete
+ */
+ public void deletePost(Post post) {
+ if (!isLocalSone(post.getSone())) {
+ logger.log(Level.WARNING, "Tried to delete post of non-local Sone: %s", post.getSone());
+ return;
+ }
+ post.getSone().removePost(post);
+ synchronized (posts) {
+ posts.remove(post.getId());
+ }
+ coreListenerManager.firePostRemoved(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);
+ synchronized (knownPosts) {
+ coreListenerManager.fireMarkPostKnown(post);
+ if (knownPosts.add(post.getId())) {
+ touchConfiguration();
+ }
+ }
+ for (PostReply reply : getReplies(post)) {
+ markReplyKnown(reply);
+ }
+ }
+
+ /**
+ * 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);
+ }
+ }
+
+ /**
+ * Removes the given post from the bookmarks.
+ *
+ * @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);
+ }
+ }
+
+ /**
+ * 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) {
+ return createReply(sone, post, System.currentTimeMillis(), text);
+ }
+
+ /**
+ * Creates a new reply.
+ *
+ * @param sone
+ * The Sone that creates the reply
+ * @param post
+ * The post that this reply refers to
+ * @param time
+ * The time of the reply
+ * @param text
+ * The text of the reply
+ * @return The created reply
+ */
+ public PostReply createReply(Sone sone, Post post, long time, String text) {
+ if (!isLocalSone(sone)) {
+ logger.log(Level.FINE, "Tried to create reply for non-local Sone: %s", sone);
+ return null;
+ }
+ final PostReply reply = new PostReply(sone, post, System.currentTimeMillis(), text);
+ synchronized (replies) {
+ replies.put(reply.getId(), reply);
+ }
+ synchronized (knownReplies) {
+ coreListenerManager.fireNewReplyFound(reply);
+ }
+ sone.addReply(reply);
+ touchConfiguration();
+ localElementTicker.registerEvent(System.currentTimeMillis() + 10 * 1000, new Runnable() {
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void run() {
+ markReplyKnown(reply);
+ }
+ }, "Mark " + reply + " read.");
+ return reply;
+ }
+
+ /**
+ * Deletes the given reply.
+ *
+ * @param reply
+ * The reply to delete
+ */
+ public void deleteReply(PostReply reply) {
+ Sone sone = reply.getSone();
+ if (!isLocalSone(sone)) {
+ logger.log(Level.FINE, "Tried to delete non-local reply: %s", reply);
+ return;
+ }
+ synchronized (replies) {
+ replies.remove(reply.getId());
+ }
+ synchronized (knownReplies) {
+ markReplyKnown(reply);
+ knownReplies.remove(reply.getId());
+ }
+ 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) {
+ reply.setKnown(true);
+ synchronized (knownReplies) {
+ coreListenerManager.fireMarkReplyKnown(reply);
+ if (knownReplies.add(reply.getId())) {
+ touchConfiguration();
+ }
+ }
+ }
+
+ /**
+ * 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, null);
+ }
+
+ /**
+ * 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 = new Album();
+ synchronized (albums) {
+ albums.put(album.getId(), album);
+ }
+ album.setSone(sone);
+ if (parent != null) {
+ parent.addAlbum(album);
+ } else {
+ sone.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) {
+ Validation.begin().isNotNull("Album", album).check().is("Local Sone", isLocalSone(album.getSone())).check();
+ if (!album.isEmpty()) {
+ return;
+ }
+ if (album.getParent() == null) {
+ album.getSone().removeAlbum(album);
+ } else {
+ album.getParent().removeAlbum(album);
+ }
+ synchronized (albums) {
+ albums.remove(album.getId());
+ }
+ saveSone(album.getSone());
+ }
+
+ /**
+ * 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) {
+ Validation.begin().isNotNull("Sone", sone).isNotNull("Album", album).isNotNull("Temporary Image", temporaryImage).check().is("Local Sone", isLocalSone(sone)).check().isEqual("Owner and Album Owner", sone, album.getSone()).check();
+ Image image = new Image(temporaryImage.getId()).setSone(sone).setCreationTime(System.currentTimeMillis());
+ album.addImage(image);
+ synchronized (images) {
+ images.put(image.getId(), image);
+ }
+ imageInserter.insertImage(temporaryImage, image);
+ return image;
+ }
+
+ /**
+ * Deletes the given image. This method will also delete a matching
+ * temporary image.
+ *
+ * @see #deleteTemporaryImage(TemporaryImage)
+ * @param image
+ * The image to delete
+ */
+ public void deleteImage(Image image) {
+ Validation.begin().isNotNull("Image", image).check().is("Local Sone", isLocalSone(image.getSone())).check();
+ deleteTemporaryImage(image.getId());
+ image.getAlbum().removeImage(image);
+ synchronized (images) {
+ images.remove(image.getId());
+ }
+ saveSone(image.getSone());
+ }
+
+ /**
+ * 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 given temporary image.
+ *
+ * @param temporaryImage
+ * The temporary image to delete
+ */
+ public void deleteTemporaryImage(TemporaryImage temporaryImage) {
+ Validation.begin().isNotNull("Temporary Image", temporaryImage).check();
+ deleteTemporaryImage(temporaryImage.getId());
+ }
+
+ /**
+ * Deletes the temporary image with the given ID.
+ *
+ * @param imageId
+ * The ID of the temporary image to delete
+ */
+ public void deleteTemporaryImage(String imageId) {
+ Validation.begin().isNotNull("Temporary Image ID", imageId).check();
+ 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.addUpdateListener(this);
+ updateChecker.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() {
+ synchronized (localSones) {
+ for (SoneInserter soneInserter : soneInserters.values()) {
+ soneInserter.removeSoneInsertListener(this);
+ soneInserter.stop();
+ }
+ }
+ updateChecker.stop();
+ updateChecker.removeUpdateListener(this);
+ soneDownloader.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 (!isLocalSone(sone)) {
+ logger.log(Level.FINE, "Tried to save non-local Sone: %s", sone);
+ return;
+ }
+ if (!(sone.getIdentity() instanceof OwnIdentity)) {
+ logger.log(Level.WARNING, "Local Sone without OwnIdentity found, refusing to save: %s", sone);
+ return;
+ }
+
+ logger.log(Level.INFO, "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.getRecipient() != null) ? post.getRecipient().getId() : null);
+ 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.getPost().getId());
+ 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 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 = sone.getAllAlbums();
+
+ 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() == null ? null : album.getParent().getId());
+ configuration.getStringValue(albumPrefix + "/AlbumImage").setValue(album.getAlbumImage() == null ? null : album.getAlbumImage().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().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.save();
+
+ ((OwnIdentity) sone.getIdentity()).setProperty("Sone.LatestEdition", String.valueOf(sone.getLatestEdition()));
+
+ logger.log(Level.INFO, "Sone %s saved.", sone);
+ } catch (ConfigurationException ce1) {
+ logger.log(Level.WARNING, "Could not save Sone: " + sone, ce1);
+ } catch (WebOfTrustException wote1) {
+ logger.log(Level.WARNING, "Could not set WoT property for Sone: " + sone, wote1);
+ }
+ }
+
+ /**
+ * 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 {
+ 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/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());
+ configuration.getBooleanValue("Option/SoneRescueMode").setValue(options.getBooleanOption("SoneRescueMode").getReal());
+ configuration.getBooleanValue("Option/ClearOnNextRestart").setValue(options.getBooleanOption("ClearOnNextRestart").getReal());
+ configuration.getBooleanValue("Option/ReallyClearOnNextRestart").setValue(options.getBooleanOption("ReallyClearOnNextRestart").getReal());
+
+ /* 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<Sone, Long> soneFollowingTime : soneFollowingTimes.entrySet()) {
+ configuration.getStringValue("SoneFollowingTimes/" + soneCounter + "/Sone").setValue(soneFollowingTime.getKey().getId());
+ configuration.getLongValue("SoneFollowingTimes/" + soneCounter + "/Time").setValue(soneFollowingTime.getValue());
+ ++soneCounter;
+ }
+ configuration.getStringValue("SoneFollowingTimes/" + soneCounter + "/Sone").setValue(null);
+ }
+
+ /* save known posts. */
+ int postCounter = 0;
+ synchronized (knownPosts) {
+ for (String knownPostId : knownPosts) {
+ configuration.getStringValue("KnownPosts/" + postCounter++ + "/ID").setValue(knownPostId);
+ }
+ configuration.getStringValue("KnownPosts/" + postCounter + "/ID").setValue(null);
+ }
+
+ /* save known replies. */
+ int replyCounter = 0;
+ synchronized (knownReplies) {
+ for (String knownReplyId : knownReplies) {
+ configuration.getStringValue("KnownReplies/" + replyCounter++ + "/ID").setValue(knownReplyId);
+ }
+ configuration.getStringValue("KnownReplies/" + replyCounter + "/ID").setValue(null);
+ }
+
+ /* 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();
+
+ } catch (ConfigurationException ce1) {
+ logger.log(Level.SEVERE, "Could not store configuration!", ce1);
+ } finally {
+ synchronized (configuration) {
+ storingConfiguration = false;
+ }
+ }
+ }
+
+ /**
+ * Loads the configuration.
+ */
+ @SuppressWarnings("unchecked")
+ private void loadConfiguration() {
+ /* create options. */
+ options.addIntegerOption("InsertionDelay", new DefaultOption<Integer>(60, new IntegerRangeValidator(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 IntegerRangeValidator(1, Integer.MAX_VALUE)));
+ options.addIntegerOption("CharactersPerPost", new DefaultOption<Integer>(400, new OrValidator<Integer>(new IntegerRangeValidator(50, Integer.MAX_VALUE), new EqualityValidator<Integer>(-1))));
+ options.addIntegerOption("PostCutOffLength", new DefaultOption<Integer>(200, new OrValidator<Integer>(new IntegerRangeValidator(50, Integer.MAX_VALUE), new EqualityValidator<Integer>(-1))));
+ options.addBooleanOption("RequireFullAccess", new DefaultOption<Boolean>(false));
+ options.addIntegerOption("PositiveTrust", new DefaultOption<Integer>(75, new IntegerRangeValidator(0, 100)));
+ options.addIntegerOption("NegativeTrust", new DefaultOption<Integer>(-25, new IntegerRangeValidator(-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]);
+ }
+
+ }));
+ options.addBooleanOption("SoneRescueMode", new DefaultOption<Boolean>(false));
+ options.addBooleanOption("ClearOnNextRestart", new DefaultOption<Boolean>(false));
+ options.addBooleanOption("ReallyClearOnNextRestart", new DefaultOption<Boolean>(false));
+
+ /* read options from configuration. */
+ options.getBooleanOption("ClearOnNextRestart").set(configuration.getBooleanValue("Option/ClearOnNextRestart").getValue(null));
+ options.getBooleanOption("ReallyClearOnNextRestart").set(configuration.getBooleanValue("Option/ReallyClearOnNextRestart").getValue(null));
+ boolean clearConfiguration = options.getBooleanOption("ClearOnNextRestart").get() && options.getBooleanOption("ReallyClearOnNextRestart").get();
+ options.getBooleanOption("ClearOnNextRestart").set(null);
+ options.getBooleanOption("ReallyClearOnNextRestart").set(null);
+ if (clearConfiguration) {
+ /* stop loading the configuration. */
+ return;
+ }
+
+ loadConfigurationValue("InsertionDelay");
+ loadConfigurationValue("PostsPerPage");
+ 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));
+ options.getBooleanOption("SoneRescueMode").set(configuration.getBooleanValue("Option/SoneRescueMode").getValue(null));
+
+ /* 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);
+ Sone followedSone = getSone(soneId);
+ if (followedSone == null) {
+ logger.log(Level.WARNING, String.format("Ignoring Sone with invalid ID: %s", soneId));
+ } else {
+ synchronized (soneFollowingTimes) {
+ soneFollowingTimes.put(getSone(soneId), time);
+ }
+ }
+ ++soneCounter;
+ }
+
+ /* load known posts. */
+ int postCounter = 0;
+ while (true) {
+ String knownPostId = configuration.getStringValue("KnownPosts/" + postCounter++ + "/ID").getValue(null);
+ if (knownPostId == null) {
+ break;
+ }
+ synchronized (knownPosts) {
+ knownPosts.add(knownPostId);
+ }
+ }
+
+ /* load known replies. */
+ int replyCounter = 0;
+ while (true) {
+ String knownReplyId = configuration.getStringValue("KnownReplies/" + replyCounter++ + "/ID").getValue(null);
+ if (knownReplyId == null) {
+ break;
+ }
+ synchronized (knownReplies) {
+ knownReplies.add(knownReplyId);
+ }
+ }
+
+ /* 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, "Invalid value for " + optionName + " in configuration, using default.");
+ }
+ }
+
+ /**
+ * Generate a Sone URI from the given URI and latest edition.
+ *
+ * @param uriString
+ * The URI to derive the Sone URI from
+ * @return The derived URI
+ */
+ private FreenetURI getSoneUri(String uriString) {
+ try {
+ FreenetURI uri = new FreenetURI(uriString).setDocName("Sone").setMetaString(new String[0]);
+ return uri;
+ } catch (MalformedURLException mue1) {
+ logger.log(Level.WARNING, "Could not create Sone URI from URI: " + uriString, mue1);
+ return null;
+ }
+ }
+
+ //
+ // INTERFACE IdentityListener
+ //
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void ownIdentityAdded(OwnIdentity ownIdentity) {
+ logger.log(Level.FINEST, "Adding OwnIdentity: %s", ownIdentity);
+ if (ownIdentity.hasContext("Sone")) {
+ trustedIdentities.put(ownIdentity, Collections.synchronizedSet(new HashSet<Identity>()));
+ addLocalSone(ownIdentity);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void ownIdentityRemoved(OwnIdentity ownIdentity) {
+ logger.log(Level.FINEST, "Removing OwnIdentity: " + ownIdentity);
+ trustedIdentities.remove(ownIdentity);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void identityAdded(OwnIdentity ownIdentity, Identity identity) {
+ logger.log(Level.FINEST, "Adding Identity: %s", identity);
+ trustedIdentities.get(ownIdentity).add(identity);
+ addRemoteSone(identity);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void identityUpdated(OwnIdentity ownIdentity, final Identity identity) {
+ new Thread(new Runnable() {
+
+ @Override
+ @SuppressWarnings("synthetic-access")
+ public void run() {
+ Sone sone = getRemoteSone(identity.getId(), false);
+ sone.setIdentity(identity);
+ sone.setLatestEdition(Numbers.safeParseLong(identity.getProperty("Sone.LatestEdition"), sone.getLatestEdition()));
+ soneDownloader.addSone(sone);
+ soneDownloader.fetchSone(sone);
+ }
+ }).start();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void identityRemoved(OwnIdentity ownIdentity, Identity identity) {
+ trustedIdentities.get(ownIdentity).remove(identity);
+ boolean foundIdentity = false;
+ for (Entry<OwnIdentity, Set<Identity>> trustedIdentity : trustedIdentities.entrySet()) {
+ if (trustedIdentity.getKey().equals(ownIdentity)) {
+ continue;
+ }
+ if (trustedIdentity.getValue().contains(identity)) {
+ foundIdentity = true;
+ }
+ }
+ if (foundIdentity) {
+ /* some local identity still trusts this identity, don’t remove. */
+ return;
+ }
+ Sone sone = getSone(identity.getId(), false);
+ if (sone == null) {
+ /* TODO - we don’t have the Sone anymore. should this happen? */
+ return;
+ }
+ synchronized (posts) {
+ synchronized (knownPosts) {
+ for (Post post : sone.getPosts()) {
+ posts.remove(post.getId());
+ coreListenerManager.firePostRemoved(post);
+ }
+ }
+ }
+ synchronized (replies) {
+ synchronized (knownReplies) {
+ for (PostReply reply : sone.getReplies()) {
+ replies.remove(reply.getId());
+ coreListenerManager.fireReplyRemoved(reply);
+ }
+ }
+ }
+ synchronized (remoteSones) {
+ remoteSones.remove(identity.getId());
+ }
+ coreListenerManager.fireSoneRemoved(sone);
+ }
+
+ //
+ // INTERFACE UpdateListener
+ //
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void updateFound(Version version, long releaseTime, long latestEdition) {
+ coreListenerManager.fireUpdateFound(version, releaseTime, latestEdition);
+ }
+
+ //
+ // INTERFACE ImageInsertListener
+ //
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void insertStarted(Sone sone) {
+ coreListenerManager.fireSoneInserting(sone);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void insertFinished(Sone sone, long insertDuration) {
+ coreListenerManager.fireSoneInserted(sone, insertDuration);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void insertAborted(Sone sone, Throwable cause) {
+ coreListenerManager.fireSoneInsertAborted(sone, cause);
+ }
+
+ //
+ // SONEINSERTLISTENER METHODS
+ //
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void imageInsertStarted(Image image) {
+ logger.log(Level.WARNING, "Image insert started for " + image);
+ coreListenerManager.fireImageInsertStarted(image);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void imageInsertAborted(Image image) {
+ logger.log(Level.WARNING, "Image insert aborted for " + image);
+ coreListenerManager.fireImageInsertAborted(image);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void imageInsertFinished(Image image, FreenetURI key) {
+ logger.log(Level.WARNING, "Image insert finished for " + image + ": " + key);
+ image.setKey(key.toString());
+ deleteTemporaryImage(image.getId());
+ saveSone(image.getSone());
+ coreListenerManager.fireImageInsertFinished(image);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void imageInsertFailed(Image image, Throwable cause) {
+ logger.log(Level.WARNING, "Image insert failed for " + image, cause);
+ coreListenerManager.fireImageInsertFailed(image, cause);
+ }
+
+ /**
+ * Convenience interface for external classes that want to access the core’s
+ * configuration.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+ public static 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;
+ }
+
+ /**
+ * Returns the insertion delay.
+ *
+ * @return The insertion delay
+ */
+ public int getInsertionDelay() {
+ return options.getIntegerOption("InsertionDelay").get();
+ }
+
+ /**
+ * Validates the given insertion delay.
+ *
+ * @param insertionDelay
+ * The insertion delay to validate
+ * @return {@code true} if the given insertion delay was valid,
+ * {@code false} otherwise
+ */
+ public boolean validateInsertionDelay(Integer insertionDelay) {
+ return options.getIntegerOption("InsertionDelay").validate(insertionDelay);
+ }
+
+ /**
+ * Sets the insertion delay
+ *
+ * @param insertionDelay
+ * The new insertion delay, or {@code null} to restore it to
+ * the default value
+ * @return This preferences
+ */
+ public Preferences setInsertionDelay(Integer insertionDelay) {
+ options.getIntegerOption("InsertionDelay").set(insertionDelay);
+ return this;
+ }
+
+ /**
+ * Returns the number of posts to show per page.
+ *
+ * @return The number of posts to show per page
+ */
+ public int getPostsPerPage() {
+ return options.getIntegerOption("PostsPerPage").get();
+ }
+
+ /**
+ * Validates the number of posts per page.
+ *
+ * @param postsPerPage
+ * The number of posts per page
+ * @return {@code true} if the number of posts per page was valid,
+ * {@code false} otherwise
+ */
+ public boolean validatePostsPerPage(Integer postsPerPage) {
+ return options.getIntegerOption("PostsPerPage").validate(postsPerPage);
+ }
+
+ /**
+ * Sets the number of posts to show per page.
+ *
+ * @param postsPerPage
+ * The number of posts to show per page
+ * @return This preferences object
+ */
+ public Preferences setPostsPerPage(Integer postsPerPage) {
+ options.getIntegerOption("PostsPerPage").set(postsPerPage);
+ return this;
+ }
+
+ /**
+ * Returns the number of characters per post, or <code>-1</code> if the
+ * posts should not be cut off.
+ *
+ * @return The numbers of characters per post
+ */
+ public int getCharactersPerPost() {
+ return options.getIntegerOption("CharactersPerPost").get();
+ }
+
+ /**
+ * Validates the number of characters per post.
+ *
+ * @param charactersPerPost
+ * The number of characters per post
+ * @return {@code true} if the number of characters per post was valid,
+ * {@code false} otherwise
+ */
+ public boolean validateCharactersPerPost(Integer charactersPerPost) {
+ return options.getIntegerOption("CharactersPerPost").validate(charactersPerPost);
+ }
+
+ /**
+ * Sets the number of characters per post.
+ *
+ * @param charactersPerPost
+ * The number of characters per post, or <code>-1</code> to
+ * not cut off the posts
+ * @return This preferences objects
+ */
+ public Preferences setCharactersPerPost(Integer charactersPerPost) {
+ options.getIntegerOption("CharactersPerPost").set(charactersPerPost);
+ return this;
+ }
+
+ /**
+ * Returns the number of characters the shortened post should have.
+ *
+ * @return The number of characters of the snippet
+ */
+ public int getPostCutOffLength() {
+ return options.getIntegerOption("PostCutOffLength").get();
+ }
+
+ /**
+ * Validates the number of characters after which to cut off the post.
+ *
+ * @param postCutOffLength
+ * The number of characters of the snippet
+ * @return {@code true} if the number of characters of the snippet is
+ * valid, {@code false} otherwise
+ */
+ public boolean validatePostCutOffLength(Integer postCutOffLength) {
+ return options.getIntegerOption("PostCutOffLength").validate(postCutOffLength);
+ }
+
+ /**
+ * Sets the number of characters the shortened post should have.
+ *
+ * @param postCutOffLength
+ * The number of characters of the snippet
+ * @return This preferences
+ */
+ public Preferences setPostCutOffLength(Integer postCutOffLength) {
+ options.getIntegerOption("PostCutOffLength").set(postCutOffLength);
+ return this;
+ }
+
+ /**
+ * Returns whether Sone requires full access to be even visible.
+ *
+ * @return {@code true} if Sone requires full access, {@code false}
+ * otherwise
+ */
+ public boolean isRequireFullAccess() {
+ return options.getBooleanOption("RequireFullAccess").get();
+ }
+
+ /**
+ * Sets whether Sone requires full access to be even visible.
+ *
+ * @param requireFullAccess
+ * {@code true} if Sone requires full access, {@code false}
+ * otherwise
+ */
+ public void setRequireFullAccess(Boolean requireFullAccess) {
+ options.getBooleanOption("RequireFullAccess").set(requireFullAccess);
+ }
+
+ /**
+ * Returns the positive trust.
+ *
+ * @return The positive trust
+ */
+ public int getPositiveTrust() {
+ return options.getIntegerOption("PositiveTrust").get();
+ }
+
+ /**
+ * Validates the positive trust.
+ *
+ * @param positiveTrust
+ * The positive trust to validate
+ * @return {@code true} if the positive trust was valid, {@code false}
+ * otherwise
+ */
+ public boolean validatePositiveTrust(Integer positiveTrust) {
+ return options.getIntegerOption("PositiveTrust").validate(positiveTrust);
+ }
+
+ /**
+ * Sets the positive trust.
+ *
+ * @param positiveTrust
+ * The new positive trust, or {@code null} to restore it to
+ * the default vlaue
+ * @return This preferences
+ */
+ public Preferences setPositiveTrust(Integer positiveTrust) {
+ options.getIntegerOption("PositiveTrust").set(positiveTrust);
+ return this;
+ }
+
+ /**
+ * Returns the negative trust.
+ *
+ * @return The negative trust
+ */
+ public int getNegativeTrust() {
+ return options.getIntegerOption("NegativeTrust").get();
+ }
+
+ /**
+ * Validates the negative trust.
+ *
+ * @param negativeTrust
+ * The negative trust to validate
+ * @return {@code true} if the negative trust was valid, {@code false}
+ * otherwise
+ */
+ public boolean validateNegativeTrust(Integer negativeTrust) {
+ return options.getIntegerOption("NegativeTrust").validate(negativeTrust);
+ }
+
+ /**
+ * Sets the negative trust.
+ *
+ * @param negativeTrust
+ * The negative trust, or {@code null} to restore it to the
+ * default value
+ * @return The preferences
+ */
+ public Preferences setNegativeTrust(Integer negativeTrust) {
+ options.getIntegerOption("NegativeTrust").set(negativeTrust);
+ return this;
+ }
+
+ /**
+ * Returns the trust comment. This is the comment that is set in the web
+ * of trust when a trust value is assigned to an identity.
+ *
+ * @return The trust comment
+ */
+ public String getTrustComment() {
+ return options.getStringOption("TrustComment").get();
+ }
+
+ /**
+ * Sets the trust comment.
+ *
+ * @param trustComment
+ * The trust comment, or {@code null} to restore it to the
+ * default value
+ * @return This preferences
+ */
+ public Preferences setTrustComment(String trustComment) {
+ options.getStringOption("TrustComment").set(trustComment);
+ return this;
+ }
+
+ /**
+ * Returns whether the {@link FcpInterface FCP interface} is currently
+ * active.
+ *
+ * @see FcpInterface#setActive(boolean)
+ * @return {@code true} if the FCP interface is currently active,
+ * {@code false} otherwise
+ */
+ public boolean isFcpInterfaceActive() {
+ return options.getBooleanOption("ActivateFcpInterface").get();
+ }
+
+ /**
+ * Sets whether the {@link FcpInterface FCP interface} is currently
+ * active.
+ *
+ * @see FcpInterface#setActive(boolean)
+ * @param fcpInterfaceActive
+ * {@code true} to activate the FCP interface, {@code false}
+ * to deactivate the FCP interface
+ * @return This preferences object
+ */
+ public Preferences setFcpInterfaceActive(boolean fcpInterfaceActive) {
+ options.getBooleanOption("ActivateFcpInterface").set(fcpInterfaceActive);
+ return this;
+ }
+
+ /**
+ * Returns the action level for which full access to the FCP interface
+ * is required.
+ *
+ * @return The action level for which full access to the FCP interface
+ * is required
+ */
+ public FullAccessRequired getFcpFullAccessRequired() {
+ return FullAccessRequired.values()[options.getIntegerOption("FcpFullAccessRequired").get()];
+ }
+
+ /**
+ * Sets the action level for which full access to the FCP interface is
+ * required
+ *
+ * @param fcpFullAccessRequired
+ * The action level
+ * @return This preferences
+ */
+ public Preferences setFcpFullAccessRequired(FullAccessRequired fcpFullAccessRequired) {
+ options.getIntegerOption("FcpFullAccessRequired").set((fcpFullAccessRequired != null) ? fcpFullAccessRequired.ordinal() : null);
+ return this;
+ }
+
+ /**
+ * Returns whether Sone should clear its settings on the next restart.
+ * In order to be effective, {@link #isReallyClearOnNextRestart()} needs
+ * to return {@code true} as well!
+ *
+ * @return {@code true} if Sone should clear its settings on the next
+ * restart, {@code false} otherwise
+ */
+ public boolean isClearOnNextRestart() {
+ return options.getBooleanOption("ClearOnNextRestart").get();
+ }
+
+ /**
+ * Sets whether Sone will clear its settings on the next restart.
+ *
+ * @param clearOnNextRestart
+ * {@code true} if Sone should clear its settings on the next
+ * restart, {@code false} otherwise
+ * @return This preferences
+ */
+ public Preferences setClearOnNextRestart(Boolean clearOnNextRestart) {
+ options.getBooleanOption("ClearOnNextRestart").set(clearOnNextRestart);
+ return this;
+ }
+
+ /**
+ * Returns whether Sone should really clear its settings on next
+ * restart. This is a confirmation option that needs to be set in
+ * addition to {@link #isClearOnNextRestart()} in order to clear Sone’s
+ * settings on the next restart.
+ *
+ * @return {@code true} if Sone should really clear its settings on the
+ * next restart, {@code false} otherwise
+ */
+ public boolean isReallyClearOnNextRestart() {
+ return options.getBooleanOption("ReallyClearOnNextRestart").get();
+ }
+
+ /**
+ * Sets whether Sone should really clear its settings on the next
+ * restart.
+ *
+ * @param reallyClearOnNextRestart
+ * {@code true} if Sone should really clear its settings on
+ * the next restart, {@code false} otherwise
+ * @return This preferences
+ */
+ public Preferences setReallyClearOnNextRestart(Boolean reallyClearOnNextRestart) {
+ options.getBooleanOption("ReallyClearOnNextRestart").set(reallyClearOnNextRestart);
+ return this;
+ }
+
+ }
+