2 * Sone - Core.java - Copyright © 2010–2012 David Roden
4 * This program is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, either version 3 of the License, or
7 * (at your option) any later version.
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
18 package net.pterodactylus.sone.core;
20 import java.net.MalformedURLException;
21 import java.util.ArrayList;
22 import java.util.Collection;
23 import java.util.Collections;
24 import java.util.HashMap;
25 import java.util.HashSet;
26 import java.util.List;
28 import java.util.Map.Entry;
30 import java.util.concurrent.ExecutorService;
31 import java.util.concurrent.Executors;
32 import java.util.logging.Level;
33 import java.util.logging.Logger;
35 import net.pterodactylus.sone.core.Options.DefaultOption;
36 import net.pterodactylus.sone.core.Options.Option;
37 import net.pterodactylus.sone.core.Options.OptionWatcher;
38 import net.pterodactylus.sone.data.Album;
39 import net.pterodactylus.sone.data.Client;
40 import net.pterodactylus.sone.data.Image;
41 import net.pterodactylus.sone.data.Post;
42 import net.pterodactylus.sone.data.PostReply;
43 import net.pterodactylus.sone.data.Profile;
44 import net.pterodactylus.sone.data.Profile.Field;
45 import net.pterodactylus.sone.data.Reply;
46 import net.pterodactylus.sone.data.Sone;
47 import net.pterodactylus.sone.data.Sone.ShowCustomAvatars;
48 import net.pterodactylus.sone.data.Sone.SoneStatus;
49 import net.pterodactylus.sone.data.TemporaryImage;
50 import net.pterodactylus.sone.data.impl.PostImpl;
51 import net.pterodactylus.sone.database.Database;
52 import net.pterodactylus.sone.fcp.FcpInterface;
53 import net.pterodactylus.sone.fcp.FcpInterface.FullAccessRequired;
54 import net.pterodactylus.sone.freenet.wot.Identity;
55 import net.pterodactylus.sone.freenet.wot.IdentityListener;
56 import net.pterodactylus.sone.freenet.wot.IdentityManager;
57 import net.pterodactylus.sone.freenet.wot.OwnIdentity;
58 import net.pterodactylus.sone.main.SonePlugin;
59 import net.pterodactylus.util.config.Configuration;
60 import net.pterodactylus.util.config.ConfigurationException;
61 import net.pterodactylus.util.logging.Logging;
62 import net.pterodactylus.util.number.Numbers;
63 import net.pterodactylus.util.service.AbstractService;
64 import net.pterodactylus.util.thread.NamedThreadFactory;
65 import net.pterodactylus.util.thread.Ticker;
66 import net.pterodactylus.util.validation.EqualityValidator;
67 import net.pterodactylus.util.validation.IntegerRangeValidator;
68 import net.pterodactylus.util.validation.OrValidator;
69 import net.pterodactylus.util.validation.Validation;
70 import net.pterodactylus.util.version.Version;
71 import freenet.keys.FreenetURI;
76 * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
78 public class Core extends AbstractService implements IdentityListener, UpdateListener, SoneProvider, PostProvider, SoneInsertListener, ImageInsertListener {
81 private static final Logger logger = Logging.getLogger(Core.class);
83 /** The start time. */
84 private final long startupTime = System.currentTimeMillis();
87 private final Options options = new Options();
89 /** The preferences. */
90 private final Preferences preferences = new Preferences(options);
92 /** The core listener manager. */
93 private final CoreListenerManager coreListenerManager = new CoreListenerManager(this);
95 /** The configuration. */
96 private Configuration configuration;
98 /** Whether we’re currently saving the configuration. */
99 private boolean storingConfiguration = false;
102 private Database database;
104 /** The identity manager. */
105 private final IdentityManager identityManager;
107 /** Interface to freenet. */
108 private final FreenetInterface freenetInterface;
110 /** The Sone downloader. */
111 private final SoneDownloader soneDownloader;
113 /** The image inserter. */
114 private final ImageInserter imageInserter;
116 /** Sone downloader thread-pool. */
117 private final ExecutorService soneDownloaders = Executors.newFixedThreadPool(10, new NamedThreadFactory("Sone Downloader %2$d"));
119 /** The update checker. */
120 private final UpdateChecker updateChecker;
122 /** The trust updater. */
123 private final WebOfTrustUpdater webOfTrustUpdater;
125 /** The FCP interface. */
126 private volatile FcpInterface fcpInterface;
128 /** The times Sones were followed. */
129 private final Map<Sone, Long> soneFollowingTimes = new HashMap<Sone, Long>();
131 /** Locked local Sones. */
132 /* synchronize on itself. */
133 private final Set<Sone> lockedSones = new HashSet<Sone>();
135 /** Sone inserters. */
136 /* synchronize access on this on localSones. */
137 private final Map<Sone, SoneInserter> soneInserters = new HashMap<Sone, SoneInserter>();
139 /** Sone rescuers. */
140 /* synchronize access on this on localSones. */
141 private final Map<Sone, SoneRescuer> soneRescuers = new HashMap<Sone, SoneRescuer>();
143 /** All known Sones. */
144 private final Set<String> knownSones = new HashSet<String>();
147 private final Map<String, Post> posts = new HashMap<String, Post>();
149 /** All known posts. */
150 private final Set<String> knownPosts = new HashSet<String>();
153 private final Map<String, PostReply> replies = new HashMap<String, PostReply>();
155 /** All known replies. */
156 private final Set<String> knownReplies = new HashSet<String>();
158 /** All bookmarked posts. */
159 /* synchronize access on itself. */
160 private final Set<String> bookmarkedPosts = new HashSet<String>();
162 /** Trusted identities, sorted by own identities. */
163 private final Map<OwnIdentity, Set<Identity>> trustedIdentities = Collections.synchronizedMap(new HashMap<OwnIdentity, Set<Identity>>());
165 /** All known albums. */
166 private final Map<String, Album> albums = new HashMap<String, Album>();
168 /** All known images. */
169 private final Map<String, Image> images = new HashMap<String, Image>();
171 /** All temporary images. */
172 private final Map<String, TemporaryImage> temporaryImages = new HashMap<String, TemporaryImage>();
174 /** Ticker for threads that mark own elements as known. */
175 private final Ticker localElementTicker = new Ticker();
177 /** The time the configuration was last touched. */
178 private volatile long lastConfigurationUpdate;
181 * Creates a new core.
183 * @param configuration
184 * The configuration of the core
186 * The database to use
187 * @param freenetInterface
188 * The freenet interface
189 * @param identityManager
190 * The identity manager
191 * @param webOfTrustUpdater
192 * The WebOfTrust updater
194 public Core(Configuration configuration, Database database, FreenetInterface freenetInterface, IdentityManager identityManager, WebOfTrustUpdater webOfTrustUpdater) {
196 this.configuration = configuration;
197 this.database = database;
198 this.freenetInterface = freenetInterface;
199 this.identityManager = identityManager;
200 this.soneDownloader = new SoneDownloader(this, freenetInterface);
201 this.imageInserter = new ImageInserter(this, freenetInterface);
202 this.updateChecker = new UpdateChecker(freenetInterface);
203 this.webOfTrustUpdater = webOfTrustUpdater;
207 // LISTENER MANAGEMENT
211 * Adds a new core listener.
213 * @param coreListener
214 * The listener to add
216 public void addCoreListener(CoreListener coreListener) {
217 coreListenerManager.addListener(coreListener);
221 * Removes a core listener.
223 * @param coreListener
224 * The listener to remove
226 public void removeCoreListener(CoreListener coreListener) {
227 coreListenerManager.removeListener(coreListener);
235 * Returns the time Sone was started.
237 * @return The startup time (in milliseconds since Jan 1, 1970 UTC)
239 public long getStartupTime() {
244 * Sets the configuration to use. This will automatically save the current
245 * configuration to the given configuration.
247 * @param configuration
248 * The new configuration to use
250 public void setConfiguration(Configuration configuration) {
251 this.configuration = configuration;
252 touchConfiguration();
256 * Returns the options used by the core.
258 * @return The options of the core
260 public Preferences getPreferences() {
265 * Returns the identity manager used by the core.
267 * @return The identity manager
269 public IdentityManager getIdentityManager() {
270 return identityManager;
274 * Returns the update checker.
276 * @return The update checker
278 public UpdateChecker getUpdateChecker() {
279 return updateChecker;
283 * Sets the FCP interface to use.
285 * @param fcpInterface
286 * The FCP interface to use
288 public void setFcpInterface(FcpInterface fcpInterface) {
289 this.fcpInterface = fcpInterface;
293 * Returns the Sone rescuer for the given local Sone.
296 * The local Sone to get the rescuer for
297 * @return The Sone rescuer for the given Sone
299 public SoneRescuer getSoneRescuer(Sone sone) {
300 Validation.begin().isNotNull("Sone", sone).check().is("Local Sone", sone.isLocal()).check();
301 synchronized (soneRescuers) {
302 SoneRescuer soneRescuer = soneRescuers.get(sone);
303 if (soneRescuer == null) {
304 soneRescuer = new SoneRescuer(this, soneDownloader, sone);
305 soneRescuers.put(sone, soneRescuer);
313 * Returns whether the given Sone is currently locked.
317 * @return {@code true} if the Sone is locked, {@code false} if it is not
319 public boolean isLocked(Sone sone) {
320 synchronized (lockedSones) {
321 return lockedSones.contains(sone);
326 * Returns all Sones, remote and local.
330 public Collection<Sone> getSones() {
331 return database.getSones();
335 * Returns the Sone with the given ID, regardless whether it’s local or
339 * The ID of the Sone to get
340 * @return The Sone with the given ID, or {@code null} if there is no such
343 public Sone getSone(String id) {
344 return getSone(id, true);
348 * Returns the Sone with the given ID, regardless whether it’s local or
352 * The ID of the Sone to get
354 * {@code true} to create a new Sone if none exists,
355 * {@code false} to return {@code null} if a Sone with the given
357 * @return The Sone with the given ID, or {@code null} if there is no such
361 public Sone getSone(String id, boolean create) {
362 return database.getSone(id, create);
366 * Checks whether the core knows a Sone with the given ID.
370 * @return {@code true} if there is a Sone with the given ID, {@code false}
373 public boolean hasSone(String id) {
374 return database.getSone(id, false) != null;
378 * Returns all local Sones.
380 * @return All local Sones
382 public Collection<Sone> getLocalSones() {
383 return database.getLocalSones();
387 * Returns the local Sone with the given ID.
390 * The ID of the Sone to get
391 * @return The Sone with the given ID
393 public Sone getLocalSone(String id) {
394 return getLocalSone(id, true);
398 * Returns the local Sone with the given ID, optionally creating a new Sone.
403 * {@code true} to create a new Sone if none exists,
404 * {@code false} to return null if none exists
405 * @return The Sone with the given ID, or {@code null}
407 public Sone getLocalSone(String id, boolean create) {
408 return database.getLocalSone(id, create);
412 * Returns all remote Sones.
414 * @return All remote Sones
416 public Collection<Sone> getRemoteSones() {
417 return database.getRemoteSones();
421 * Returns the remote Sone with the given ID.
424 * The ID of the remote Sone to get
426 * {@code true} to always create a Sone, {@code false} to return
427 * {@code null} if no Sone with the given ID exists
428 * @return The Sone with the given ID
430 public Sone getRemoteSone(String id, boolean create) {
431 return database.getRemoteSone(id, create);
435 * Returns whether the given Sone has been modified.
438 * The Sone to check for modifications
439 * @return {@code true} if a modification has been detected in the Sone,
440 * {@code false} otherwise
442 public boolean isModifiedSone(Sone sone) {
443 return (soneInserters.containsKey(sone)) ? soneInserters.get(sone).isModified() : false;
447 * Returns the time when the given was first followed by any local Sone.
450 * The Sone to get the time for
451 * @return The time (in milliseconds since Jan 1, 1970) the Sone has first
452 * been followed, or {@link Long#MAX_VALUE}
454 public long getSoneFollowingTime(Sone sone) {
455 synchronized (soneFollowingTimes) {
456 if (soneFollowingTimes.containsKey(sone)) {
457 return soneFollowingTimes.get(sone);
459 return Long.MAX_VALUE;
464 * Returns whether the target Sone is trusted by the origin Sone.
470 * @return {@code true} if the target Sone is trusted by the origin Sone
472 public boolean isSoneTrusted(Sone origin, Sone target) {
473 Validation.begin().isNotNull("Origin", origin).isNotNull("Target", target).check().isInstanceOf("Origin’s OwnIdentity", origin.getIdentity(), OwnIdentity.class).check();
474 return trustedIdentities.containsKey(origin.getIdentity()) && trustedIdentities.get(origin.getIdentity()).contains(target.getIdentity());
478 * Returns the post with the given ID, optionally creating a new post.
481 * The ID of the post to get
483 * {@code true} it create a new post if no post with the given ID
484 * exists, {@code false} to return {@code null}
485 * @return The post, or {@code null} if there is no such post
488 public Post getPost(String postId, boolean create) {
489 synchronized (posts) {
490 Post post = posts.get(postId);
491 if ((post == null) && create) {
492 post = new PostImpl(postId);
493 posts.put(postId, post);
500 * Returns all posts that have the given Sone as recipient.
502 * @see Post#getRecipient()
504 * The recipient of the posts
505 * @return All posts that have the given Sone as recipient
507 public Set<Post> getDirectedPosts(Sone recipient) {
508 Validation.begin().isNotNull("Recipient", recipient).check();
509 Set<Post> directedPosts = new HashSet<Post>();
510 synchronized (posts) {
511 for (Post post : posts.values()) {
512 if (recipient.equals(post.getRecipient())) {
513 directedPosts.add(post);
517 return directedPosts;
521 * Returns the reply with the given ID. If there is no reply with the given
522 * ID yet, a new one is created.
525 * The ID of the reply to get
528 public PostReply getReply(String replyId) {
529 return getReply(replyId, true);
533 * Returns the reply with the given ID. If there is no reply with the given
534 * ID yet, a new one is created, unless {@code create} is false in which
535 * case {@code null} is returned.
538 * The ID of the reply to get
540 * {@code true} to always return a {@link Reply}, {@code false}
541 * to return {@code null} if no reply can be found
542 * @return The reply, or {@code null} if there is no such reply
544 public PostReply getReply(String replyId, boolean create) {
545 synchronized (replies) {
546 PostReply reply = replies.get(replyId);
547 if (create && (reply == null)) {
548 reply = new PostReply(replyId);
549 replies.put(replyId, reply);
556 * Returns all replies for the given post, order ascending by time.
559 * The post to get all replies for
560 * @return All replies for the given post
562 public List<PostReply> getReplies(Post post) {
563 Collection<Sone> sones = getSones();
564 List<PostReply> replies = new ArrayList<PostReply>();
565 for (Sone sone : sones) {
566 for (PostReply reply : sone.getReplies()) {
567 if (reply.getPost().equals(post)) {
572 Collections.sort(replies, Reply.TIME_COMPARATOR);
577 * Returns all Sones that have liked the given post.
580 * The post to get the liking Sones for
581 * @return The Sones that like the given post
583 public Set<Sone> getLikes(Post post) {
584 Set<Sone> sones = new HashSet<Sone>();
585 for (Sone sone : getSones()) {
586 if (sone.getLikedPostIds().contains(post.getId())) {
594 * Returns all Sones that have liked the given reply.
597 * The reply to get the liking Sones for
598 * @return The Sones that like the given reply
600 public Set<Sone> getLikes(PostReply reply) {
601 Set<Sone> sones = new HashSet<Sone>();
602 for (Sone sone : getSones()) {
603 if (sone.getLikedReplyIds().contains(reply.getId())) {
611 * Returns whether the given post is bookmarked.
615 * @return {@code true} if the given post is bookmarked, {@code false}
618 public boolean isBookmarked(Post post) {
619 return isPostBookmarked(post.getId());
623 * Returns whether the post with the given ID is bookmarked.
626 * The ID of the post to check
627 * @return {@code true} if the post with the given ID is bookmarked,
628 * {@code false} otherwise
630 public boolean isPostBookmarked(String id) {
631 synchronized (bookmarkedPosts) {
632 return bookmarkedPosts.contains(id);
637 * Returns all currently known bookmarked posts.
639 * @return All bookmarked posts
641 public Set<Post> getBookmarkedPosts() {
642 Set<Post> posts = new HashSet<Post>();
643 synchronized (bookmarkedPosts) {
644 for (String bookmarkedPostId : bookmarkedPosts) {
645 Post post = getPost(bookmarkedPostId, false);
655 * Returns the album with the given ID, creating a new album if no album
656 * with the given ID can be found.
659 * The ID of the album
660 * @return The album with the given ID
662 public Album getAlbum(String albumId) {
663 return getAlbum(albumId, true);
667 * Returns the album with the given ID, optionally creating a new album if
668 * an album with the given ID can not be found.
671 * The ID of the album
673 * {@code true} to create a new album if none exists for the
675 * @return The album with the given ID, or {@code null} if no album with the
676 * given ID exists and {@code create} is {@code false}
678 public Album getAlbum(String albumId, boolean create) {
679 synchronized (albums) {
680 Album album = albums.get(albumId);
681 if (create && (album == null)) {
682 album = new Album(albumId);
683 albums.put(albumId, album);
690 * Returns the image with the given ID, creating it if necessary.
693 * The ID of the image
694 * @return The image with the given ID
696 public Image getImage(String imageId) {
697 return getImage(imageId, true);
701 * Returns the image with the given ID, optionally creating it if it does
705 * The ID of the image
707 * {@code true} to create an image if none exists with the given
709 * @return The image with the given ID, or {@code null} if none exists and
712 public Image getImage(String imageId, boolean create) {
713 synchronized (images) {
714 Image image = images.get(imageId);
715 if (create && (image == null)) {
716 image = new Image(imageId);
717 images.put(imageId, image);
724 * Returns the temporary image with the given ID.
727 * The ID of the temporary image
728 * @return The temporary image, or {@code null} if there is no temporary
729 * image with the given ID
731 public TemporaryImage getTemporaryImage(String imageId) {
732 synchronized (temporaryImages) {
733 return temporaryImages.get(imageId);
742 * Locks the given Sone. A locked Sone will not be inserted by
743 * {@link SoneInserter} until it is {@link #unlockSone(Sone) unlocked}
749 public void lockSone(Sone sone) {
750 synchronized (lockedSones) {
751 if (lockedSones.add(sone)) {
752 coreListenerManager.fireSoneLocked(sone);
758 * Unlocks the given Sone.
760 * @see #lockSone(Sone)
764 public void unlockSone(Sone sone) {
765 synchronized (lockedSones) {
766 if (lockedSones.remove(sone)) {
767 coreListenerManager.fireSoneUnlocked(sone);
773 * Adds a local Sone from the given own identity.
776 * The own identity to create a Sone from
777 * @return The added (or already existing) Sone
779 public Sone addLocalSone(OwnIdentity ownIdentity) {
780 if (ownIdentity == null) {
781 logger.log(Level.WARNING, "Given OwnIdentity is null!");
786 sone = getLocalSone(ownIdentity.getId()).setIdentity(ownIdentity).setInsertUri(new FreenetURI(ownIdentity.getInsertUri())).setRequestUri(new FreenetURI(ownIdentity.getRequestUri()));
787 } catch (MalformedURLException mue1) {
788 logger.log(Level.SEVERE, String.format("Could not convert the Identity’s URIs to Freenet URIs: %s, %s", ownIdentity.getInsertUri(), ownIdentity.getRequestUri()), mue1);
791 sone.setLatestEdition(Numbers.safeParseLong(ownIdentity.getProperty("Sone.LatestEdition"), (long) 0));
792 sone.setClient(new Client("Sone", SonePlugin.VERSION.toString()));
794 /* TODO - load posts ’n stuff */
795 final SoneInserter soneInserter = new SoneInserter(this, freenetInterface, sone);
796 soneInserter.addSoneInsertListener(this);
797 soneInserters.put(sone, soneInserter);
798 sone.setStatus(SoneStatus.idle);
800 soneInserter.start();
801 database.saveSone(sone);
806 * Creates a new Sone for the given own identity.
809 * The own identity to create a Sone for
810 * @return The created Sone
812 public Sone createSone(OwnIdentity ownIdentity) {
813 if (!webOfTrustUpdater.addContextWait(ownIdentity, "Sone")) {
814 logger.log(Level.SEVERE, String.format("Could not add “Sone” context to own identity: %s", ownIdentity));
817 Sone sone = addLocalSone(ownIdentity);
818 sone.getOptions().addBooleanOption("AutoFollow", new DefaultOption<Boolean>(false));
819 sone.getOptions().addBooleanOption("EnableSoneInsertNotifications", new DefaultOption<Boolean>(false));
820 sone.getOptions().addBooleanOption("ShowNotification/NewSones", new DefaultOption<Boolean>(true));
821 sone.getOptions().addBooleanOption("ShowNotification/NewPosts", new DefaultOption<Boolean>(true));
822 sone.getOptions().addBooleanOption("ShowNotification/NewReplies", new DefaultOption<Boolean>(true));
823 sone.getOptions().addEnumOption("ShowCustomAvatars", new DefaultOption<ShowCustomAvatars>(ShowCustomAvatars.NEVER));
825 followSone(sone, getSone("nwa8lHa271k2QvJ8aa0Ov7IHAV-DFOCFgmDt3X6BpCI"));
826 touchConfiguration();
831 * Adds the Sone of the given identity.
834 * The identity whose Sone to add
835 * @return The added or already existing Sone
837 public Sone addRemoteSone(Identity identity) {
838 if (identity == null) {
839 logger.log(Level.WARNING, "Given Identity is null!");
842 final Sone sone = getRemoteSone(identity.getId(), true).setIdentity(identity);
843 boolean newSone = sone.getRequestUri() == null;
844 sone.setRequestUri(getSoneUri(identity.getRequestUri()));
845 sone.setLatestEdition(Numbers.safeParseLong(identity.getProperty("Sone.LatestEdition"), (long) 0));
847 synchronized (knownSones) {
848 newSone = !knownSones.contains(sone.getId());
850 sone.setKnown(!newSone);
852 coreListenerManager.fireNewSoneFound(sone);
853 for (Sone localSone : getLocalSones()) {
854 if (localSone.getOptions().getBooleanOption("AutoFollow").get()) {
855 followSone(localSone, sone);
860 soneDownloader.addSone(sone);
861 soneDownloaders.execute(new Runnable() {
864 @SuppressWarnings("synthetic-access")
866 soneDownloader.fetchSone(sone, sone.getRequestUri());
870 database.saveSone(sone);
875 * Lets the given local Sone follow the Sone with the given ID.
878 * The local Sone that should follow another Sone
880 * The ID of the Sone to follow
882 public void followSone(Sone sone, String soneId) {
883 Validation.begin().isNotNull("Sone", sone).isNotNull("Sone ID", soneId).check();
884 Sone followedSone = getSone(soneId, true);
885 if (followedSone == null) {
886 logger.log(Level.INFO, String.format("Ignored Sone with invalid ID: %s", soneId));
889 followSone(sone, getSone(soneId));
893 * Lets the given local Sone follow the other given Sone. If the given Sone
894 * was not followed by any local Sone before, this will mark all elements of
895 * the followed Sone as read that have been created before the current
899 * The local Sone that should follow the other Sone
900 * @param followedSone
901 * The Sone that should be followed
903 public void followSone(Sone sone, Sone followedSone) {
904 Validation.begin().isNotNull("Sone", sone).isNotNull("Followed Sone", followedSone).check();
905 sone.addFriend(followedSone.getId());
906 synchronized (soneFollowingTimes) {
907 if (!soneFollowingTimes.containsKey(followedSone)) {
908 long now = System.currentTimeMillis();
909 soneFollowingTimes.put(followedSone, now);
910 for (Post post : followedSone.getPosts()) {
911 if (post.getTime() < now) {
915 for (PostReply reply : followedSone.getReplies()) {
916 if (reply.getTime() < now) {
917 markReplyKnown(reply);
922 touchConfiguration();
926 * Lets the given local Sone unfollow the Sone with the given ID.
929 * The local Sone that should unfollow another Sone
931 * The ID of the Sone being unfollowed
933 public void unfollowSone(Sone sone, String soneId) {
934 Validation.begin().isNotNull("Sone", sone).isNotNull("Sone ID", soneId).check();
935 unfollowSone(sone, getSone(soneId, false));
939 * Lets the given local Sone unfollow the other given Sone. If the given
940 * local Sone is the last local Sone that followed the given Sone, its
941 * following time will be removed.
944 * The local Sone that should unfollow another Sone
945 * @param unfollowedSone
946 * The Sone being unfollowed
948 public void unfollowSone(Sone sone, Sone unfollowedSone) {
949 Validation.begin().isNotNull("Sone", sone).isNotNull("Unfollowed Sone", unfollowedSone).check();
950 sone.removeFriend(unfollowedSone.getId());
951 boolean unfollowedSoneStillFollowed = false;
952 for (Sone localSone : getLocalSones()) {
953 unfollowedSoneStillFollowed |= localSone.hasFriend(unfollowedSone.getId());
955 if (!unfollowedSoneStillFollowed) {
956 synchronized (soneFollowingTimes) {
957 soneFollowingTimes.remove(unfollowedSone);
960 touchConfiguration();
964 * Sets the trust value of the given origin Sone for
972 * The trust value (from {@code -100} to {@code 100})
974 public void setTrust(Sone origin, Sone target, int trustValue) {
975 Validation.begin().isNotNull("Trust Origin", origin).check().isInstanceOf("Trust Origin", origin.getIdentity(), OwnIdentity.class).isNotNull("Trust Target", target).isLessOrEqual("Trust Value", trustValue, 100).isGreaterOrEqual("Trust Value", trustValue, -100).check();
976 webOfTrustUpdater.setTrust((OwnIdentity) origin.getIdentity(), target.getIdentity(), trustValue, preferences.getTrustComment());
980 * Removes any trust assignment for the given target Sone.
987 public void removeTrust(Sone origin, Sone target) {
988 Validation.begin().isNotNull("Trust Origin", origin).isNotNull("Trust Target", target).check().isInstanceOf("Trust Origin Identity", origin.getIdentity(), OwnIdentity.class).check();
989 webOfTrustUpdater.setTrust((OwnIdentity) origin.getIdentity(), target.getIdentity(), null, null);
993 * Assigns the configured positive trust value for the given target.
1000 public void trustSone(Sone origin, Sone target) {
1001 setTrust(origin, target, preferences.getPositiveTrust());
1005 * Assigns the configured negative trust value for the given target.
1012 public void distrustSone(Sone origin, Sone target) {
1013 setTrust(origin, target, preferences.getNegativeTrust());
1017 * Removes the trust assignment for the given target.
1024 public void untrustSone(Sone origin, Sone target) {
1025 removeTrust(origin, target);
1029 * Updates the stored Sone with the given Sone.
1034 public void updateSone(Sone sone) {
1035 updateSone(sone, false);
1039 * Updates the stored Sone with the given Sone. If {@code soneRescueMode} is
1040 * {@code true}, an older Sone than the current Sone can be given to restore
1044 * The Sone to update
1045 * @param soneRescueMode
1046 * {@code true} if the stored Sone should be updated regardless
1047 * of the age of the given Sone
1049 public void updateSone(Sone sone, boolean soneRescueMode) {
1050 if (hasSone(sone.getId())) {
1051 Sone storedSone = getSone(sone.getId());
1052 if (!soneRescueMode && !(sone.getTime() > storedSone.getTime())) {
1053 logger.log(Level.FINE, String.format("Downloaded Sone %s is not newer than stored Sone %s.", sone, storedSone));
1056 synchronized (posts) {
1057 if (!soneRescueMode) {
1058 for (Post post : storedSone.getPosts()) {
1059 posts.remove(post.getId());
1060 if (!sone.getPosts().contains(post)) {
1061 coreListenerManager.firePostRemoved(post);
1065 List<Post> storedPosts = storedSone.getPosts();
1066 synchronized (knownPosts) {
1067 for (Post post : sone.getPosts()) {
1068 post.setSone(storedSone).setKnown(knownPosts.contains(post.getId()));
1069 if (!storedPosts.contains(post)) {
1070 if (post.getTime() < getSoneFollowingTime(sone)) {
1071 knownPosts.add(post.getId());
1072 post.setKnown(true);
1073 } else if (!knownPosts.contains(post.getId())) {
1074 coreListenerManager.fireNewPostFound(post);
1077 posts.put(post.getId(), post);
1081 synchronized (replies) {
1082 if (!soneRescueMode) {
1083 for (PostReply reply : storedSone.getReplies()) {
1084 replies.remove(reply.getId());
1085 if (!sone.getReplies().contains(reply)) {
1086 coreListenerManager.fireReplyRemoved(reply);
1090 Set<PostReply> storedReplies = storedSone.getReplies();
1091 synchronized (knownReplies) {
1092 for (PostReply reply : sone.getReplies()) {
1093 reply.setSone(storedSone).setKnown(knownReplies.contains(reply.getId()));
1094 if (!storedReplies.contains(reply)) {
1095 if (reply.getTime() < getSoneFollowingTime(sone)) {
1096 knownReplies.add(reply.getId());
1097 reply.setKnown(true);
1098 } else if (!knownReplies.contains(reply.getId())) {
1099 coreListenerManager.fireNewReplyFound(reply);
1102 replies.put(reply.getId(), reply);
1106 synchronized (albums) {
1107 synchronized (images) {
1108 for (Album album : storedSone.getAlbums()) {
1109 albums.remove(album.getId());
1110 for (Image image : album.getImages()) {
1111 images.remove(image.getId());
1114 for (Album album : sone.getAlbums()) {
1115 albums.put(album.getId(), album);
1116 for (Image image : album.getImages()) {
1117 images.put(image.getId(), image);
1122 synchronized (storedSone) {
1123 if (!soneRescueMode || (sone.getTime() > storedSone.getTime())) {
1124 storedSone.setTime(sone.getTime());
1126 storedSone.setClient(sone.getClient());
1127 storedSone.setProfile(sone.getProfile());
1128 if (soneRescueMode) {
1129 for (Post post : sone.getPosts()) {
1130 storedSone.addPost(post);
1132 for (PostReply reply : sone.getReplies()) {
1133 storedSone.addReply(reply);
1135 for (String likedPostId : sone.getLikedPostIds()) {
1136 storedSone.addLikedPostId(likedPostId);
1138 for (String likedReplyId : sone.getLikedReplyIds()) {
1139 storedSone.addLikedReplyId(likedReplyId);
1141 for (Album album : sone.getAlbums()) {
1142 storedSone.addAlbum(album);
1145 storedSone.setPosts(sone.getPosts());
1146 storedSone.setReplies(sone.getReplies());
1147 storedSone.setLikePostIds(sone.getLikedPostIds());
1148 storedSone.setLikeReplyIds(sone.getLikedReplyIds());
1149 storedSone.setAlbums(sone.getAlbums());
1151 storedSone.setLatestEdition(sone.getLatestEdition());
1157 * Deletes the given Sone. This will remove the Sone from the
1158 * {@link #getLocalSone(String) local Sones}, stops its {@link SoneInserter}
1159 * and remove the context from its identity.
1162 * The Sone to delete
1164 public void deleteSone(Sone sone) {
1165 if (!(sone.getIdentity() instanceof OwnIdentity)) {
1166 logger.log(Level.WARNING, String.format("Tried to delete Sone of non-own identity: %s", sone));
1169 if (!sone.isLocal()) {
1170 logger.log(Level.WARNING, String.format("Tried to delete non-local Sone: %s", sone));
1173 database.removeSone(sone.getId());
1174 SoneInserter soneInserter = soneInserters.remove(sone);
1175 soneInserter.removeSoneInsertListener(this);
1176 soneInserter.stop();
1177 webOfTrustUpdater.removeContext((OwnIdentity) sone.getIdentity(), "Sone");
1178 webOfTrustUpdater.removeProperty((OwnIdentity) sone.getIdentity(), "Sone.LatestEdition");
1180 configuration.getLongValue("Sone/" + sone.getId() + "/Time").setValue(null);
1181 } catch (ConfigurationException ce1) {
1182 logger.log(Level.WARNING, "Could not remove Sone from configuration!", ce1);
1187 * Marks the given Sone as known. If the Sone was not {@link Post#isKnown()
1188 * known} before, a {@link CoreListener#markSoneKnown(Sone)} event is fired.
1191 * The Sone to mark as known
1193 public void markSoneKnown(Sone sone) {
1194 if (!sone.isKnown()) {
1195 sone.setKnown(true);
1196 synchronized (knownSones) {
1197 knownSones.add(sone.getId());
1199 coreListenerManager.fireMarkSoneKnown(sone);
1200 touchConfiguration();
1205 * Loads and updates the given Sone from the configuration. If any error is
1206 * encountered, loading is aborted and the given Sone is not changed.
1209 * The Sone to load and update
1211 public void loadSone(Sone sone) {
1212 if (!sone.isLocal()) {
1213 logger.log(Level.FINE, String.format("Tried to load non-local Sone: %s", sone));
1217 /* initialize options. */
1218 sone.getOptions().addBooleanOption("AutoFollow", new DefaultOption<Boolean>(false));
1219 sone.getOptions().addBooleanOption("EnableSoneInsertNotifications", new DefaultOption<Boolean>(false));
1220 sone.getOptions().addBooleanOption("ShowNotification/NewSones", new DefaultOption<Boolean>(true));
1221 sone.getOptions().addBooleanOption("ShowNotification/NewPosts", new DefaultOption<Boolean>(true));
1222 sone.getOptions().addBooleanOption("ShowNotification/NewReplies", new DefaultOption<Boolean>(true));
1223 sone.getOptions().addEnumOption("ShowCustomAvatars", new DefaultOption<ShowCustomAvatars>(ShowCustomAvatars.NEVER));
1226 String sonePrefix = "Sone/" + sone.getId();
1227 Long soneTime = configuration.getLongValue(sonePrefix + "/Time").getValue(null);
1228 if (soneTime == null) {
1229 logger.log(Level.INFO, "Could not load Sone because no Sone has been saved.");
1232 String lastInsertFingerprint = configuration.getStringValue(sonePrefix + "/LastInsertFingerprint").getValue("");
1235 Profile profile = new Profile(sone);
1236 profile.setFirstName(configuration.getStringValue(sonePrefix + "/Profile/FirstName").getValue(null));
1237 profile.setMiddleName(configuration.getStringValue(sonePrefix + "/Profile/MiddleName").getValue(null));
1238 profile.setLastName(configuration.getStringValue(sonePrefix + "/Profile/LastName").getValue(null));
1239 profile.setBirthDay(configuration.getIntValue(sonePrefix + "/Profile/BirthDay").getValue(null));
1240 profile.setBirthMonth(configuration.getIntValue(sonePrefix + "/Profile/BirthMonth").getValue(null));
1241 profile.setBirthYear(configuration.getIntValue(sonePrefix + "/Profile/BirthYear").getValue(null));
1243 /* load profile fields. */
1245 String fieldPrefix = sonePrefix + "/Profile/Fields/" + profile.getFields().size();
1246 String fieldName = configuration.getStringValue(fieldPrefix + "/Name").getValue(null);
1247 if (fieldName == null) {
1250 String fieldValue = configuration.getStringValue(fieldPrefix + "/Value").getValue("");
1251 profile.addField(fieldName).setValue(fieldValue);
1255 Set<Post> posts = new HashSet<Post>();
1257 String postPrefix = sonePrefix + "/Posts/" + posts.size();
1258 String postId = configuration.getStringValue(postPrefix + "/ID").getValue(null);
1259 if (postId == null) {
1262 String postRecipientId = configuration.getStringValue(postPrefix + "/Recipient").getValue(null);
1263 long postTime = configuration.getLongValue(postPrefix + "/Time").getValue((long) 0);
1264 String postText = configuration.getStringValue(postPrefix + "/Text").getValue(null);
1265 if ((postTime == 0) || (postText == null)) {
1266 logger.log(Level.WARNING, "Invalid post found, aborting load!");
1269 Post post = getPost(postId, true).setSone(sone).setTime(postTime).setText(postText);
1270 if ((postRecipientId != null) && (postRecipientId.length() == 43)) {
1271 post.setRecipient(getSone(postRecipientId));
1277 Set<PostReply> replies = new HashSet<PostReply>();
1279 String replyPrefix = sonePrefix + "/Replies/" + replies.size();
1280 String replyId = configuration.getStringValue(replyPrefix + "/ID").getValue(null);
1281 if (replyId == null) {
1284 String postId = configuration.getStringValue(replyPrefix + "/Post/ID").getValue(null);
1285 long replyTime = configuration.getLongValue(replyPrefix + "/Time").getValue((long) 0);
1286 String replyText = configuration.getStringValue(replyPrefix + "/Text").getValue(null);
1287 if ((postId == null) || (replyTime == 0) || (replyText == null)) {
1288 logger.log(Level.WARNING, "Invalid reply found, aborting load!");
1291 replies.add(getReply(replyId).setSone(sone).setPost(getPost(postId, true)).setTime(replyTime).setText(replyText));
1294 /* load post likes. */
1295 Set<String> likedPostIds = new HashSet<String>();
1297 String likedPostId = configuration.getStringValue(sonePrefix + "/Likes/Post/" + likedPostIds.size() + "/ID").getValue(null);
1298 if (likedPostId == null) {
1301 likedPostIds.add(likedPostId);
1304 /* load reply likes. */
1305 Set<String> likedReplyIds = new HashSet<String>();
1307 String likedReplyId = configuration.getStringValue(sonePrefix + "/Likes/Reply/" + likedReplyIds.size() + "/ID").getValue(null);
1308 if (likedReplyId == null) {
1311 likedReplyIds.add(likedReplyId);
1315 Set<String> friends = new HashSet<String>();
1317 String friendId = configuration.getStringValue(sonePrefix + "/Friends/" + friends.size() + "/ID").getValue(null);
1318 if (friendId == null) {
1321 friends.add(friendId);
1325 List<Album> topLevelAlbums = new ArrayList<Album>();
1326 int albumCounter = 0;
1328 String albumPrefix = sonePrefix + "/Albums/" + albumCounter++;
1329 String albumId = configuration.getStringValue(albumPrefix + "/ID").getValue(null);
1330 if (albumId == null) {
1333 String albumTitle = configuration.getStringValue(albumPrefix + "/Title").getValue(null);
1334 String albumDescription = configuration.getStringValue(albumPrefix + "/Description").getValue(null);
1335 String albumParentId = configuration.getStringValue(albumPrefix + "/Parent").getValue(null);
1336 String albumImageId = configuration.getStringValue(albumPrefix + "/AlbumImage").getValue(null);
1337 if ((albumTitle == null) || (albumDescription == null)) {
1338 logger.log(Level.WARNING, "Invalid album found, aborting load!");
1341 Album album = getAlbum(albumId).setSone(sone).setTitle(albumTitle).setDescription(albumDescription).setAlbumImage(albumImageId);
1342 if (albumParentId != null) {
1343 Album parentAlbum = getAlbum(albumParentId, false);
1344 if (parentAlbum == null) {
1345 logger.log(Level.WARNING, String.format("Invalid parent album ID: %s", albumParentId));
1348 parentAlbum.addAlbum(album);
1350 if (!topLevelAlbums.contains(album)) {
1351 topLevelAlbums.add(album);
1357 int imageCounter = 0;
1359 String imagePrefix = sonePrefix + "/Images/" + imageCounter++;
1360 String imageId = configuration.getStringValue(imagePrefix + "/ID").getValue(null);
1361 if (imageId == null) {
1364 String albumId = configuration.getStringValue(imagePrefix + "/Album").getValue(null);
1365 String key = configuration.getStringValue(imagePrefix + "/Key").getValue(null);
1366 String title = configuration.getStringValue(imagePrefix + "/Title").getValue(null);
1367 String description = configuration.getStringValue(imagePrefix + "/Description").getValue(null);
1368 Long creationTime = configuration.getLongValue(imagePrefix + "/CreationTime").getValue(null);
1369 Integer width = configuration.getIntValue(imagePrefix + "/Width").getValue(null);
1370 Integer height = configuration.getIntValue(imagePrefix + "/Height").getValue(null);
1371 if ((albumId == null) || (key == null) || (title == null) || (description == null) || (creationTime == null) || (width == null) || (height == null)) {
1372 logger.log(Level.WARNING, "Invalid image found, aborting load!");
1375 Album album = getAlbum(albumId, false);
1376 if (album == null) {
1377 logger.log(Level.WARNING, "Invalid album image encountered, aborting load!");
1380 Image image = getImage(imageId).setSone(sone).setCreationTime(creationTime).setKey(key);
1381 image.setTitle(title).setDescription(description).setWidth(width).setHeight(height);
1382 album.addImage(image);
1386 String avatarId = configuration.getStringValue(sonePrefix + "/Profile/Avatar").getValue(null);
1387 if (avatarId != null) {
1388 profile.setAvatar(getImage(avatarId, false));
1392 sone.getOptions().getBooleanOption("AutoFollow").set(configuration.getBooleanValue(sonePrefix + "/Options/AutoFollow").getValue(null));
1393 sone.getOptions().getBooleanOption("EnableSoneInsertNotifications").set(configuration.getBooleanValue(sonePrefix + "/Options/EnableSoneInsertNotifications").getValue(null));
1394 sone.getOptions().getBooleanOption("ShowNotification/NewSones").set(configuration.getBooleanValue(sonePrefix + "/Options/ShowNotification/NewSones").getValue(null));
1395 sone.getOptions().getBooleanOption("ShowNotification/NewPosts").set(configuration.getBooleanValue(sonePrefix + "/Options/ShowNotification/NewPosts").getValue(null));
1396 sone.getOptions().getBooleanOption("ShowNotification/NewReplies").set(configuration.getBooleanValue(sonePrefix + "/Options/ShowNotification/NewReplies").getValue(null));
1397 sone.getOptions().<ShowCustomAvatars> getEnumOption("ShowCustomAvatars").set(ShowCustomAvatars.valueOf(configuration.getStringValue(sonePrefix + "/Options/ShowCustomAvatars").getValue(ShowCustomAvatars.NEVER.name())));
1399 /* if we’re still here, Sone was loaded successfully. */
1400 synchronized (sone) {
1401 sone.setTime(soneTime);
1402 sone.setProfile(profile);
1403 sone.setPosts(posts);
1404 sone.setReplies(replies);
1405 sone.setLikePostIds(likedPostIds);
1406 sone.setLikeReplyIds(likedReplyIds);
1407 for (String friendId : friends) {
1408 followSone(sone, friendId);
1410 sone.setAlbums(topLevelAlbums);
1411 soneInserters.get(sone).setLastInsertFingerprint(lastInsertFingerprint);
1413 synchronized (knownSones) {
1414 for (String friend : friends) {
1415 knownSones.add(friend);
1418 synchronized (knownPosts) {
1419 for (Post post : posts) {
1420 knownPosts.add(post.getId());
1423 synchronized (knownReplies) {
1424 for (PostReply reply : replies) {
1425 knownReplies.add(reply.getId());
1431 * Creates a new post.
1434 * The Sone that creates the post
1436 * The recipient Sone, or {@code null} if this post does not have
1439 * The time of the post
1441 * The text of the post
1442 * @return The created post
1444 public Post createPost(Sone sone, Sone recipient, long time, String text) {
1445 Validation.begin().isNotNull("Text", text).check().isGreater("Text Length", text.length(), 0).check();
1446 if (!sone.isLocal()) {
1447 logger.log(Level.FINE, String.format("Tried to create post for non-local Sone: %s", sone));
1450 final Post post = new PostImpl(sone, time, text);
1451 if (recipient != null) {
1452 post.setRecipient(recipient);
1454 synchronized (posts) {
1455 posts.put(post.getId(), post);
1457 coreListenerManager.fireNewPostFound(post);
1459 touchConfiguration();
1460 localElementTicker.registerEvent(System.currentTimeMillis() + 10 * 1000, new Runnable() {
1467 markPostKnown(post);
1469 }, "Mark " + post + " read.");
1474 * Deletes the given post.
1477 * The post to delete
1479 public void deletePost(Post post) {
1480 if (!post.getSone().isLocal()) {
1481 logger.log(Level.WARNING, String.format("Tried to delete post of non-local Sone: %s", post.getSone()));
1484 post.getSone().removePost(post);
1485 synchronized (posts) {
1486 posts.remove(post.getId());
1488 coreListenerManager.firePostRemoved(post);
1489 markPostKnown(post);
1490 touchConfiguration();
1494 * Marks the given post as known, if it is currently not a known post
1495 * (according to {@link Post#isKnown()}).
1498 * The post to mark as known
1500 public void markPostKnown(Post post) {
1501 post.setKnown(true);
1502 synchronized (knownPosts) {
1503 coreListenerManager.fireMarkPostKnown(post);
1504 if (knownPosts.add(post.getId())) {
1505 touchConfiguration();
1508 for (PostReply reply : getReplies(post)) {
1509 markReplyKnown(reply);
1514 * Bookmarks the given post.
1517 * The post to bookmark
1519 public void bookmark(Post post) {
1520 bookmarkPost(post.getId());
1524 * Bookmarks the post with the given ID.
1527 * The ID of the post to bookmark
1529 public void bookmarkPost(String id) {
1530 synchronized (bookmarkedPosts) {
1531 bookmarkedPosts.add(id);
1536 * Removes the given post from the bookmarks.
1539 * The post to unbookmark
1541 public void unbookmark(Post post) {
1542 unbookmarkPost(post.getId());
1546 * Removes the post with the given ID from the bookmarks.
1549 * The ID of the post to unbookmark
1551 public void unbookmarkPost(String id) {
1552 synchronized (bookmarkedPosts) {
1553 bookmarkedPosts.remove(id);
1558 * Creates a new reply.
1561 * The Sone that creates the reply
1563 * The post that this reply refers to
1565 * The text of the reply
1566 * @return The created reply
1568 public PostReply createReply(Sone sone, Post post, String text) {
1569 return createReply(sone, post, System.currentTimeMillis(), text);
1573 * Creates a new reply.
1576 * The Sone that creates the reply
1578 * The post that this reply refers to
1580 * The time of the reply
1582 * The text of the reply
1583 * @return The created reply
1585 public PostReply createReply(Sone sone, Post post, long time, String text) {
1586 Validation.begin().isNotNull("Text", text).check().isGreater("Text Length", text.trim().length(), 0).check();
1587 if (!sone.isLocal()) {
1588 logger.log(Level.FINE, String.format("Tried to create reply for non-local Sone: %s", sone));
1591 final PostReply reply = new PostReply(sone, post, System.currentTimeMillis(), text);
1592 synchronized (replies) {
1593 replies.put(reply.getId(), reply);
1595 synchronized (knownReplies) {
1596 coreListenerManager.fireNewReplyFound(reply);
1598 sone.addReply(reply);
1599 touchConfiguration();
1600 localElementTicker.registerEvent(System.currentTimeMillis() + 10 * 1000, new Runnable() {
1607 markReplyKnown(reply);
1609 }, "Mark " + reply + " read.");
1614 * Deletes the given reply.
1617 * The reply to delete
1619 public void deleteReply(PostReply reply) {
1620 Sone sone = reply.getSone();
1621 if (!sone.isLocal()) {
1622 logger.log(Level.FINE, String.format("Tried to delete non-local reply: %s", reply));
1625 synchronized (replies) {
1626 replies.remove(reply.getId());
1628 synchronized (knownReplies) {
1629 markReplyKnown(reply);
1630 knownReplies.remove(reply.getId());
1632 sone.removeReply(reply);
1633 touchConfiguration();
1637 * Marks the given reply as known, if it is currently not a known reply
1638 * (according to {@link Reply#isKnown()}).
1641 * The reply to mark as known
1643 public void markReplyKnown(PostReply reply) {
1644 reply.setKnown(true);
1645 synchronized (knownReplies) {
1646 coreListenerManager.fireMarkReplyKnown(reply);
1647 if (knownReplies.add(reply.getId())) {
1648 touchConfiguration();
1654 * Creates a new top-level album for the given Sone.
1657 * The Sone to create the album for
1658 * @return The new album
1660 public Album createAlbum(Sone sone) {
1661 return createAlbum(sone, null);
1665 * Creates a new album for the given Sone.
1668 * The Sone to create the album for
1670 * The parent of the album (may be {@code null} to create a
1672 * @return The new album
1674 public Album createAlbum(Sone sone, Album parent) {
1675 Album album = new Album();
1676 synchronized (albums) {
1677 albums.put(album.getId(), album);
1679 album.setSone(sone);
1680 if (parent != null) {
1681 parent.addAlbum(album);
1683 sone.addAlbum(album);
1689 * Deletes the given album. The owner of the album has to be a local Sone,
1690 * and the album has to be {@link Album#isEmpty() empty} to be deleted.
1693 * The album to remove
1695 public void deleteAlbum(Album album) {
1696 Validation.begin().isNotNull("Album", album).check().is("Local Sone", album.getSone().isLocal()).check();
1697 if (!album.isEmpty()) {
1700 if (album.getParent() == null) {
1701 album.getSone().removeAlbum(album);
1703 album.getParent().removeAlbum(album);
1705 synchronized (albums) {
1706 albums.remove(album.getId());
1708 touchConfiguration();
1712 * Creates a new image.
1715 * The Sone creating the image
1717 * The album the image will be inserted into
1718 * @param temporaryImage
1719 * The temporary image to create the image from
1720 * @return The newly created image
1722 public Image createImage(Sone sone, Album album, TemporaryImage temporaryImage) {
1723 Validation.begin().isNotNull("Sone", sone).isNotNull("Album", album).isNotNull("Temporary Image", temporaryImage).check().is("Local Sone", sone.isLocal()).check().isEqual("Owner and Album Owner", sone, album.getSone()).check();
1724 Image image = new Image(temporaryImage.getId()).setSone(sone).setCreationTime(System.currentTimeMillis());
1725 album.addImage(image);
1726 synchronized (images) {
1727 images.put(image.getId(), image);
1729 imageInserter.insertImage(temporaryImage, image);
1734 * Deletes the given image. This method will also delete a matching
1737 * @see #deleteTemporaryImage(TemporaryImage)
1739 * The image to delete
1741 public void deleteImage(Image image) {
1742 Validation.begin().isNotNull("Image", image).check().is("Local Sone", image.getSone().isLocal()).check();
1743 deleteTemporaryImage(image.getId());
1744 image.getAlbum().removeImage(image);
1745 synchronized (images) {
1746 images.remove(image.getId());
1748 touchConfiguration();
1752 * Creates a new temporary image.
1755 * The MIME type of the temporary image
1757 * The encoded data of the image
1758 * @return The temporary image
1760 public TemporaryImage createTemporaryImage(String mimeType, byte[] imageData) {
1761 TemporaryImage temporaryImage = new TemporaryImage();
1762 temporaryImage.setMimeType(mimeType).setImageData(imageData);
1763 synchronized (temporaryImages) {
1764 temporaryImages.put(temporaryImage.getId(), temporaryImage);
1766 return temporaryImage;
1770 * Deletes the given temporary image.
1772 * @param temporaryImage
1773 * The temporary image to delete
1775 public void deleteTemporaryImage(TemporaryImage temporaryImage) {
1776 Validation.begin().isNotNull("Temporary Image", temporaryImage).check();
1777 deleteTemporaryImage(temporaryImage.getId());
1781 * Deletes the temporary image with the given ID.
1784 * The ID of the temporary image to delete
1786 public void deleteTemporaryImage(String imageId) {
1787 Validation.begin().isNotNull("Temporary Image ID", imageId).check();
1788 synchronized (temporaryImages) {
1789 temporaryImages.remove(imageId);
1791 Image image = getImage(imageId, false);
1792 if (image != null) {
1793 imageInserter.cancelImageInsert(image);
1798 * Notifies the core that the configuration, either of the core or of a
1799 * single local Sone, has changed, and that the configuration should be
1802 public void touchConfiguration() {
1803 lastConfigurationUpdate = System.currentTimeMillis();
1814 public void serviceStart() {
1815 loadConfiguration();
1816 updateChecker.addUpdateListener(this);
1817 updateChecker.start();
1818 webOfTrustUpdater.start();
1825 public void serviceRun() {
1826 long lastSaved = System.currentTimeMillis();
1827 while (!shouldStop()) {
1829 long now = System.currentTimeMillis();
1830 if (shouldStop() || ((lastConfigurationUpdate > lastSaved) && ((now - lastConfigurationUpdate) > 5000))) {
1831 for (Sone localSone : getLocalSones()) {
1832 saveSone(localSone);
1834 saveConfiguration();
1844 public void serviceStop() {
1845 for (Entry<Sone, SoneInserter> soneInserter : soneInserters.entrySet()) {
1846 soneInserter.getValue().removeSoneInsertListener(this);
1847 soneInserter.getValue().stop();
1848 saveSone(soneInserter.getKey());
1850 saveConfiguration();
1851 webOfTrustUpdater.stop();
1852 updateChecker.stop();
1853 updateChecker.removeUpdateListener(this);
1854 soneDownloader.stop();
1862 * Saves the given Sone. This will persist all local settings for the given
1863 * Sone, such as the friends list and similar, private options.
1868 private synchronized void saveSone(Sone sone) {
1869 if (!sone.isLocal()) {
1870 logger.log(Level.FINE, String.format("Tried to save non-local Sone: %s", sone));
1873 if (!(sone.getIdentity() instanceof OwnIdentity)) {
1874 logger.log(Level.WARNING, String.format("Local Sone without OwnIdentity found, refusing to save: %s", sone));
1878 logger.log(Level.INFO, String.format("Saving Sone: %s", sone));
1880 /* save Sone into configuration. */
1881 String sonePrefix = "Sone/" + sone.getId();
1882 configuration.getLongValue(sonePrefix + "/Time").setValue(sone.getTime());
1883 configuration.getStringValue(sonePrefix + "/LastInsertFingerprint").setValue(soneInserters.get(sone).getLastInsertFingerprint());
1886 Profile profile = sone.getProfile();
1887 configuration.getStringValue(sonePrefix + "/Profile/FirstName").setValue(profile.getFirstName());
1888 configuration.getStringValue(sonePrefix + "/Profile/MiddleName").setValue(profile.getMiddleName());
1889 configuration.getStringValue(sonePrefix + "/Profile/LastName").setValue(profile.getLastName());
1890 configuration.getIntValue(sonePrefix + "/Profile/BirthDay").setValue(profile.getBirthDay());
1891 configuration.getIntValue(sonePrefix + "/Profile/BirthMonth").setValue(profile.getBirthMonth());
1892 configuration.getIntValue(sonePrefix + "/Profile/BirthYear").setValue(profile.getBirthYear());
1893 configuration.getStringValue(sonePrefix + "/Profile/Avatar").setValue(profile.getAvatar());
1895 /* save profile fields. */
1896 int fieldCounter = 0;
1897 for (Field profileField : profile.getFields()) {
1898 String fieldPrefix = sonePrefix + "/Profile/Fields/" + fieldCounter++;
1899 configuration.getStringValue(fieldPrefix + "/Name").setValue(profileField.getName());
1900 configuration.getStringValue(fieldPrefix + "/Value").setValue(profileField.getValue());
1902 configuration.getStringValue(sonePrefix + "/Profile/Fields/" + fieldCounter + "/Name").setValue(null);
1905 int postCounter = 0;
1906 for (Post post : sone.getPosts()) {
1907 String postPrefix = sonePrefix + "/Posts/" + postCounter++;
1908 configuration.getStringValue(postPrefix + "/ID").setValue(post.getId());
1909 configuration.getStringValue(postPrefix + "/Recipient").setValue((post.getRecipient() != null) ? post.getRecipient().getId() : null);
1910 configuration.getLongValue(postPrefix + "/Time").setValue(post.getTime());
1911 configuration.getStringValue(postPrefix + "/Text").setValue(post.getText());
1913 configuration.getStringValue(sonePrefix + "/Posts/" + postCounter + "/ID").setValue(null);
1916 int replyCounter = 0;
1917 for (PostReply reply : sone.getReplies()) {
1918 String replyPrefix = sonePrefix + "/Replies/" + replyCounter++;
1919 configuration.getStringValue(replyPrefix + "/ID").setValue(reply.getId());
1920 configuration.getStringValue(replyPrefix + "/Post/ID").setValue(reply.getPost().getId());
1921 configuration.getLongValue(replyPrefix + "/Time").setValue(reply.getTime());
1922 configuration.getStringValue(replyPrefix + "/Text").setValue(reply.getText());
1924 configuration.getStringValue(sonePrefix + "/Replies/" + replyCounter + "/ID").setValue(null);
1926 /* save post likes. */
1927 int postLikeCounter = 0;
1928 for (String postId : sone.getLikedPostIds()) {
1929 configuration.getStringValue(sonePrefix + "/Likes/Post/" + postLikeCounter++ + "/ID").setValue(postId);
1931 configuration.getStringValue(sonePrefix + "/Likes/Post/" + postLikeCounter + "/ID").setValue(null);
1933 /* save reply likes. */
1934 int replyLikeCounter = 0;
1935 for (String replyId : sone.getLikedReplyIds()) {
1936 configuration.getStringValue(sonePrefix + "/Likes/Reply/" + replyLikeCounter++ + "/ID").setValue(replyId);
1938 configuration.getStringValue(sonePrefix + "/Likes/Reply/" + replyLikeCounter + "/ID").setValue(null);
1941 int friendCounter = 0;
1942 for (String friendId : sone.getFriends()) {
1943 configuration.getStringValue(sonePrefix + "/Friends/" + friendCounter++ + "/ID").setValue(friendId);
1945 configuration.getStringValue(sonePrefix + "/Friends/" + friendCounter + "/ID").setValue(null);
1947 /* save albums. first, collect in a flat structure, top-level first. */
1948 List<Album> albums = sone.getAllAlbums();
1950 int albumCounter = 0;
1951 for (Album album : albums) {
1952 String albumPrefix = sonePrefix + "/Albums/" + albumCounter++;
1953 configuration.getStringValue(albumPrefix + "/ID").setValue(album.getId());
1954 configuration.getStringValue(albumPrefix + "/Title").setValue(album.getTitle());
1955 configuration.getStringValue(albumPrefix + "/Description").setValue(album.getDescription());
1956 configuration.getStringValue(albumPrefix + "/Parent").setValue(album.getParent() == null ? null : album.getParent().getId());
1957 configuration.getStringValue(albumPrefix + "/AlbumImage").setValue(album.getAlbumImage() == null ? null : album.getAlbumImage().getId());
1959 configuration.getStringValue(sonePrefix + "/Albums/" + albumCounter + "/ID").setValue(null);
1962 int imageCounter = 0;
1963 for (Album album : albums) {
1964 for (Image image : album.getImages()) {
1965 if (!image.isInserted()) {
1968 String imagePrefix = sonePrefix + "/Images/" + imageCounter++;
1969 configuration.getStringValue(imagePrefix + "/ID").setValue(image.getId());
1970 configuration.getStringValue(imagePrefix + "/Album").setValue(album.getId());
1971 configuration.getStringValue(imagePrefix + "/Key").setValue(image.getKey());
1972 configuration.getStringValue(imagePrefix + "/Title").setValue(image.getTitle());
1973 configuration.getStringValue(imagePrefix + "/Description").setValue(image.getDescription());
1974 configuration.getLongValue(imagePrefix + "/CreationTime").setValue(image.getCreationTime());
1975 configuration.getIntValue(imagePrefix + "/Width").setValue(image.getWidth());
1976 configuration.getIntValue(imagePrefix + "/Height").setValue(image.getHeight());
1979 configuration.getStringValue(sonePrefix + "/Images/" + imageCounter + "/ID").setValue(null);
1982 configuration.getBooleanValue(sonePrefix + "/Options/AutoFollow").setValue(sone.getOptions().getBooleanOption("AutoFollow").getReal());
1983 configuration.getBooleanValue(sonePrefix + "/Options/ShowNotification/NewSones").setValue(sone.getOptions().getBooleanOption("ShowNotification/NewSones").getReal());
1984 configuration.getBooleanValue(sonePrefix + "/Options/ShowNotification/NewPosts").setValue(sone.getOptions().getBooleanOption("ShowNotification/NewPosts").getReal());
1985 configuration.getBooleanValue(sonePrefix + "/Options/ShowNotification/NewReplies").setValue(sone.getOptions().getBooleanOption("ShowNotification/NewReplies").getReal());
1986 configuration.getBooleanValue(sonePrefix + "/Options/EnableSoneInsertNotifications").setValue(sone.getOptions().getBooleanOption("EnableSoneInsertNotifications").getReal());
1987 configuration.getStringValue(sonePrefix + "/Options/ShowCustomAvatars").setValue(sone.getOptions().<ShowCustomAvatars> getEnumOption("ShowCustomAvatars").get().name());
1989 configuration.save();
1991 webOfTrustUpdater.setProperty((OwnIdentity) sone.getIdentity(), "Sone.LatestEdition", String.valueOf(sone.getLatestEdition()));
1993 logger.log(Level.INFO, String.format("Sone %s saved.", sone));
1994 } catch (ConfigurationException ce1) {
1995 logger.log(Level.WARNING, String.format("Could not save Sone: %s", sone), ce1);
2000 * Saves the current options.
2002 private void saveConfiguration() {
2003 synchronized (configuration) {
2004 if (storingConfiguration) {
2005 logger.log(Level.FINE, "Already storing configuration…");
2008 storingConfiguration = true;
2011 /* store the options first. */
2013 configuration.getIntValue("Option/ConfigurationVersion").setValue(0);
2014 configuration.getIntValue("Option/InsertionDelay").setValue(options.getIntegerOption("InsertionDelay").getReal());
2015 configuration.getIntValue("Option/PostsPerPage").setValue(options.getIntegerOption("PostsPerPage").getReal());
2016 configuration.getIntValue("Option/ImagesPerPage").setValue(options.getIntegerOption("ImagesPerPage").getReal());
2017 configuration.getIntValue("Option/CharactersPerPost").setValue(options.getIntegerOption("CharactersPerPost").getReal());
2018 configuration.getIntValue("Option/PostCutOffLength").setValue(options.getIntegerOption("PostCutOffLength").getReal());
2019 configuration.getBooleanValue("Option/RequireFullAccess").setValue(options.getBooleanOption("RequireFullAccess").getReal());
2020 configuration.getIntValue("Option/PositiveTrust").setValue(options.getIntegerOption("PositiveTrust").getReal());
2021 configuration.getIntValue("Option/NegativeTrust").setValue(options.getIntegerOption("NegativeTrust").getReal());
2022 configuration.getStringValue("Option/TrustComment").setValue(options.getStringOption("TrustComment").getReal());
2023 configuration.getBooleanValue("Option/ActivateFcpInterface").setValue(options.getBooleanOption("ActivateFcpInterface").getReal());
2024 configuration.getIntValue("Option/FcpFullAccessRequired").setValue(options.getIntegerOption("FcpFullAccessRequired").getReal());
2026 /* save known Sones. */
2027 int soneCounter = 0;
2028 synchronized (knownSones) {
2029 for (String knownSoneId : knownSones) {
2030 configuration.getStringValue("KnownSone/" + soneCounter++ + "/ID").setValue(knownSoneId);
2032 configuration.getStringValue("KnownSone/" + soneCounter + "/ID").setValue(null);
2035 /* save Sone following times. */
2037 synchronized (soneFollowingTimes) {
2038 for (Entry<Sone, Long> soneFollowingTime : soneFollowingTimes.entrySet()) {
2039 configuration.getStringValue("SoneFollowingTimes/" + soneCounter + "/Sone").setValue(soneFollowingTime.getKey().getId());
2040 configuration.getLongValue("SoneFollowingTimes/" + soneCounter + "/Time").setValue(soneFollowingTime.getValue());
2043 configuration.getStringValue("SoneFollowingTimes/" + soneCounter + "/Sone").setValue(null);
2046 /* save known posts. */
2047 int postCounter = 0;
2048 synchronized (knownPosts) {
2049 for (String knownPostId : knownPosts) {
2050 configuration.getStringValue("KnownPosts/" + postCounter++ + "/ID").setValue(knownPostId);
2052 configuration.getStringValue("KnownPosts/" + postCounter + "/ID").setValue(null);
2055 /* save known replies. */
2056 int replyCounter = 0;
2057 synchronized (knownReplies) {
2058 for (String knownReplyId : knownReplies) {
2059 configuration.getStringValue("KnownReplies/" + replyCounter++ + "/ID").setValue(knownReplyId);
2061 configuration.getStringValue("KnownReplies/" + replyCounter + "/ID").setValue(null);
2064 /* save bookmarked posts. */
2065 int bookmarkedPostCounter = 0;
2066 synchronized (bookmarkedPosts) {
2067 for (String bookmarkedPostId : bookmarkedPosts) {
2068 configuration.getStringValue("Bookmarks/Post/" + bookmarkedPostCounter++ + "/ID").setValue(bookmarkedPostId);
2071 configuration.getStringValue("Bookmarks/Post/" + bookmarkedPostCounter++ + "/ID").setValue(null);
2074 configuration.save();
2076 } catch (ConfigurationException ce1) {
2077 logger.log(Level.SEVERE, "Could not store configuration!", ce1);
2079 synchronized (configuration) {
2080 storingConfiguration = false;
2086 * Loads the configuration.
2088 @SuppressWarnings("unchecked")
2089 private void loadConfiguration() {
2090 /* create options. */
2091 options.addIntegerOption("InsertionDelay", new DefaultOption<Integer>(60, new IntegerRangeValidator(0, Integer.MAX_VALUE), new OptionWatcher<Integer>() {
2094 public void optionChanged(Option<Integer> option, Integer oldValue, Integer newValue) {
2095 SoneInserter.setInsertionDelay(newValue);
2099 options.addIntegerOption("PostsPerPage", new DefaultOption<Integer>(10, new IntegerRangeValidator(1, Integer.MAX_VALUE)));
2100 options.addIntegerOption("ImagesPerPage", new DefaultOption<Integer>(9, new IntegerRangeValidator(1, Integer.MAX_VALUE)));
2101 options.addIntegerOption("CharactersPerPost", new DefaultOption<Integer>(400, new OrValidator<Integer>(new IntegerRangeValidator(50, Integer.MAX_VALUE), new EqualityValidator<Integer>(-1))));
2102 options.addIntegerOption("PostCutOffLength", new DefaultOption<Integer>(200, new OrValidator<Integer>(new IntegerRangeValidator(50, Integer.MAX_VALUE), new EqualityValidator<Integer>(-1))));
2103 options.addBooleanOption("RequireFullAccess", new DefaultOption<Boolean>(false));
2104 options.addIntegerOption("PositiveTrust", new DefaultOption<Integer>(75, new IntegerRangeValidator(0, 100)));
2105 options.addIntegerOption("NegativeTrust", new DefaultOption<Integer>(-25, new IntegerRangeValidator(-100, 100)));
2106 options.addStringOption("TrustComment", new DefaultOption<String>("Set from Sone Web Interface"));
2107 options.addBooleanOption("ActivateFcpInterface", new DefaultOption<Boolean>(false, new OptionWatcher<Boolean>() {
2110 @SuppressWarnings("synthetic-access")
2111 public void optionChanged(Option<Boolean> option, Boolean oldValue, Boolean newValue) {
2112 fcpInterface.setActive(newValue);
2115 options.addIntegerOption("FcpFullAccessRequired", new DefaultOption<Integer>(2, new OptionWatcher<Integer>() {
2118 @SuppressWarnings("synthetic-access")
2119 public void optionChanged(Option<Integer> option, Integer oldValue, Integer newValue) {
2120 fcpInterface.setFullAccessRequired(FullAccessRequired.values()[newValue]);
2125 loadConfigurationValue("InsertionDelay");
2126 loadConfigurationValue("PostsPerPage");
2127 loadConfigurationValue("ImagesPerPage");
2128 loadConfigurationValue("CharactersPerPost");
2129 loadConfigurationValue("PostCutOffLength");
2130 options.getBooleanOption("RequireFullAccess").set(configuration.getBooleanValue("Option/RequireFullAccess").getValue(null));
2131 loadConfigurationValue("PositiveTrust");
2132 loadConfigurationValue("NegativeTrust");
2133 options.getStringOption("TrustComment").set(configuration.getStringValue("Option/TrustComment").getValue(null));
2134 options.getBooleanOption("ActivateFcpInterface").set(configuration.getBooleanValue("Option/ActivateFcpInterface").getValue(null));
2135 options.getIntegerOption("FcpFullAccessRequired").set(configuration.getIntValue("Option/FcpFullAccessRequired").getValue(null));
2137 /* load known Sones. */
2138 int soneCounter = 0;
2140 String knownSoneId = configuration.getStringValue("KnownSone/" + soneCounter++ + "/ID").getValue(null);
2141 if (knownSoneId == null) {
2144 synchronized (knownSones) {
2145 knownSones.add(knownSoneId);
2149 /* load Sone following times. */
2152 String soneId = configuration.getStringValue("SoneFollowingTimes/" + soneCounter + "/Sone").getValue(null);
2153 if (soneId == null) {
2156 long time = configuration.getLongValue("SoneFollowingTimes/" + soneCounter + "/Time").getValue(Long.MAX_VALUE);
2157 Sone followedSone = getSone(soneId);
2158 if (followedSone == null) {
2159 logger.log(Level.WARNING, String.format("Ignoring Sone with invalid ID: %s", soneId));
2161 synchronized (soneFollowingTimes) {
2162 soneFollowingTimes.put(getSone(soneId), time);
2168 /* load known posts. */
2169 int postCounter = 0;
2171 String knownPostId = configuration.getStringValue("KnownPosts/" + postCounter++ + "/ID").getValue(null);
2172 if (knownPostId == null) {
2175 synchronized (knownPosts) {
2176 knownPosts.add(knownPostId);
2180 /* load known replies. */
2181 int replyCounter = 0;
2183 String knownReplyId = configuration.getStringValue("KnownReplies/" + replyCounter++ + "/ID").getValue(null);
2184 if (knownReplyId == null) {
2187 synchronized (knownReplies) {
2188 knownReplies.add(knownReplyId);
2192 /* load bookmarked posts. */
2193 int bookmarkedPostCounter = 0;
2195 String bookmarkedPostId = configuration.getStringValue("Bookmarks/Post/" + bookmarkedPostCounter++ + "/ID").getValue(null);
2196 if (bookmarkedPostId == null) {
2199 synchronized (bookmarkedPosts) {
2200 bookmarkedPosts.add(bookmarkedPostId);
2207 * Loads an {@link Integer} configuration value for the option with the
2208 * given name, logging validation failures.
2211 * The name of the option to load
2213 private void loadConfigurationValue(String optionName) {
2215 options.getIntegerOption(optionName).set(configuration.getIntValue("Option/" + optionName).getValue(null));
2216 } catch (IllegalArgumentException iae1) {
2217 logger.log(Level.WARNING, String.format("Invalid value for %s in configuration, using default.", optionName));
2222 * Generate a Sone URI from the given URI and latest edition.
2225 * The URI to derive the Sone URI from
2226 * @return The derived URI
2228 private static FreenetURI getSoneUri(String uriString) {
2230 FreenetURI uri = new FreenetURI(uriString).setDocName("Sone").setMetaString(new String[0]);
2232 } catch (MalformedURLException mue1) {
2233 logger.log(Level.WARNING, String.format("Could not create Sone URI from URI: %s", uriString), mue1);
2239 // INTERFACE IdentityListener
2246 public void ownIdentityAdded(OwnIdentity ownIdentity) {
2247 logger.log(Level.FINEST, String.format("Adding OwnIdentity: %s", ownIdentity));
2248 if (ownIdentity.hasContext("Sone")) {
2249 trustedIdentities.put(ownIdentity, Collections.synchronizedSet(new HashSet<Identity>()));
2250 addLocalSone(ownIdentity);
2258 public void ownIdentityRemoved(OwnIdentity ownIdentity) {
2259 logger.log(Level.FINEST, String.format("Removing OwnIdentity: %s", ownIdentity));
2260 trustedIdentities.remove(ownIdentity);
2267 public void identityAdded(OwnIdentity ownIdentity, Identity identity) {
2268 logger.log(Level.FINEST, String.format("Adding Identity: %s", identity));
2269 trustedIdentities.get(ownIdentity).add(identity);
2270 addRemoteSone(identity);
2277 public void identityUpdated(OwnIdentity ownIdentity, final Identity identity) {
2278 soneDownloaders.execute(new Runnable() {
2281 @SuppressWarnings("synthetic-access")
2283 Sone sone = getRemoteSone(identity.getId(), false);
2284 sone.setIdentity(identity);
2285 sone.setLatestEdition(Numbers.safeParseLong(identity.getProperty("Sone.LatestEdition"), sone.getLatestEdition()));
2286 soneDownloader.addSone(sone);
2287 soneDownloader.fetchSone(sone);
2296 public void identityRemoved(OwnIdentity ownIdentity, Identity identity) {
2297 trustedIdentities.get(ownIdentity).remove(identity);
2298 boolean foundIdentity = false;
2299 for (Entry<OwnIdentity, Set<Identity>> trustedIdentity : trustedIdentities.entrySet()) {
2300 if (trustedIdentity.getKey().equals(ownIdentity)) {
2303 if (trustedIdentity.getValue().contains(identity)) {
2304 foundIdentity = true;
2307 if (foundIdentity) {
2308 /* some local identity still trusts this identity, don’t remove. */
2311 Sone sone = getSone(identity.getId(), false);
2313 /* TODO - we don’t have the Sone anymore. should this happen? */
2316 synchronized (posts) {
2317 synchronized (knownPosts) {
2318 for (Post post : sone.getPosts()) {
2319 posts.remove(post.getId());
2320 coreListenerManager.firePostRemoved(post);
2324 synchronized (replies) {
2325 synchronized (knownReplies) {
2326 for (PostReply reply : sone.getReplies()) {
2327 replies.remove(reply.getId());
2328 coreListenerManager.fireReplyRemoved(reply);
2332 database.removeSone(identity.getId());
2333 coreListenerManager.fireSoneRemoved(sone);
2337 // INTERFACE UpdateListener
2344 public void updateFound(Version version, long releaseTime, long latestEdition) {
2345 coreListenerManager.fireUpdateFound(version, releaseTime, latestEdition);
2349 // INTERFACE ImageInsertListener
2356 public void insertStarted(Sone sone) {
2357 coreListenerManager.fireSoneInserting(sone);
2364 public void insertFinished(Sone sone, long insertDuration) {
2365 coreListenerManager.fireSoneInserted(sone, insertDuration);
2372 public void insertAborted(Sone sone, Throwable cause) {
2373 coreListenerManager.fireSoneInsertAborted(sone, cause);
2377 // SONEINSERTLISTENER METHODS
2384 public void imageInsertStarted(Image image) {
2385 logger.log(Level.WARNING, String.format("Image insert started for %s...", image));
2386 coreListenerManager.fireImageInsertStarted(image);
2393 public void imageInsertAborted(Image image) {
2394 logger.log(Level.WARNING, String.format("Image insert aborted for %s.", image));
2395 coreListenerManager.fireImageInsertAborted(image);
2402 public void imageInsertFinished(Image image, FreenetURI key) {
2403 logger.log(Level.WARNING, String.format("Image insert finished for %s: %s", image, key));
2404 image.setKey(key.toString());
2405 deleteTemporaryImage(image.getId());
2406 touchConfiguration();
2407 coreListenerManager.fireImageInsertFinished(image);
2414 public void imageInsertFailed(Image image, Throwable cause) {
2415 logger.log(Level.WARNING, String.format("Image insert failed for %s." + image), cause);
2416 coreListenerManager.fireImageInsertFailed(image, cause);
2420 * Convenience interface for external classes that want to access the core’s
2423 * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
2425 public static class Preferences {
2427 /** The wrapped options. */
2428 private final Options options;
2431 * Creates a new preferences object wrapped around the given options.
2434 * The options to wrap
2436 public Preferences(Options options) {
2437 this.options = options;
2441 * Returns the insertion delay.
2443 * @return The insertion delay
2445 public int getInsertionDelay() {
2446 return options.getIntegerOption("InsertionDelay").get();
2450 * Validates the given insertion delay.
2452 * @param insertionDelay
2453 * The insertion delay to validate
2454 * @return {@code true} if the given insertion delay was valid,
2455 * {@code false} otherwise
2457 public boolean validateInsertionDelay(Integer insertionDelay) {
2458 return options.getIntegerOption("InsertionDelay").validate(insertionDelay);
2462 * Sets the insertion delay
2464 * @param insertionDelay
2465 * The new insertion delay, or {@code null} to restore it to
2467 * @return This preferences
2469 public Preferences setInsertionDelay(Integer insertionDelay) {
2470 options.getIntegerOption("InsertionDelay").set(insertionDelay);
2475 * Returns the number of posts to show per page.
2477 * @return The number of posts to show per page
2479 public int getPostsPerPage() {
2480 return options.getIntegerOption("PostsPerPage").get();
2484 * Validates the number of posts per page.
2486 * @param postsPerPage
2487 * The number of posts per page
2488 * @return {@code true} if the number of posts per page was valid,
2489 * {@code false} otherwise
2491 public boolean validatePostsPerPage(Integer postsPerPage) {
2492 return options.getIntegerOption("PostsPerPage").validate(postsPerPage);
2496 * Sets the number of posts to show per page.
2498 * @param postsPerPage
2499 * The number of posts to show per page
2500 * @return This preferences object
2502 public Preferences setPostsPerPage(Integer postsPerPage) {
2503 options.getIntegerOption("PostsPerPage").set(postsPerPage);
2508 * Returns the number of images to show per page.
2510 * @return The number of images to show per page
2512 public int getImagesPerPage() {
2513 return options.getIntegerOption("ImagesPerPage").get();
2517 * Validates the number of images per page.
2519 * @param imagesPerPage
2520 * The number of images per page
2521 * @return {@code true} if the number of images per page was valid,
2522 * {@code false} otherwise
2524 public boolean validateImagesPerPage(Integer imagesPerPage) {
2525 return options.getIntegerOption("ImagesPerPage").validate(imagesPerPage);
2529 * Sets the number of images per page.
2531 * @param imagesPerPage
2532 * The number of images per page
2533 * @return This preferences object
2535 public Preferences setImagesPerPage(Integer imagesPerPage) {
2536 options.getIntegerOption("ImagesPerPage").set(imagesPerPage);
2541 * Returns the number of characters per post, or <code>-1</code> if the
2542 * posts should not be cut off.
2544 * @return The numbers of characters per post
2546 public int getCharactersPerPost() {
2547 return options.getIntegerOption("CharactersPerPost").get();
2551 * Validates the number of characters per post.
2553 * @param charactersPerPost
2554 * The number of characters per post
2555 * @return {@code true} if the number of characters per post was valid,
2556 * {@code false} otherwise
2558 public boolean validateCharactersPerPost(Integer charactersPerPost) {
2559 return options.getIntegerOption("CharactersPerPost").validate(charactersPerPost);
2563 * Sets the number of characters per post.
2565 * @param charactersPerPost
2566 * The number of characters per post, or <code>-1</code> to
2567 * not cut off the posts
2568 * @return This preferences objects
2570 public Preferences setCharactersPerPost(Integer charactersPerPost) {
2571 options.getIntegerOption("CharactersPerPost").set(charactersPerPost);
2576 * Returns the number of characters the shortened post should have.
2578 * @return The number of characters of the snippet
2580 public int getPostCutOffLength() {
2581 return options.getIntegerOption("PostCutOffLength").get();
2585 * Validates the number of characters after which to cut off the post.
2587 * @param postCutOffLength
2588 * The number of characters of the snippet
2589 * @return {@code true} if the number of characters of the snippet is
2590 * valid, {@code false} otherwise
2592 public boolean validatePostCutOffLength(Integer postCutOffLength) {
2593 return options.getIntegerOption("PostCutOffLength").validate(postCutOffLength);
2597 * Sets the number of characters the shortened post should have.
2599 * @param postCutOffLength
2600 * The number of characters of the snippet
2601 * @return This preferences
2603 public Preferences setPostCutOffLength(Integer postCutOffLength) {
2604 options.getIntegerOption("PostCutOffLength").set(postCutOffLength);
2609 * Returns whether Sone requires full access to be even visible.
2611 * @return {@code true} if Sone requires full access, {@code false}
2614 public boolean isRequireFullAccess() {
2615 return options.getBooleanOption("RequireFullAccess").get();
2619 * Sets whether Sone requires full access to be even visible.
2621 * @param requireFullAccess
2622 * {@code true} if Sone requires full access, {@code false}
2625 public void setRequireFullAccess(Boolean requireFullAccess) {
2626 options.getBooleanOption("RequireFullAccess").set(requireFullAccess);
2630 * Returns the positive trust.
2632 * @return The positive trust
2634 public int getPositiveTrust() {
2635 return options.getIntegerOption("PositiveTrust").get();
2639 * Validates the positive trust.
2641 * @param positiveTrust
2642 * The positive trust to validate
2643 * @return {@code true} if the positive trust was valid, {@code false}
2646 public boolean validatePositiveTrust(Integer positiveTrust) {
2647 return options.getIntegerOption("PositiveTrust").validate(positiveTrust);
2651 * Sets the positive trust.
2653 * @param positiveTrust
2654 * The new positive trust, or {@code null} to restore it to
2656 * @return This preferences
2658 public Preferences setPositiveTrust(Integer positiveTrust) {
2659 options.getIntegerOption("PositiveTrust").set(positiveTrust);
2664 * Returns the negative trust.
2666 * @return The negative trust
2668 public int getNegativeTrust() {
2669 return options.getIntegerOption("NegativeTrust").get();
2673 * Validates the negative trust.
2675 * @param negativeTrust
2676 * The negative trust to validate
2677 * @return {@code true} if the negative trust was valid, {@code false}
2680 public boolean validateNegativeTrust(Integer negativeTrust) {
2681 return options.getIntegerOption("NegativeTrust").validate(negativeTrust);
2685 * Sets the negative trust.
2687 * @param negativeTrust
2688 * The negative trust, or {@code null} to restore it to the
2690 * @return The preferences
2692 public Preferences setNegativeTrust(Integer negativeTrust) {
2693 options.getIntegerOption("NegativeTrust").set(negativeTrust);
2698 * Returns the trust comment. This is the comment that is set in the web
2699 * of trust when a trust value is assigned to an identity.
2701 * @return The trust comment
2703 public String getTrustComment() {
2704 return options.getStringOption("TrustComment").get();
2708 * Sets the trust comment.
2710 * @param trustComment
2711 * The trust comment, or {@code null} to restore it to the
2713 * @return This preferences
2715 public Preferences setTrustComment(String trustComment) {
2716 options.getStringOption("TrustComment").set(trustComment);
2721 * Returns whether the {@link FcpInterface FCP interface} is currently
2724 * @see FcpInterface#setActive(boolean)
2725 * @return {@code true} if the FCP interface is currently active,
2726 * {@code false} otherwise
2728 public boolean isFcpInterfaceActive() {
2729 return options.getBooleanOption("ActivateFcpInterface").get();
2733 * Sets whether the {@link FcpInterface FCP interface} is currently
2736 * @see FcpInterface#setActive(boolean)
2737 * @param fcpInterfaceActive
2738 * {@code true} to activate the FCP interface, {@code false}
2739 * to deactivate the FCP interface
2740 * @return This preferences object
2742 public Preferences setFcpInterfaceActive(boolean fcpInterfaceActive) {
2743 options.getBooleanOption("ActivateFcpInterface").set(fcpInterfaceActive);
2748 * Returns the action level for which full access to the FCP interface
2751 * @return The action level for which full access to the FCP interface
2754 public FullAccessRequired getFcpFullAccessRequired() {
2755 return FullAccessRequired.values()[options.getIntegerOption("FcpFullAccessRequired").get()];
2759 * Sets the action level for which full access to the FCP interface is
2762 * @param fcpFullAccessRequired
2764 * @return This preferences
2766 public Preferences setFcpFullAccessRequired(FullAccessRequired fcpFullAccessRequired) {
2767 options.getIntegerOption("FcpFullAccessRequired").set((fcpFullAccessRequired != null) ? fcpFullAccessRequired.ordinal() : null);