X-Git-Url: https://git.pterodactylus.net/?p=Sone.git;a=blobdiff_plain;f=src%2Fmain%2Fjava%2Fnet%2Fpterodactylus%2Fsone%2Fcore%2FCore.java;h=e67bf68c4ceeef7c0f530458f8ca85ecb9d75002;hp=ea36aad76bf88c22ff6376495271d050c5133f61;hb=0df5e91852f737d760c5a9f54c5667309fbadcc2;hpb=bb2c8cb2d47a77bcef2d299ad4bf8e16f22a6198 diff --git a/src/main/java/net/pterodactylus/sone/core/Core.java b/src/main/java/net/pterodactylus/sone/core/Core.java index ea36aad..e67bf68 100644 --- a/src/main/java/net/pterodactylus/sone/core/Core.java +++ b/src/main/java/net/pterodactylus/sone/core/Core.java @@ -34,12 +34,15 @@ 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.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; @@ -53,6 +56,7 @@ 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; @@ -66,7 +70,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 +119,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,10 +131,6 @@ public class Core implements IdentityListener, UpdateListener, SoneProvider, Pos /** The FCP interface. */ private volatile FcpInterface fcpInterface; - /** Whether the core has been stopped. */ - @SuppressWarnings("unused") - private volatile boolean stopped; - /** The Sones’ statuses. */ /* synchronize access on itself. */ private final Map soneStatuses = new HashMap(); @@ -185,9 +188,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. * @@ -199,10 +214,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); } @@ -243,7 +260,7 @@ public class Core implements IdentityListener, UpdateListener, SoneProvider, Pos */ public void setConfiguration(Configuration configuration) { this.configuration = configuration; - saveConfiguration(); + touchConfiguration(); } /** @@ -695,6 +712,7 @@ public class Core implements IdentityListener, UpdateListener, SoneProvider, Pos */ public List getReplies(Post post) { Set sones = getSones(); + @SuppressWarnings("hiding") List replies = new ArrayList(); for (Sone sone : sones) { for (Reply reply : sone.getReplies()) { @@ -787,6 +805,7 @@ public class Core implements IdentityListener, UpdateListener, SoneProvider, Pos * @return All bookmarked posts */ public Set getBookmarkedPosts() { + @SuppressWarnings("hiding") Set posts = new HashSet(); synchronized (bookmarkedPosts) { for (String bookmarkedPostId : bookmarkedPosts) { @@ -799,6 +818,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 // @@ -882,6 +984,7 @@ 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); @@ -907,7 +1010,7 @@ 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); + touchConfiguration(); return sone; } @@ -940,7 +1043,7 @@ public class Core implements IdentityListener, UpdateListener, SoneProvider, Pos for (Sone localSone : getLocalSones()) { if (localSone.getOptions().getBooleanOption("AutoFollow").get()) { localSone.addFriend(sone.getId()); - saveSone(localSone); + touchConfiguration(); } } } @@ -1146,6 +1249,7 @@ public class Core implements IdentityListener, UpdateListener, SoneProvider, Pos storedSone.setReplies(sone.getReplies()); storedSone.setLikePostIds(sone.getLikedPostIds()); storedSone.setLikeReplyIds(sone.getLikedReplyIds()); + storedSone.setAlbums(sone.getAlbums()); } storedSone.setLatestEdition(sone.getLatestEdition()); } @@ -1171,7 +1275,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"); @@ -1198,7 +1304,7 @@ public class Core implements IdentityListener, UpdateListener, SoneProvider, Pos if (newSones.remove(sone.getId())) { knownSones.add(sone.getId()); coreListenerManager.fireMarkSoneKnown(sone); - saveConfiguration(); + touchConfiguration(); } } } @@ -1218,6 +1324,7 @@ public class Core implements IdentityListener, UpdateListener, SoneProvider, Pos /* initialize options. */ sone.getOptions().addBooleanOption("AutoFollow", new DefaultOption(false)); + sone.getOptions().addBooleanOption("EnableSoneInsertNotifications", new DefaultOption(false)); /* load Sone. */ String sonePrefix = "Sone/" + sone.getId(); @@ -1249,6 +1356,7 @@ public class Core implements IdentityListener, UpdateListener, SoneProvider, Pos } /* load posts. */ + @SuppressWarnings("hiding") Set posts = new HashSet(); while (true) { String postPrefix = sonePrefix + "/Posts/" + posts.size(); @@ -1271,6 +1379,7 @@ public class Core implements IdentityListener, UpdateListener, SoneProvider, Pos } /* load replies. */ + @SuppressWarnings("hiding") Set replies = new HashSet(); while (true) { String replyPrefix = sonePrefix + "/Replies/" + replies.size(); @@ -1318,8 +1427,67 @@ 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); + 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); + 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)); /* if we’re still here, Sone was loaded successfully. */ synchronized (sone) { @@ -1330,6 +1498,7 @@ public class Core implements IdentityListener, UpdateListener, SoneProvider, Pos sone.setLikePostIds(likedPostIds); sone.setLikeReplyIds(likedReplyIds); sone.setFriends(friends); + sone.setAlbums(topLevelAlbums); soneInserters.get(sone).setLastInsertFingerprint(lastInsertFingerprint); } synchronized (newSones) { @@ -1350,105 +1519,6 @@ public class Core implements IdentityListener, UpdateListener, SoneProvider, Pos } /** - * 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 - */ - public 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 { - ((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 @@ -1523,7 +1593,7 @@ public class Core implements IdentityListener, UpdateListener, SoneProvider, Pos coreListenerManager.fireNewPostFound(post); } sone.addPost(post); - saveSone(sone); + touchConfiguration(); localElementTicker.registerEvent(System.currentTimeMillis() + 10 * 1000, new Runnable() { /** @@ -1557,7 +1627,7 @@ public class Core implements IdentityListener, UpdateListener, SoneProvider, Pos markPostKnown(post); knownPosts.remove(post.getId()); } - saveSone(post.getSone()); + touchConfiguration(); } /** @@ -1572,7 +1642,7 @@ public class Core implements IdentityListener, UpdateListener, SoneProvider, Pos if (newPosts.remove(post.getId())) { knownPosts.add(post.getId()); coreListenerManager.fireMarkPostKnown(post); - saveConfiguration(); + touchConfiguration(); } } } @@ -1663,7 +1733,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() { /** @@ -1697,7 +1767,7 @@ public class Core implements IdentityListener, UpdateListener, SoneProvider, Pos knownReplies.remove(reply.getId()); } sone.removeReply(reply); - saveSone(sone); + touchConfiguration(); } /** @@ -1712,43 +1782,354 @@ public class Core implements IdentityListener, UpdateListener, SoneProvider, Pos 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 { + ((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 albums. first, collect in a flat structure, top-level first. */ + List albums = Sone.flattenAlbums(sone.getAlbums()); + + 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(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/EnableSoneInsertNotifications").setValue(sone.getOptions().getBooleanOption("EnableSoneInsertNotifications").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); + } } /** * Saves the current options. */ - public void saveConfiguration() { + private void saveConfiguration() { synchronized (configuration) { if (storingConfiguration) { logger.log(Level.FINE, "Already storing configuration…"); @@ -1821,10 +2202,6 @@ public class Core implements IdentityListener, UpdateListener, SoneProvider, Pos } } - // - // PRIVATE METHODS - // - /** * Loads the configuration. */ @@ -2087,6 +2464,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.