X-Git-Url: https://git.pterodactylus.net/?a=blobdiff_plain;ds=inline;f=src%2Fmain%2Fjava%2Fnet%2Fpterodactylus%2Fsone%2Fcore%2FCore.java;h=b612c740a5a07588546436bf42009e5eba79b980;hb=c21a3eda7b784f1c5b39ed12a5ce983d2bf62166;hp=3c99eef26708a6b027a0277c886fc13c4a562e72;hpb=74223530641d7581c7d8f572e98eff1a01946ebd;p=Sone.git
diff --git a/src/main/java/net/pterodactylus/sone/core/Core.java b/src/main/java/net/pterodactylus/sone/core/Core.java
index 3c99eef..b612c74 100644
--- a/src/main/java/net/pterodactylus/sone/core/Core.java
+++ b/src/main/java/net/pterodactylus/sone/core/Core.java
@@ -34,12 +34,16 @@ import java.util.logging.Logger;
import net.pterodactylus.sone.core.Options.DefaultOption;
import net.pterodactylus.sone.core.Options.Option;
import net.pterodactylus.sone.core.Options.OptionWatcher;
+import net.pterodactylus.sone.data.Album;
import net.pterodactylus.sone.data.Client;
+import net.pterodactylus.sone.data.Image;
import net.pterodactylus.sone.data.Post;
+import net.pterodactylus.sone.data.PostReply;
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.data.TemporaryImage;
+import net.pterodactylus.sone.data.Profile.Field;
import net.pterodactylus.sone.fcp.FcpInterface;
import net.pterodactylus.sone.fcp.FcpInterface.FullAccessRequired;
import net.pterodactylus.sone.freenet.wot.Identity;
@@ -49,16 +53,17 @@ import net.pterodactylus.sone.freenet.wot.OwnIdentity;
import net.pterodactylus.sone.freenet.wot.Trust;
import net.pterodactylus.sone.freenet.wot.WebOfTrustException;
import net.pterodactylus.sone.main.SonePlugin;
-import net.pterodactylus.util.collection.Pair;
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.service.AbstractService;
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.client.FetchResult;
import freenet.keys.FreenetURI;
/**
@@ -66,7 +71,7 @@ import freenet.keys.FreenetURI;
*
* @author David âBombeâ Roden
*/
-public class Core implements IdentityListener, UpdateListener, SoneProvider, PostProvider {
+public class Core extends AbstractService implements IdentityListener, UpdateListener, SoneProvider, PostProvider, SoneInsertListener, ImageInsertListener {
/**
* Enumeration for the possible states of a {@link Sone}.
@@ -115,6 +120,9 @@ public class Core implements IdentityListener, UpdateListener, SoneProvider, Pos
/** The Sone downloader. */
private final SoneDownloader soneDownloader;
+ /** The image inserter. */
+ private final ImageInserter imageInserter;
+
/** Sone downloader thread-pool. */
private final ExecutorService soneDownloaders = Executors.newFixedThreadPool(10);
@@ -124,13 +132,13 @@ public class Core implements IdentityListener, UpdateListener, SoneProvider, Pos
/** The FCP interface. */
private volatile FcpInterface fcpInterface;
- /** Whether the core has been stopped. */
- private volatile boolean stopped;
-
/** The Sonesâ statuses. */
/* synchronize access on itself. */
private final Map soneStatuses = new HashMap();
+ /** The times Sones were followed. */
+ private final Map soneFollowingTimes = new HashMap();
+
/** Locked local Sones. */
/* synchronize on itself. */
private final Set lockedSones = new HashSet();
@@ -139,6 +147,10 @@ public class Core implements IdentityListener, UpdateListener, SoneProvider, Pos
/* 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();
@@ -165,7 +177,7 @@ public class Core implements IdentityListener, UpdateListener, SoneProvider, Pos
private Set knownPosts = new HashSet();
/** All replies. */
- private Map replies = new HashMap();
+ private Map replies = new HashMap();
/** All new replies. */
private Set newReplies = new HashSet();
@@ -180,9 +192,21 @@ public class Core implements IdentityListener, UpdateListener, SoneProvider, Pos
/** Trusted identities, sorted by own identities. */
private Map> trustedIdentities = Collections.synchronizedMap(new HashMap>());
+ /** All known albums. */
+ private Map albums = new HashMap();
+
+ /** All known images. */
+ private Map images = new HashMap();
+
+ /** All temporary images. */
+ private Map temporaryImages = new HashMap();
+
/** Ticker for threads that mark own elements as known. */
private Ticker localElementTicker = new Ticker();
+ /** The time the configuration was last touched. */
+ private volatile long lastConfigurationUpdate;
+
/**
* Creates a new core.
*
@@ -194,10 +218,12 @@ public class Core implements IdentityListener, UpdateListener, SoneProvider, Pos
* The identity manager
*/
public Core(Configuration configuration, FreenetInterface freenetInterface, IdentityManager identityManager) {
+ super("Sone Core");
this.configuration = configuration;
this.freenetInterface = freenetInterface;
this.identityManager = identityManager;
this.soneDownloader = new SoneDownloader(this, freenetInterface);
+ this.imageInserter = new ImageInserter(this, freenetInterface);
this.updateChecker = new UpdateChecker(freenetInterface);
}
@@ -238,7 +264,7 @@ public class Core implements IdentityListener, UpdateListener, SoneProvider, Pos
*/
public void setConfiguration(Configuration configuration) {
this.configuration = configuration;
- saveConfiguration();
+ touchConfiguration();
}
/**
@@ -306,6 +332,26 @@ public class Core implements IdentityListener, UpdateListener, SoneProvider, Pos
}
/**
+ * 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
@@ -463,17 +509,6 @@ public class Core implements IdentityListener, UpdateListener, SoneProvider, Pos
*
* @param id
* The ID of the remote Sone to get
- * @return The Sone with the given ID
- */
- public Sone getRemoteSone(String id) {
- return getRemoteSone(id, true);
- }
-
- /**
- * Returns the remote Sone with the given ID.
- *
- * @param id
- * The ID of the remote Sone to get
* @param create
* {@code true} to always create a Sone, {@code false} to return
* {@code null} if no Sone with the given ID exists
@@ -482,7 +517,7 @@ public class Core implements IdentityListener, UpdateListener, SoneProvider, Pos
public Sone getRemoteSone(String id, boolean create) {
synchronized (remoteSones) {
Sone sone = remoteSones.get(id);
- if ((sone == null) && create) {
+ if ((sone == null) && create && (id != null) && (id.length() == 43)) {
sone = new Sone(id);
remoteSones.put(id, sone);
setSoneStatus(sone, SoneStatus.unknown);
@@ -545,6 +580,23 @@ public class Core implements IdentityListener, UpdateListener, SoneProvider, Pos
}
/**
+ * Returns the time when the given was first followed by any local Sone.
+ *
+ * @param sone
+ * The Sone to get the time for
+ * @return The time (in milliseconds since Jan 1, 1970) the Sone has first
+ * been followed, or {@link Long#MAX_VALUE}
+ */
+ public long getSoneFollowingTime(Sone sone) {
+ synchronized (soneFollowingTimes) {
+ if (soneFollowingTimes.containsKey(sone)) {
+ return soneFollowingTimes.get(sone);
+ }
+ return Long.MAX_VALUE;
+ }
+ }
+
+ /**
* Returns whether the target Sone is trusted by the origin Sone.
*
* @param origin
@@ -634,7 +686,7 @@ public class Core implements IdentityListener, UpdateListener, SoneProvider, Pos
* The ID of the reply to get
* @return The reply
*/
- public Reply getReply(String replyId) {
+ public PostReply getReply(String replyId) {
return getReply(replyId, true);
}
@@ -650,11 +702,11 @@ public class Core implements IdentityListener, UpdateListener, SoneProvider, Pos
* to return {@code null} if no reply can be found
* @return The reply, or {@code null} if there is no such reply
*/
- public Reply getReply(String replyId, boolean create) {
+ public PostReply getReply(String replyId, boolean create) {
synchronized (replies) {
- Reply reply = replies.get(replyId);
+ PostReply reply = replies.get(replyId);
if (create && (reply == null)) {
- reply = new Reply(replyId);
+ reply = new PostReply(replyId);
replies.put(replyId, reply);
}
return reply;
@@ -668,11 +720,11 @@ public class Core implements IdentityListener, UpdateListener, SoneProvider, Pos
* The post to get all replies for
* @return All replies for the given post
*/
- public List getReplies(Post post) {
+ public List getReplies(Post post) {
Set sones = getSones();
- List replies = new ArrayList();
+ List replies = new ArrayList();
for (Sone sone : sones) {
- for (Reply reply : sone.getReplies()) {
+ for (PostReply reply : sone.getReplies()) {
if (reply.getPost().equals(post)) {
replies.add(reply);
}
@@ -720,7 +772,7 @@ public class Core implements IdentityListener, UpdateListener, SoneProvider, Pos
* The reply to get the liking Sones for
* @return The Sones that like the given reply
*/
- public Set getLikes(Reply reply) {
+ public Set getLikes(PostReply reply) {
Set sones = new HashSet();
for (Sone sone : getSones()) {
if (sone.getLikedReplyIds().contains(reply.getId())) {
@@ -774,6 +826,89 @@ public class Core implements IdentityListener, UpdateListener, SoneProvider, Pos
return posts;
}
+ /**
+ * Returns the album with the given ID, creating a new album if no album
+ * with the given ID can be found.
+ *
+ * @param albumId
+ * The ID of the album
+ * @return The album with the given ID
+ */
+ public Album getAlbum(String albumId) {
+ return getAlbum(albumId, true);
+ }
+
+ /**
+ * Returns the album with the given ID, optionally creating a new album if
+ * an album with the given ID can not be found.
+ *
+ * @param albumId
+ * The ID of the album
+ * @param create
+ * {@code true} to create a new album if none exists for the
+ * given ID
+ * @return The album with the given ID, or {@code null} if no album with the
+ * given ID exists and {@code create} is {@code false}
+ */
+ public Album getAlbum(String albumId, boolean create) {
+ synchronized (albums) {
+ Album album = albums.get(albumId);
+ if (create && (album == null)) {
+ album = new Album(albumId);
+ albums.put(albumId, album);
+ }
+ return album;
+ }
+ }
+
+ /**
+ * Returns the image with the given ID, creating it if necessary.
+ *
+ * @param imageId
+ * The ID of the image
+ * @return The image with the given ID
+ */
+ public Image getImage(String imageId) {
+ return getImage(imageId, true);
+ }
+
+ /**
+ * Returns the image with the given ID, optionally creating it if it does
+ * not exist.
+ *
+ * @param imageId
+ * The ID of the image
+ * @param create
+ * {@code true} to create an image if none exists with the given
+ * ID
+ * @return The image with the given ID, or {@code null} if none exists and
+ * none was created
+ */
+ public Image getImage(String imageId, boolean create) {
+ synchronized (images) {
+ Image image = images.get(imageId);
+ if (create && (image == null)) {
+ image = new Image(imageId);
+ images.put(imageId, image);
+ }
+ return image;
+ }
+ }
+
+ /**
+ * Returns the temporary image with the given ID.
+ *
+ * @param imageId
+ * The ID of the temporary image
+ * @return The temporary image, or {@code null} if there is no temporary
+ * image with the given ID
+ */
+ public TemporaryImage getTemporaryImage(String imageId) {
+ synchronized (temporaryImages) {
+ return temporaryImages.get(imageId);
+ }
+ }
+
//
// ACTIONS
//
@@ -810,29 +945,6 @@ public class Core implements IdentityListener, UpdateListener, SoneProvider, Pos
}
/**
- * Adds a local Sone from the given ID which has to be the ID of an own
- * identity.
- *
- * @param id
- * The ID of an own identity to add a Sone for
- * @return The added (or already existing) Sone
- */
- public Sone addLocalSone(String id) {
- synchronized (localSones) {
- if (localSones.containsKey(id)) {
- logger.log(Level.FINE, "Tried to add known local Sone: %s", id);
- return localSones.get(id);
- }
- OwnIdentity ownIdentity = identityManager.getOwnIdentity(id);
- if (ownIdentity == null) {
- logger.log(Level.INFO, "Invalid Sone ID: %s", id);
- return null;
- }
- return addLocalSone(ownIdentity);
- }
- }
-
- /**
* Adds a local Sone from the given own identity.
*
* @param ownIdentity
@@ -857,44 +969,11 @@ public class Core implements IdentityListener, UpdateListener, SoneProvider, Pos
/* TODO - load posts ân stuff */
localSones.put(ownIdentity.getId(), sone);
final SoneInserter soneInserter = new SoneInserter(this, freenetInterface, sone);
+ soneInserter.addSoneInsertListener(this);
soneInserters.put(sone, soneInserter);
setSoneStatus(sone, SoneStatus.idle);
loadSone(sone);
- if (!preferences.isSoneRescueMode()) {
- soneInserter.start();
- }
- new Thread(new Runnable() {
-
- @Override
- @SuppressWarnings("synthetic-access")
- public void run() {
- if (!preferences.isSoneRescueMode()) {
- return;
- }
- logger.log(Level.INFO, "Trying to restore Sone from Freenetâ¦");
- coreListenerManager.fireRescuingSone(sone);
- lockSone(sone);
- long edition = sone.getLatestEdition();
- /* find the latest edition the node knows about. */
- Pair currentUri = freenetInterface.fetchUri(sone.getRequestUri());
- if (currentUri != null) {
- long currentEdition = currentUri.getLeft().getEdition();
- if (currentEdition > edition) {
- edition = currentEdition;
- }
- }
- while (!stopped && (edition >= 0) && preferences.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;
}
}
@@ -915,8 +994,12 @@ public class Core implements IdentityListener, UpdateListener, SoneProvider, Pos
}
Sone sone = addLocalSone(ownIdentity);
sone.getOptions().addBooleanOption("AutoFollow", new DefaultOption(false));
- sone.addFriend("nwa8lHa271k2QvJ8aa0Ov7IHAV-DFOCFgmDt3X6BpCI");
- saveSone(sone);
+ sone.getOptions().addBooleanOption("EnableSoneInsertNotifications", new DefaultOption(false));
+ sone.getOptions().addBooleanOption("ShowNotification/NewSones", new DefaultOption(true));
+ sone.getOptions().addBooleanOption("ShowNotification/NewPosts", new DefaultOption(true));
+ sone.getOptions().addBooleanOption("ShowNotification/NewReplies", new DefaultOption(true));
+ followSone(sone, getSone("nwa8lHa271k2QvJ8aa0Ov7IHAV-DFOCFgmDt3X6BpCI"));
+ touchConfiguration();
return sone;
}
@@ -933,7 +1016,7 @@ public class Core implements IdentityListener, UpdateListener, SoneProvider, Pos
return null;
}
synchronized (remoteSones) {
- final Sone sone = getRemoteSone(identity.getId()).setIdentity(identity);
+ final Sone sone = getRemoteSone(identity.getId(), true).setIdentity(identity);
boolean newSone = sone.getRequestUri() == null;
sone.setRequestUri(getSoneUri(identity.getRequestUri()));
sone.setLatestEdition(Numbers.safeParseLong(identity.getProperty("Sone.LatestEdition"), (long) 0));
@@ -948,13 +1031,11 @@ public class Core implements IdentityListener, UpdateListener, SoneProvider, Pos
coreListenerManager.fireNewSoneFound(sone);
for (Sone localSone : getLocalSones()) {
if (localSone.getOptions().getBooleanOption("AutoFollow").get()) {
- localSone.addFriend(sone.getId());
- saveSone(localSone);
+ followSone(localSone, sone);
}
}
}
}
- remoteSones.put(identity.getId(), sone);
soneDownloader.addSone(sone);
setSoneStatus(sone, SoneStatus.unknown);
soneDownloaders.execute(new Runnable() {
@@ -971,6 +1052,90 @@ public class Core implements IdentityListener, UpdateListener, SoneProvider, Pos
}
/**
+ * Lets the given local Sone follow the Sone with the given ID.
+ *
+ * @param sone
+ * The local Sone that should follow another Sone
+ * @param soneId
+ * The ID of the Sone to follow
+ */
+ public void followSone(Sone sone, String soneId) {
+ Validation.begin().isNotNull("Sone", sone).isNotNull("Sone ID", soneId).check();
+ followSone(sone, getSone(soneId));
+ }
+
+ /**
+ * Lets the given local Sone follow the other given Sone. If the given Sone
+ * was not followed by any local Sone before, this will mark all elements of
+ * the followed Sone as read that have been created before the current
+ * moment.
+ *
+ * @param sone
+ * The local Sone that should follow the other Sone
+ * @param followedSone
+ * The Sone that should be followed
+ */
+ public void followSone(Sone sone, Sone followedSone) {
+ Validation.begin().isNotNull("Sone", sone).isNotNull("Followed Sone", followedSone).check();
+ sone.addFriend(followedSone.getId());
+ synchronized (soneFollowingTimes) {
+ if (!soneFollowingTimes.containsKey(followedSone)) {
+ long now = System.currentTimeMillis();
+ soneFollowingTimes.put(followedSone, now);
+ for (Post post : followedSone.getPosts()) {
+ if (post.getTime() < now) {
+ markPostKnown(post);
+ }
+ }
+ for (PostReply reply : followedSone.getReplies()) {
+ if (reply.getTime() < now) {
+ markReplyKnown(reply);
+ }
+ }
+ }
+ }
+ touchConfiguration();
+ }
+
+ /**
+ * Lets the given local Sone unfollow the Sone with the given ID.
+ *
+ * @param sone
+ * The local Sone that should unfollow another Sone
+ * @param soneId
+ * The ID of the Sone being unfollowed
+ */
+ public void unfollowSone(Sone sone, String soneId) {
+ Validation.begin().isNotNull("Sone", sone).isNotNull("Sone ID", soneId).check();
+ unfollowSone(sone, getSone(soneId, false));
+ }
+
+ /**
+ * Lets the given local Sone unfollow the other given Sone. If the given
+ * local Sone is the last local Sone that followed the given Sone, its
+ * following time will be removed.
+ *
+ * @param sone
+ * The local Sone that should unfollow another Sone
+ * @param unfollowedSone
+ * The Sone being unfollowed
+ */
+ public void unfollowSone(Sone sone, Sone unfollowedSone) {
+ Validation.begin().isNotNull("Sone", sone).isNotNull("Unfollowed Sone", unfollowedSone).check();
+ sone.removeFriend(unfollowedSone.getId());
+ boolean unfollowedSoneStillFollowed = false;
+ for (Sone localSone : getLocalSones()) {
+ unfollowedSoneStillFollowed |= localSone.hasFriend(unfollowedSone.getId());
+ }
+ if (!unfollowedSoneStillFollowed) {
+ synchronized (soneFollowingTimes) {
+ soneFollowingTimes.remove(unfollowedSone);
+ }
+ }
+ touchConfiguration();
+ }
+
+ /**
* Retrieves the trust relationship from the origin to the target. If the
* trust relationship can not be retrieved, {@code null} is returned.
*
@@ -1062,14 +1227,28 @@ public class Core implements IdentityListener, UpdateListener, SoneProvider, Pos
}
/**
- * Updates the stores Sone with the given Sone.
+ * 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) && preferences.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 });
@@ -1088,9 +1267,13 @@ public class Core implements IdentityListener, UpdateListener, SoneProvider, Pos
synchronized (newPosts) {
for (Post post : sone.getPosts()) {
post.setSone(storedSone);
- if (!storedPosts.contains(post) && !knownPosts.contains(post.getId())) {
- newPosts.add(post.getId());
- coreListenerManager.fireNewPostFound(post);
+ if (!storedPosts.contains(post)) {
+ if (post.getTime() < getSoneFollowingTime(sone)) {
+ knownPosts.add(post.getId());
+ } else if (!knownPosts.contains(post.getId())) {
+ newPosts.add(post.getId());
+ coreListenerManager.fireNewPostFound(post);
+ }
}
posts.put(post.getId(), post);
}
@@ -1098,25 +1281,45 @@ public class Core implements IdentityListener, UpdateListener, SoneProvider, Pos
}
synchronized (replies) {
if (!soneRescueMode) {
- for (Reply reply : storedSone.getReplies()) {
+ for (PostReply reply : storedSone.getReplies()) {
replies.remove(reply.getId());
if (!sone.getReplies().contains(reply)) {
coreListenerManager.fireReplyRemoved(reply);
}
}
}
- Set storedReplies = storedSone.getReplies();
+ Set storedReplies = storedSone.getReplies();
synchronized (newReplies) {
- for (Reply reply : sone.getReplies()) {
+ for (PostReply reply : sone.getReplies()) {
reply.setSone(storedSone);
- if (!storedReplies.contains(reply) && !knownReplies.contains(reply.getId())) {
- newReplies.add(reply.getId());
- coreListenerManager.fireNewReplyFound(reply);
+ if (!storedReplies.contains(reply)) {
+ if (reply.getTime() < getSoneFollowingTime(sone)) {
+ knownReplies.add(reply.getId());
+ } else if (!knownReplies.contains(reply.getId())) {
+ newReplies.add(reply.getId());
+ coreListenerManager.fireNewReplyFound(reply);
+ }
}
replies.put(reply.getId(), reply);
}
}
}
+ synchronized (albums) {
+ synchronized (images) {
+ for (Album album : storedSone.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 (storedSone) {
if (!soneRescueMode || (sone.getTime() > storedSone.getTime())) {
storedSone.setTime(sone.getTime());
@@ -1127,7 +1330,7 @@ public class Core implements IdentityListener, UpdateListener, SoneProvider, Pos
for (Post post : sone.getPosts()) {
storedSone.addPost(post);
}
- for (Reply reply : sone.getReplies()) {
+ for (PostReply reply : sone.getReplies()) {
storedSone.addReply(reply);
}
for (String likedPostId : sone.getLikedPostIds()) {
@@ -1136,11 +1339,15 @@ public class Core implements IdentityListener, UpdateListener, SoneProvider, Pos
for (String likedReplyId : sone.getLikedReplyIds()) {
storedSone.addLikedReplyId(likedReplyId);
}
+ for (Album album : sone.getAlbums()) {
+ storedSone.addAlbum(album);
+ }
} else {
storedSone.setPosts(sone.getPosts());
storedSone.setReplies(sone.getReplies());
storedSone.setLikePostIds(sone.getLikedPostIds());
storedSone.setLikeReplyIds(sone.getLikedReplyIds());
+ storedSone.setAlbums(sone.getAlbums());
}
storedSone.setLatestEdition(sone.getLatestEdition());
}
@@ -1166,7 +1373,9 @@ public class Core implements IdentityListener, UpdateListener, SoneProvider, Pos
return;
}
localSones.remove(sone.getId());
- soneInserters.remove(sone).stop();
+ SoneInserter soneInserter = soneInserters.remove(sone);
+ soneInserter.removeSoneInsertListener(this);
+ soneInserter.stop();
}
try {
((OwnIdentity) sone.getIdentity()).removeContext("Sone");
@@ -1193,7 +1402,7 @@ public class Core implements IdentityListener, UpdateListener, SoneProvider, Pos
if (newSones.remove(sone.getId())) {
knownSones.add(sone.getId());
coreListenerManager.fireMarkSoneKnown(sone);
- saveConfiguration();
+ touchConfiguration();
}
}
}
@@ -1213,6 +1422,10 @@ public class Core implements IdentityListener, UpdateListener, SoneProvider, Pos
/* initialize options. */
sone.getOptions().addBooleanOption("AutoFollow", new DefaultOption(false));
+ sone.getOptions().addBooleanOption("EnableSoneInsertNotifications", new DefaultOption(false));
+ sone.getOptions().addBooleanOption("ShowNotification/NewSones", new DefaultOption(true));
+ sone.getOptions().addBooleanOption("ShowNotification/NewPosts", new DefaultOption(true));
+ sone.getOptions().addBooleanOption("ShowNotification/NewReplies", new DefaultOption(true));
/* load Sone. */
String sonePrefix = "Sone/" + sone.getId();
@@ -1266,7 +1479,7 @@ public class Core implements IdentityListener, UpdateListener, SoneProvider, Pos
}
/* load replies. */
- Set replies = new HashSet();
+ Set replies = new HashSet();
while (true) {
String replyPrefix = sonePrefix + "/Replies/" + replies.size();
String replyId = configuration.getStringValue(replyPrefix + "/ID").getValue(null);
@@ -1313,8 +1526,71 @@ public class Core implements IdentityListener, UpdateListener, SoneProvider, Pos
friends.add(friendId);
}
+ /* load albums. */
+ List topLevelAlbums = new ArrayList();
+ 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, "Invalid parent album ID: " + albumParentId);
+ return;
+ }
+ parentAlbum.addAlbum(album);
+ } else {
+ 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 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));
/* if weâre still here, Sone was loaded successfully. */
synchronized (sone) {
@@ -1324,7 +1600,10 @@ public class Core implements IdentityListener, UpdateListener, SoneProvider, Pos
sone.setReplies(replies);
sone.setLikePostIds(likedPostIds);
sone.setLikeReplyIds(likedReplyIds);
- sone.setFriends(friends);
+ for (String friendId : friends) {
+ followSone(sone, friendId);
+ }
+ sone.setAlbums(topLevelAlbums);
soneInserters.get(sone).setLastInsertFingerprint(lastInsertFingerprint);
}
synchronized (newSones) {
@@ -1338,199 +1617,100 @@ public class Core implements IdentityListener, UpdateListener, SoneProvider, Pos
}
}
synchronized (newReplies) {
- for (Reply reply : replies) {
+ for (PostReply reply : replies) {
knownReplies.add(reply.getId());
}
}
}
/**
- * Saves the given Sone. This will persist all local settings for the given
- * Sone, such as the friends list and similar, private options.
+ * Creates a new post.
*
* @param sone
- * The Sone to save
+ * 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 synchronized void saveSone(Sone sone) {
+ public Post createPost(Sone sone, Sone recipient, long time, String text) {
if (!isLocalSone(sone)) {
- logger.log(Level.FINE, "Tried to save non-local Sone: %s", sone);
- return;
+ logger.log(Level.FINE, "Tried to create post for non-local Sone: %s", sone);
+ return null;
}
- if (!(sone.getIdentity() instanceof OwnIdentity)) {
- logger.log(Level.WARNING, "Local Sone without OwnIdentity found, refusing to save: %s", sone);
- return;
+ final Post post = new Post(sone, time, text);
+ if (recipient != null) {
+ post.setRecipient(recipient);
}
+ synchronized (posts) {
+ posts.put(post.getId(), post);
+ }
+ synchronized (newPosts) {
+ newPosts.add(post.getId());
+ coreListenerManager.fireNewPostFound(post);
+ }
+ sone.addPost(post);
+ touchConfiguration();
+ localElementTicker.registerEvent(System.currentTimeMillis() + 10 * 1000, new Runnable() {
- logger.log(Level.INFO, "Saving Sone: %s", sone);
- try {
- ((OwnIdentity) sone.getIdentity()).setProperty("Sone.LatestEdition", String.valueOf(sone.getLatestEdition()));
-
- /* 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());
-
- /* 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 (Reply 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 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) {
- 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);
- }
- }
-
- /**
- * 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);
- }
- synchronized (newPosts) {
- 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;
- }
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void run() {
+ markPostKnown(post);
+ }
+ }, "Mark " + post + " read.");
+ return post;
+ }
/**
* Deletes the given post.
@@ -1552,7 +1732,7 @@ public class Core implements IdentityListener, UpdateListener, SoneProvider, Pos
markPostKnown(post);
knownPosts.remove(post.getId());
}
- saveSone(post.getSone());
+ touchConfiguration();
}
/**
@@ -1567,7 +1747,7 @@ public class Core implements IdentityListener, UpdateListener, SoneProvider, Pos
if (newPosts.remove(post.getId())) {
knownPosts.add(post.getId());
coreListenerManager.fireMarkPostKnown(post);
- saveConfiguration();
+ touchConfiguration();
}
}
}
@@ -1627,7 +1807,7 @@ public class Core implements IdentityListener, UpdateListener, SoneProvider, Pos
* The text of the reply
* @return The created reply
*/
- public Reply createReply(Sone sone, Post post, String text) {
+ public PostReply createReply(Sone sone, Post post, String text) {
return createReply(sone, post, System.currentTimeMillis(), text);
}
@@ -1644,12 +1824,12 @@ public class Core implements IdentityListener, UpdateListener, SoneProvider, Pos
* The text of the reply
* @return The created reply
*/
- public Reply createReply(Sone sone, Post post, long time, String text) {
+ 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 Reply reply = new Reply(sone, post, System.currentTimeMillis(), text);
+ final PostReply reply = new PostReply(sone, post, System.currentTimeMillis(), text);
synchronized (replies) {
replies.put(reply.getId(), reply);
}
@@ -1658,7 +1838,7 @@ public class Core implements IdentityListener, UpdateListener, SoneProvider, Pos
coreListenerManager.fireNewReplyFound(reply);
}
sone.addReply(reply);
- saveSone(sone);
+ touchConfiguration();
localElementTicker.registerEvent(System.currentTimeMillis() + 10 * 1000, new Runnable() {
/**
@@ -1678,7 +1858,7 @@ public class Core implements IdentityListener, UpdateListener, SoneProvider, Pos
* @param reply
* The reply to delete
*/
- public void deleteReply(Reply reply) {
+ public void deleteReply(PostReply reply) {
Sone sone = reply.getSone();
if (!isLocalSone(sone)) {
logger.log(Level.FINE, "Tried to delete non-local reply: %s", reply);
@@ -1692,7 +1872,7 @@ public class Core implements IdentityListener, UpdateListener, SoneProvider, Pos
knownReplies.remove(reply.getId());
}
sone.removeReply(reply);
- saveSone(sone);
+ touchConfiguration();
}
/**
@@ -1702,48 +1882,364 @@ public class Core implements IdentityListener, UpdateListener, SoneProvider, Pos
* @param reply
* The reply to mark as known
*/
- public void markReplyKnown(Reply reply) {
+ public void markReplyKnown(PostReply reply) {
synchronized (newReplies) {
if (newReplies.remove(reply.getId())) {
knownReplies.add(reply.getId());
coreListenerManager.fireMarkReplyKnown(reply);
- saveConfiguration();
+ 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.
*/
- public void start() {
+ @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.
*/
- public void stop() {
+ @Override
+ public void serviceStop() {
synchronized (localSones) {
for (SoneInserter soneInserter : soneInserters.values()) {
+ soneInserter.removeSoneInsertListener(this);
soneInserter.stop();
}
- for (Sone localSone : localSones.values()) {
- saveSone(localSone);
- }
}
updateChecker.stop();
updateChecker.removeUpdateListener(this);
soneDownloader.stop();
- saveConfiguration();
- stopped = true;
+ }
+
+ //
+ // 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());
+
+ /* 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 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.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.
*/
- public void saveConfiguration() {
+ private void saveConfiguration() {
synchronized (configuration) {
if (storingConfiguration) {
logger.log(Level.FINE, "Already storing configurationâ¦");
@@ -1757,6 +2253,8 @@ public class Core implements IdentityListener, UpdateListener, SoneProvider, Pos
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());
@@ -1776,6 +2274,17 @@ public class Core implements IdentityListener, UpdateListener, SoneProvider, Pos
configuration.getStringValue("KnownSone/" + soneCounter + "/ID").setValue(null);
}
+ /* save Sone following times. */
+ soneCounter = 0;
+ synchronized (soneFollowingTimes) {
+ for (Entry 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 (newPosts) {
@@ -1815,10 +2324,6 @@ public class Core implements IdentityListener, UpdateListener, SoneProvider, Pos
}
}
- //
- // PRIVATE METHODS
- //
-
/**
* Loads the configuration.
*/
@@ -1834,6 +2339,8 @@ public class Core implements IdentityListener, UpdateListener, SoneProvider, Pos
}));
options.addIntegerOption("PostsPerPage", new DefaultOption(10, new IntegerRangeValidator(1, Integer.MAX_VALUE)));
+ options.addIntegerOption("CharactersPerPost", new DefaultOption(400, new OrValidator(new IntegerRangeValidator(50, Integer.MAX_VALUE), new EqualityValidator(-1))));
+ options.addIntegerOption("PostCutOffLength", 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)));
@@ -1872,6 +2379,8 @@ public class Core implements IdentityListener, UpdateListener, SoneProvider, Pos
loadConfigurationValue("InsertionDelay");
loadConfigurationValue("PostsPerPage");
+ loadConfigurationValue("CharactersPerPost");
+ loadConfigurationValue("PostCutOffLength");
options.getBooleanOption("RequireFullAccess").set(configuration.getBooleanValue("Option/RequireFullAccess").getValue(null));
loadConfigurationValue("PositiveTrust");
loadConfigurationValue("NegativeTrust");
@@ -1892,6 +2401,20 @@ public class Core implements IdentityListener, UpdateListener, SoneProvider, Pos
}
}
+ /* load Sone following times. */
+ soneCounter = 0;
+ while (true) {
+ String soneId = configuration.getStringValue("SoneFollowingTimes/" + soneCounter + "/Sone").getValue(null);
+ if (soneId == null) {
+ break;
+ }
+ long time = configuration.getLongValue("SoneFollowingTimes/" + soneCounter + "/Time").getValue(Long.MAX_VALUE);
+ synchronized (soneFollowingTimes) {
+ soneFollowingTimes.put(getSone(soneId), time);
+ }
+ ++soneCounter;
+ }
+
/* load known posts. */
int postCounter = 0;
while (true) {
@@ -2007,7 +2530,7 @@ public class Core implements IdentityListener, UpdateListener, SoneProvider, Pos
@Override
@SuppressWarnings("synthetic-access")
public void run() {
- Sone sone = getRemoteSone(identity.getId());
+ Sone sone = getRemoteSone(identity.getId(), false);
sone.setIdentity(identity);
sone.setLatestEdition(Numbers.safeParseLong(identity.getProperty("Sone.LatestEdition"), sone.getLatestEdition()));
soneDownloader.addSone(sone);
@@ -2051,7 +2574,7 @@ public class Core implements IdentityListener, UpdateListener, SoneProvider, Pos
}
synchronized (replies) {
synchronized (newReplies) {
- for (Reply reply : sone.getReplies()) {
+ for (PostReply reply : sone.getReplies()) {
replies.remove(reply.getId());
newReplies.remove(reply.getId());
coreListenerManager.fireReplyRemoved(reply);
@@ -2079,6 +2602,77 @@ public class Core implements IdentityListener, UpdateListener, SoneProvider, Pos
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.
@@ -2168,6 +2762,74 @@ public class Core implements IdentityListener, UpdateListener, SoneProvider, Pos
}
/**
+ * 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 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}
@@ -2331,29 +2993,6 @@ public class Core implements IdentityListener, UpdateListener, SoneProvider, Pos
}
/**
- * Returns whether the rescue mode is active.
- *
- * @return {@code true} if the rescue mode is active, {@code false}
- * otherwise
- */
- public boolean isSoneRescueMode() {
- return options.getBooleanOption("SoneRescueMode").get();
- }
-
- /**
- * Sets whether the rescue mode is active.
- *
- * @param soneRescueMode
- * {@code true} if the rescue mode is active, {@code false}
- * otherwise
- * @return This preferences
- */
- public Preferences setSoneRescueMode(Boolean soneRescueMode) {
- options.getBooleanOption("SoneRescueMode").set(soneRescueMode);
- 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!