X-Git-Url: https://git.pterodactylus.net/?p=Sone.git;a=blobdiff_plain;f=src%2Fmain%2Fjava%2Fnet%2Fpterodactylus%2Fsone%2Fcore%2FCore.java;h=ea36aad76bf88c22ff6376495271d050c5133f61;hp=9b4828c791ccc807551c3f57b8a3bd55c622fda8;hb=bb2c8cb2d47a77bcef2d299ad4bf8e16f22a6198;hpb=a211a3bd360593efc9e7fba33d607a3b14d0a965 diff --git a/src/main/java/net/pterodactylus/sone/core/Core.java b/src/main/java/net/pterodactylus/sone/core/Core.java index 9b4828c..ea36aad 100644 --- a/src/main/java/net/pterodactylus/sone/core/Core.java +++ b/src/main/java/net/pterodactylus/sone/core/Core.java @@ -25,6 +25,9 @@ import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.Map.Entry; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import java.util.logging.Level; import java.util.logging.Logger; @@ -34,8 +37,11 @@ import net.pterodactylus.sone.core.Options.OptionWatcher; import net.pterodactylus.sone.data.Client; import net.pterodactylus.sone.data.Post; import net.pterodactylus.sone.data.Profile; +import net.pterodactylus.sone.data.Profile.Field; import net.pterodactylus.sone.data.Reply; import net.pterodactylus.sone.data.Sone; +import net.pterodactylus.sone.fcp.FcpInterface; +import net.pterodactylus.sone.fcp.FcpInterface.FullAccessRequired; import net.pterodactylus.sone.freenet.wot.Identity; import net.pterodactylus.sone.freenet.wot.IdentityListener; import net.pterodactylus.sone.freenet.wot.IdentityManager; @@ -47,6 +53,12 @@ import net.pterodactylus.util.config.Configuration; import net.pterodactylus.util.config.ConfigurationException; import net.pterodactylus.util.logging.Logging; import net.pterodactylus.util.number.Numbers; +import net.pterodactylus.util.thread.Ticker; +import net.pterodactylus.util.validation.EqualityValidator; +import net.pterodactylus.util.validation.IntegerRangeValidator; +import net.pterodactylus.util.validation.OrValidator; +import net.pterodactylus.util.validation.Validation; +import net.pterodactylus.util.version.Version; import freenet.keys.FreenetURI; /** @@ -54,7 +66,7 @@ import freenet.keys.FreenetURI; * * @author David ‘Bombe’ Roden */ -public class Core implements IdentityListener { +public class Core implements IdentityListener, UpdateListener, SoneProvider, PostProvider { /** * Enumeration for the possible states of a {@link Sone}. @@ -82,12 +94,18 @@ public class Core implements IdentityListener { /** The options. */ private final Options options = new Options(); + /** The preferences. */ + private final Preferences preferences = new Preferences(options); + /** The core listener manager. */ private final CoreListenerManager coreListenerManager = new CoreListenerManager(this); /** The configuration. */ private Configuration configuration; + /** Whether we’re currently saving the configuration. */ + private boolean storingConfiguration = false; + /** The identity manager. */ private final IdentityManager identityManager; @@ -97,7 +115,17 @@ public class Core implements IdentityListener { /** The Sone downloader. */ private final SoneDownloader soneDownloader; + /** Sone downloader thread-pool. */ + private final ExecutorService soneDownloaders = Executors.newFixedThreadPool(10); + + /** The update checker. */ + private final UpdateChecker updateChecker; + + /** The FCP interface. */ + private volatile FcpInterface fcpInterface; + /** Whether the core has been stopped. */ + @SuppressWarnings("unused") private volatile boolean stopped; /** The Sones’ statuses. */ @@ -112,6 +140,10 @@ public class Core implements IdentityListener { /* synchronize access on this on localSones. */ private final Map soneInserters = new HashMap(); + /** Sone rescuers. */ + /* synchronize access on this on localSones. */ + private final Map soneRescuers = new HashMap(); + /** All local Sones. */ /* synchronize access on this on itself. */ private Map localSones = new HashMap(); @@ -146,9 +178,16 @@ public class Core implements IdentityListener { /** All known replies. */ private Set knownReplies = new HashSet(); + /** All bookmarked posts. */ + /* synchronize access on itself. */ + private Set bookmarkedPosts = new HashSet(); + /** Trusted identities, sorted by own identities. */ private Map> trustedIdentities = Collections.synchronizedMap(new HashMap>()); + /** Ticker for threads that mark own elements as known. */ + private Ticker localElementTicker = new Ticker(); + /** * Creates a new core. * @@ -164,6 +203,7 @@ public class Core implements IdentityListener { this.freenetInterface = freenetInterface; this.identityManager = identityManager; this.soneDownloader = new SoneDownloader(this, freenetInterface); + this.updateChecker = new UpdateChecker(freenetInterface); } // @@ -211,27 +251,36 @@ public class Core implements IdentityListener { * * @return The options of the core */ - public Options getOptions() { - return options; + public Preferences getPreferences() { + return preferences; } /** - * Returns whether the “Sone rescue mode” is currently activated. + * Returns the identity manager used by the core. * - * @return {@code true} if the “Sone rescue mode” is currently activated, - * {@code false} if it is not + * @return The identity manager */ - public boolean isSoneRescueMode() { - return options.getBooleanOption("SoneRescueMode").get(); + public IdentityManager getIdentityManager() { + return identityManager; } /** - * Returns the identity manager used by the core. + * Returns the update checker. * - * @return The identity manager + * @return The update checker */ - public IdentityManager getIdentityManager() { - return identityManager; + public UpdateChecker getUpdateChecker() { + return updateChecker; + } + + /** + * Sets the FCP interface to use. + * + * @param fcpInterface + * The FCP interface to use + */ + public void setFcpInterface(FcpInterface fcpInterface) { + this.fcpInterface = fcpInterface; } /** @@ -262,6 +311,26 @@ public class Core implements IdentityListener { } /** + * Returns the Sone rescuer for the given local Sone. + * + * @param sone + * The local Sone to get the rescuer for + * @return The Sone rescuer for the given Sone + */ + public SoneRescuer getSoneRescuer(Sone sone) { + Validation.begin().isNotNull("Sone", sone).check().is("Local Sone", isLocalSone(sone)).check(); + synchronized (localSones) { + SoneRescuer soneRescuer = soneRescuers.get(sone); + if (soneRescuer == null) { + soneRescuer = new SoneRescuer(this, soneDownloader, sone); + soneRescuers.put(sone, soneRescuer); + soneRescuer.start(); + } + return soneRescuer; + } + } + + /** * Returns whether the given Sone is currently locked. * * @param sone @@ -312,6 +381,7 @@ public class Core implements IdentityListener { * @return The Sone with the given ID, or {@code null} if there is no such * Sone */ + @Override public Sone getSone(String id, boolean create) { if (isLocalSone(id)) { return getLocalSone(id); @@ -396,6 +466,7 @@ public class Core implements IdentityListener { if ((sone == null) && create) { sone = new Sone(id); localSones.put(id, sone); + setSoneStatus(sone, SoneStatus.unknown); } return sone; } @@ -439,6 +510,7 @@ public class Core implements IdentityListener { if ((sone == null) && create) { sone = new Sone(id); remoteSones.put(id, sone); + setSoneStatus(sone, SoneStatus.unknown); } return sone; } @@ -473,22 +545,15 @@ public class Core implements IdentityListener { } /** - * Returns whether the given Sone is a new Sone. After this check, the Sone - * is marked as known, i.e. a second call with the same parameters will - * always yield {@code false}. + * Returns whether the Sone with the given ID is a new Sone. * - * @param sone - * The sone to check for + * @param soneId + * The ID of the sone to check for * @return {@code true} if the given Sone is new, false otherwise */ - public boolean isNewSone(Sone sone) { + public boolean isNewSone(String soneId) { synchronized (newSones) { - boolean isNew = !knownSones.contains(sone.getId()) && newSones.remove(sone.getId()); - knownSones.add(sone.getId()); - if (isNew) { - coreListenerManager.fireMarkSoneKnown(sone); - } - return isNew; + return !knownSones.contains(soneId) && newSones.contains(soneId); } } @@ -514,7 +579,8 @@ public class Core implements IdentityListener { * @return {@code true} if the target Sone is trusted by the origin Sone */ public boolean isSoneTrusted(Sone origin, Sone target) { - return trustedIdentities.containsKey(origin) && trustedIdentities.get(origin.getIdentity()).contains(target); + Validation.begin().isNotNull("Origin", origin).isNotNull("Target", target).check().isInstanceOf("Origin’s OwnIdentity", origin.getIdentity(), OwnIdentity.class).check(); + return trustedIdentities.containsKey(origin.getIdentity()) && trustedIdentities.get(origin.getIdentity()).contains(target.getIdentity()); } /** @@ -522,7 +588,7 @@ public class Core implements IdentityListener { * * @param postId * The ID of the post to get - * @return The post, or {@code null} if there is no such post + * @return The post with the given ID, or a new post with the given ID */ public Post getPost(String postId) { return getPost(postId, true); @@ -538,6 +604,7 @@ public class Core implements IdentityListener { * exists, {@code false} to return {@code null} * @return The post, or {@code null} if there is no such post */ + @Override public Post getPost(String postId, boolean create) { synchronized (posts) { Post post = posts.get(postId); @@ -550,8 +617,7 @@ public class Core implements IdentityListener { } /** - * Returns whether the given post ID is new. After this method returns it is - * marked a known post ID. + * Returns whether the given post ID is new. * * @param postId * The post ID @@ -559,33 +625,30 @@ public class Core implements IdentityListener { * otherwise */ public boolean isNewPost(String postId) { - return isNewPost(postId, true); + synchronized (newPosts) { + return !knownPosts.contains(postId) && newPosts.contains(postId); + } } /** - * Returns whether the given post ID is new. If {@code markAsKnown} is - * {@code true} then after this method returns the post ID is marked a known - * post ID. + * Returns all posts that have the given Sone as recipient. * - * @param postId - * The post ID - * @param markAsKnown - * {@code true} to mark the post ID as known, {@code false} to - * not to mark it as known - * @return {@code true} if the post is considered to be new, {@code false} - * otherwise + * @see Post#getRecipient() + * @param recipient + * The recipient of the posts + * @return All posts that have the given Sone as recipient */ - public boolean isNewPost(String postId, boolean markAsKnown) { - synchronized (newPosts) { - boolean isNew = !knownPosts.contains(postId) && newPosts.contains(postId); - if (markAsKnown) { - Post post = getPost(postId, false); - if (post != null) { - markPostKnown(post); + public Set getDirectedPosts(Sone recipient) { + Validation.begin().isNotNull("Recipient", recipient).check(); + Set directedPosts = new HashSet(); + synchronized (posts) { + for (Post post : posts.values()) { + if (recipient.equals(post.getRecipient())) { + directedPosts.add(post); } } - return isNew; } + return directedPosts; } /** @@ -653,30 +716,8 @@ public class Core implements IdentityListener { * otherwise */ public boolean isNewReply(String replyId) { - return isNewReply(replyId, true); - } - - /** - * Returns whether the reply with the given ID is new. - * - * @param replyId - * The ID of the reply to check - * @param markAsKnown - * {@code true} to mark the reply as known, {@code false} to not - * to mark it as known - * @return {@code true} if the reply is considered to be new, {@code false} - * otherwise - */ - public boolean isNewReply(String replyId, boolean markAsKnown) { synchronized (newReplies) { - boolean isNew = !knownReplies.contains(replyId) && newReplies.contains(replyId); - if (markAsKnown) { - Reply reply = getReply(replyId, false); - if (reply != null) { - markReplyKnown(reply); - } - } - return isNew; + return !knownReplies.contains(replyId) && newReplies.contains(replyId); } } @@ -714,6 +755,50 @@ public class Core implements IdentityListener { return sones; } + /** + * Returns whether the given post is bookmarked. + * + * @param post + * The post to check + * @return {@code true} if the given post is bookmarked, {@code false} + * otherwise + */ + public boolean isBookmarked(Post post) { + return isPostBookmarked(post.getId()); + } + + /** + * Returns whether the post with the given ID is bookmarked. + * + * @param id + * The ID of the post to check + * @return {@code true} if the post with the given ID is bookmarked, + * {@code false} otherwise + */ + public boolean isPostBookmarked(String id) { + synchronized (bookmarkedPosts) { + return bookmarkedPosts.contains(id); + } + } + + /** + * Returns all currently known bookmarked posts. + * + * @return All bookmarked posts + */ + public Set getBookmarkedPosts() { + Set posts = new HashSet(); + synchronized (bookmarkedPosts) { + for (String bookmarkedPostId : bookmarkedPosts) { + Post post = getPost(bookmarkedPostId, false); + if (post != null) { + posts.add(post); + } + } + } + return posts; + } + // // ACTIONS // @@ -800,34 +885,7 @@ public class Core implements IdentityListener { soneInserters.put(sone, soneInserter); setSoneStatus(sone, SoneStatus.idle); loadSone(sone); - if (!isSoneRescueMode()) { - soneInserter.start(); - } - new Thread(new Runnable() { - - @Override - @SuppressWarnings("synthetic-access") - public void run() { - if (!isSoneRescueMode()) { - soneDownloader.fetchSone(sone); - return; - } - logger.log(Level.INFO, "Trying to restore Sone from Freenet…"); - coreListenerManager.fireRescuingSone(sone); - lockSone(sone); - long edition = sone.getLatestEdition(); - while (!stopped && (edition >= 0) && isSoneRescueMode()) { - logger.log(Level.FINE, "Downloading edition " + edition + "…"); - soneDownloader.fetchSone(sone, sone.getRequestUri().setKeyType("SSK").setDocName("Sone-" + edition)); - --edition; - } - logger.log(Level.INFO, "Finished restoring Sone from Freenet, starting Inserter…"); - saveSone(sone); - coreListenerManager.fireRescuedSone(sone); - soneInserter.start(); - } - - }, "Sone Downloader").start(); + soneInserter.start(); return sone; } } @@ -847,6 +905,9 @@ public class Core implements IdentityListener { return null; } Sone sone = addLocalSone(ownIdentity); + sone.getOptions().addBooleanOption("AutoFollow", new DefaultOption(false)); + sone.addFriend("nwa8lHa271k2QvJ8aa0Ov7IHAV-DFOCFgmDt3X6BpCI"); + saveSone(sone); return sone; } @@ -876,27 +937,35 @@ public class Core implements IdentityListener { } if (newSone) { coreListenerManager.fireNewSoneFound(sone); + for (Sone localSone : getLocalSones()) { + if (localSone.getOptions().getBooleanOption("AutoFollow").get()) { + localSone.addFriend(sone.getId()); + saveSone(localSone); + } + } } } remoteSones.put(identity.getId(), sone); soneDownloader.addSone(sone); setSoneStatus(sone, SoneStatus.unknown); - new Thread(new Runnable() { + soneDownloaders.execute(new Runnable() { @Override @SuppressWarnings("synthetic-access") public void run() { - soneDownloader.fetchSone(sone); + soneDownloader.fetchSone(sone, sone.getRequestUri()); } - }, "Sone Downloader").start(); + }); return sone; } } /** - * Retrieves the trust relationship from the origin to the target. + * Retrieves the trust relationship from the origin to the target. If the + * trust relationship can not be retrieved, {@code null} is returned. * + * @see Identity#getTrust(OwnIdentity) * @param origin * The origin of the trust tree * @param target @@ -908,23 +977,104 @@ public class Core implements IdentityListener { logger.log(Level.WARNING, "Tried to get trust from remote Sone: %s", origin); return null; } + return target.getIdentity().getTrust((OwnIdentity) origin.getIdentity()); + } + + /** + * 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) { + Validation.begin().isNotNull("Trust Origin", origin).check().isInstanceOf("Trust Origin", origin.getIdentity(), OwnIdentity.class).isNotNull("Trust Target", target).isLessOrEqual("Trust Value", trustValue, 100).isGreaterOrEqual("Trust Value", trustValue, -100).check(); try { - return target.getIdentity().getTrust((OwnIdentity) origin.getIdentity()); + ((OwnIdentity) origin.getIdentity()).setTrust(target.getIdentity(), trustValue, preferences.getTrustComment()); } catch (WebOfTrustException wote1) { - logger.log(Level.WARNING, "Could not get trust for Sone: " + target, wote1); - return null; + logger.log(Level.WARNING, "Could not set trust for Sone: " + target, wote1); } } /** - * Updates the stores Sone with the given Sone. + * 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) { + Validation.begin().isNotNull("Trust Origin", origin).isNotNull("Trust Target", target).check().isInstanceOf("Trust Origin Identity", origin.getIdentity(), OwnIdentity.class).check(); + try { + ((OwnIdentity) origin.getIdentity()).removeTrust(target.getIdentity()); + } catch (WebOfTrustException wote1) { + logger.log(Level.WARNING, "Could not remove trust for Sone: " + target, wote1); + } + } + + /** + * 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(Sone sone, boolean soneRescueMode) { if (hasSone(sone.getId())) { - boolean soneRescueMode = isLocalSone(sone) && isSoneRescueMode(); Sone storedSone = getSone(sone.getId()); if (!soneRescueMode && !(sone.getTime() > storedSone.getTime())) { logger.log(Level.FINE, "Downloaded Sone %s is not newer than stored Sone %s.", new Object[] { sone, storedSone }); @@ -939,10 +1089,11 @@ public class Core implements IdentityListener { } } } + List storedPosts = storedSone.getPosts(); synchronized (newPosts) { for (Post post : sone.getPosts()) { - post.setSone(getSone(post.getSone().getId())); - if (!storedSone.getPosts().contains(post) && !knownPosts.contains(post.getId())) { + post.setSone(storedSone); + if (!storedPosts.contains(post) && !knownPosts.contains(post.getId())) { newPosts.add(post.getId()); coreListenerManager.fireNewPostFound(post); } @@ -959,10 +1110,11 @@ public class Core implements IdentityListener { } } } + Set storedReplies = storedSone.getReplies(); synchronized (newReplies) { for (Reply reply : sone.getReplies()) { - reply.setSone(getSone(reply.getSone().getId())); - if (!storedSone.getReplies().contains(reply) && !knownReplies.contains(reply.getId())) { + reply.setSone(storedSone); + if (!storedReplies.contains(reply) && !knownReplies.contains(reply.getId())) { newReplies.add(reply.getId()); coreListenerManager.fireNewReplyFound(reply); } @@ -1035,6 +1187,23 @@ public class Core implements IdentityListener { } /** + * Marks the given Sone as known. If the Sone was {@link #isNewPost(String) + * new} before, a {@link CoreListener#markSoneKnown(Sone)} event is fired. + * + * @param sone + * The Sone to mark as known + */ + public void markSoneKnown(Sone sone) { + synchronized (newSones) { + if (newSones.remove(sone.getId())) { + knownSones.add(sone.getId()); + coreListenerManager.fireMarkSoneKnown(sone); + saveConfiguration(); + } + } + } + + /** * Loads and updates the given Sone from the configuration. If any error is * encountered, loading is aborted and the given Sone is not changed. * @@ -1047,6 +1216,9 @@ public class Core implements IdentityListener { return; } + /* initialize options. */ + sone.getOptions().addBooleanOption("AutoFollow", new DefaultOption(false)); + /* load Sone. */ String sonePrefix = "Sone/" + sone.getId(); Long soneTime = configuration.getLongValue(sonePrefix + "/Time").getValue(null); @@ -1065,6 +1237,17 @@ public class Core implements IdentityListener { 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 posts = new HashSet(); while (true) { @@ -1135,6 +1318,9 @@ public class Core implements IdentityListener { friends.add(friendId); } + /* load options. */ + sone.getOptions().getBooleanOption("AutoFollow").set(configuration.getBooleanValue(sonePrefix + "/Options/AutoFollow").getValue(null)); + /* if we’re still here, Sone was loaded successfully. */ synchronized (sone) { sone.setTime(soneTime); @@ -1198,6 +1384,15 @@ public class Core implements IdentityListener { configuration.getIntValue(sonePrefix + "/Profile/BirthMonth").setValue(profile.getBirthMonth()); configuration.getIntValue(sonePrefix + "/Profile/BirthYear").setValue(profile.getBirthYear()); + /* 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()) { @@ -1241,6 +1436,9 @@ public class Core implements IdentityListener { } configuration.getStringValue(sonePrefix + "/Friends/" + friendCounter + "/ID").setValue(null); + /* save options. */ + configuration.getBooleanValue(sonePrefix + "/Options/AutoFollow").setValue(sone.getOptions().getBooleanOption("AutoFollow").getReal()); + configuration.save(); logger.log(Level.INFO, "Sone %s saved.", sone); } catch (ConfigurationException ce1) { @@ -1313,7 +1511,7 @@ public class Core implements IdentityListener { logger.log(Level.FINE, "Tried to create post for non-local Sone: %s", sone); return null; } - Post post = new Post(sone, time, text); + final Post post = new Post(sone, time, text); if (recipient != null) { post.setRecipient(recipient); } @@ -1321,10 +1519,21 @@ public class Core implements IdentityListener { posts.put(post.getId(), post); } synchronized (newPosts) { - knownPosts.add(post.getId()); + newPosts.add(post.getId()); + coreListenerManager.fireNewPostFound(post); } sone.addPost(post); saveSone(sone); + localElementTicker.registerEvent(System.currentTimeMillis() + 10 * 1000, new Runnable() { + + /** + * {@inheritDoc} + */ + @Override + public void run() { + markPostKnown(post); + } + }, "Mark " + post + " read."); return post; } @@ -1343,6 +1552,11 @@ public class Core implements IdentityListener { synchronized (posts) { posts.remove(post.getId()); } + coreListenerManager.firePostRemoved(post); + synchronized (newPosts) { + markPostKnown(post); + knownPosts.remove(post.getId()); + } saveSone(post.getSone()); } @@ -1364,6 +1578,50 @@ public class Core implements IdentityListener { } /** + * 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 @@ -1396,15 +1654,26 @@ public class Core implements IdentityListener { logger.log(Level.FINE, "Tried to create reply for non-local Sone: %s", sone); return null; } - Reply reply = new Reply(sone, post, System.currentTimeMillis(), text); + final Reply reply = new Reply(sone, post, System.currentTimeMillis(), text); synchronized (replies) { replies.put(reply.getId(), reply); } synchronized (newReplies) { - knownReplies.add(reply.getId()); + newReplies.add(reply.getId()); + coreListenerManager.fireNewReplyFound(reply); } sone.addReply(reply); saveSone(sone); + localElementTicker.registerEvent(System.currentTimeMillis() + 10 * 1000, new Runnable() { + + /** + * {@inheritDoc} + */ + @Override + public void run() { + markReplyKnown(reply); + } + }, "Mark " + reply + " read."); return reply; } @@ -1423,6 +1692,10 @@ public class Core implements IdentityListener { synchronized (replies) { replies.remove(reply.getId()); } + synchronized (newReplies) { + markReplyKnown(reply); + knownReplies.remove(reply.getId()); + } sone.removeReply(reply); saveSone(sone); } @@ -1449,6 +1722,8 @@ public class Core implements IdentityListener { */ public void start() { loadConfiguration(); + updateChecker.addUpdateListener(this); + updateChecker.start(); } /** @@ -1459,7 +1734,12 @@ public class Core implements IdentityListener { for (SoneInserter soneInserter : soneInserters.values()) { soneInserter.stop(); } + for (Sone localSone : localSones.values()) { + saveSone(localSone); + } } + updateChecker.stop(); + updateChecker.removeUpdateListener(this); soneDownloader.stop(); saveConfiguration(); stopped = true; @@ -1468,10 +1748,27 @@ public class Core implements IdentityListener { /** * Saves the current options. */ - public synchronized void saveConfiguration() { + public 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.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()); @@ -1503,11 +1800,24 @@ public class Core implements IdentityListener { 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; + } } } @@ -1521,7 +1831,7 @@ public class Core implements IdentityListener { @SuppressWarnings("unchecked") private void loadConfiguration() { /* create options. */ - options.addIntegerOption("InsertionDelay", new DefaultOption(60, new OptionWatcher() { + options.addIntegerOption("InsertionDelay", new DefaultOption(60, new IntegerRangeValidator(0, Integer.MAX_VALUE), new OptionWatcher() { @Override public void optionChanged(Option option, Integer oldValue, Integer newValue) { @@ -1529,6 +1839,29 @@ public class Core implements IdentityListener { } })); + options.addIntegerOption("PostsPerPage", new DefaultOption(10, new IntegerRangeValidator(1, Integer.MAX_VALUE))); + options.addIntegerOption("CharactersPerPost", new DefaultOption(200, new OrValidator(new IntegerRangeValidator(50, Integer.MAX_VALUE), new EqualityValidator(-1)))); + options.addBooleanOption("RequireFullAccess", new DefaultOption(false)); + options.addIntegerOption("PositiveTrust", new DefaultOption(75, new IntegerRangeValidator(0, 100))); + options.addIntegerOption("NegativeTrust", new DefaultOption(-25, new IntegerRangeValidator(-100, 100))); + options.addStringOption("TrustComment", new DefaultOption("Set from Sone Web Interface")); + options.addBooleanOption("ActivateFcpInterface", new DefaultOption(false, new OptionWatcher() { + + @Override + @SuppressWarnings("synthetic-access") + public void optionChanged(Option option, Boolean oldValue, Boolean newValue) { + fcpInterface.setActive(newValue); + } + })); + options.addIntegerOption("FcpFullAccessRequired", new DefaultOption(2, new OptionWatcher() { + + @Override + @SuppressWarnings("synthetic-access") + public void optionChanged(Option option, Integer oldValue, Integer newValue) { + fcpInterface.setFullAccessRequired(FullAccessRequired.values()[newValue]); + } + + })); options.addBooleanOption("SoneRescueMode", new DefaultOption(false)); options.addBooleanOption("ClearOnNextRestart", new DefaultOption(false)); options.addBooleanOption("ReallyClearOnNextRestart", new DefaultOption(false)); @@ -1544,7 +1877,15 @@ public class Core implements IdentityListener { return; } - options.getIntegerOption("InsertionDelay").set(configuration.getIntValue("Option/InsertionDelay").getValue(null)); + loadConfigurationValue("InsertionDelay"); + loadConfigurationValue("PostsPerPage"); + loadConfigurationValue("CharactersPerPost"); + 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. */ @@ -1583,6 +1924,33 @@ public class Core implements IdentityListener { } } + /* 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."); + } } /** @@ -1648,6 +2016,9 @@ public class Core implements IdentityListener { @SuppressWarnings("synthetic-access") public void run() { Sone sone = getRemoteSone(identity.getId()); + sone.setIdentity(identity); + sone.setLatestEdition(Numbers.safeParseLong(identity.getProperty("Sone.LatestEdition"), sone.getLatestEdition())); + soneDownloader.addSone(sone); soneDownloader.fetchSone(sone); } }).start(); @@ -1659,6 +2030,401 @@ public class Core implements IdentityListener { @Override public void identityRemoved(OwnIdentity ownIdentity, Identity identity) { trustedIdentities.get(ownIdentity).remove(identity); + boolean foundIdentity = false; + for (Entry> 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 (newPosts) { + for (Post post : sone.getPosts()) { + posts.remove(post.getId()); + newPosts.remove(post.getId()); + coreListenerManager.firePostRemoved(post); + } + } + } + synchronized (replies) { + synchronized (newReplies) { + for (Reply reply : sone.getReplies()) { + replies.remove(reply.getId()); + newReplies.remove(reply.getId()); + coreListenerManager.fireReplyRemoved(reply); + } + } + } + synchronized (remoteSones) { + remoteSones.remove(identity.getId()); + } + synchronized (newSones) { + newSones.remove(identity.getId()); + coreListenerManager.fireSoneRemoved(sone); + } + } + + // + // INTERFACE UpdateListener + // + + /** + * {@inheritDoc} + */ + @Override + public void updateFound(Version version, long releaseTime, long latestEdition) { + coreListenerManager.fireUpdateFound(version, releaseTime, latestEdition); + } + + /** + * Convenience interface for external classes that want to access the core’s + * configuration. + * + * @author David ‘Bombe’ Roden + */ + 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 -1 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 -1 to + * not cut off the posts + * @return This preferences objects + */ + public Preferences setCharactersPerPost(Integer charactersPerPost) { + options.getIntegerOption("CharactersPerPost").set(charactersPerPost); + 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; + } + } }