From: David ‘Bombe’ Roden Date: Tue, 16 Jun 2015 15:54:09 +0000 (+0200) Subject: Merge branch 'release/0.9-rc1' X-Git-Tag: 0.9-rc1^0 X-Git-Url: https://git.pterodactylus.net/?p=Sone.git;a=commitdiff_plain;h=0e8f7804ce344bdd69f5ecc7febe25a60a53561d;hp=012d7828324b0ebe36376d795aee99a63bdc4884 Merge branch 'release/0.9-rc1' --- diff --git a/pom.xml b/pom.xml index e96a971..139d3c1 100644 --- a/pom.xml +++ b/pom.xml @@ -2,7 +2,7 @@ 4.0.0 net.pterodactylus sone - 0.8.9 + 0.9-rc1 net.pterodactylus @@ -22,15 +22,20 @@ test + org.hamcrest + hamcrest-all + 1.3 + + org.freenetproject fred - 0.7.5.1405 + 0.7.5.1467.99.3 provided org.freenetproject freenet-ext - 26 + 29 provided @@ -41,7 +46,7 @@ com.google.guava guava - 14.0-rc1 + 14.0.1 commons-lang @@ -140,6 +145,48 @@
© 2010–2013 David ‘Bombe’ Roden
+ + org.jacoco + jacoco-maven-plugin + 0.7.1.201405082137 + + + default-prepare-agent + + prepare-agent + + + + default-report + prepare-package + + report + + + + default-check + + check + + + + + + BUNDLE + + + + COMPLEXITY + COVEREDRATIO + 0.60 + + + + + + + + diff --git a/src/main/java/net/pterodactylus/sone/core/ConfigurationSoneParser.java b/src/main/java/net/pterodactylus/sone/core/ConfigurationSoneParser.java new file mode 100644 index 0000000..a29856b --- /dev/null +++ b/src/main/java/net/pterodactylus/sone/core/ConfigurationSoneParser.java @@ -0,0 +1,312 @@ +package net.pterodactylus.sone.core; + +import static java.util.Collections.unmodifiableMap; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.annotation.Nullable; + +import net.pterodactylus.sone.data.Album; +import net.pterodactylus.sone.data.Image; +import net.pterodactylus.sone.data.Post; +import net.pterodactylus.sone.data.PostReply; +import net.pterodactylus.sone.data.Profile; +import net.pterodactylus.sone.data.Sone; +import net.pterodactylus.sone.database.AlbumBuilderFactory; +import net.pterodactylus.sone.database.ImageBuilderFactory; +import net.pterodactylus.sone.database.PostBuilder; +import net.pterodactylus.sone.database.PostBuilderFactory; +import net.pterodactylus.sone.database.PostReplyBuilder; +import net.pterodactylus.sone.database.PostReplyBuilderFactory; +import net.pterodactylus.util.config.Configuration; + +/** + * Parses a {@link Sone}’s data from a {@link Configuration}. + * + * @author David ‘Bombe’ Roden + */ +public class ConfigurationSoneParser { + + private final Configuration configuration; + private final Sone sone; + private final String sonePrefix; + private final Map albums = new HashMap(); + private final List topLevelAlbums = new ArrayList(); + private final Map images = new HashMap(); + + public ConfigurationSoneParser(Configuration configuration, Sone sone) { + this.configuration = configuration; + this.sone = sone; + sonePrefix = "Sone/" + sone.getId(); + } + + public Profile parseProfile() { + Profile profile = new Profile(sone); + profile.setFirstName(getString("/Profile/FirstName", null)); + profile.setMiddleName(getString("/Profile/MiddleName", null)); + profile.setLastName(getString("/Profile/LastName", null)); + profile.setBirthDay(getInt("/Profile/BirthDay", null)); + profile.setBirthMonth(getInt("/Profile/BirthMonth", null)); + profile.setBirthYear(getInt("/Profile/BirthYear", null)); + + /* load profile fields. */ + int fieldCount = 0; + while (true) { + String fieldPrefix = "/Profile/Fields/" + fieldCount++; + String fieldName = getString(fieldPrefix + "/Name", null); + if (fieldName == null) { + break; + } + String fieldValue = getString(fieldPrefix + "/Value", ""); + profile.addField(fieldName).setValue(fieldValue); + } + + return profile; + } + + private String getString(String nodeName, @Nullable String defaultValue) { + return configuration.getStringValue(sonePrefix + nodeName) + .getValue(defaultValue); + } + + private Integer getInt(String nodeName, @Nullable Integer defaultValue) { + return configuration.getIntValue(sonePrefix + nodeName) + .getValue(defaultValue); + } + + private Long getLong(String nodeName, @Nullable Long defaultValue) { + return configuration.getLongValue(sonePrefix + nodeName) + .getValue(defaultValue); + } + + public Set parsePosts(PostBuilderFactory postBuilderFactory) + throws InvalidPostFound { + Set posts = new HashSet(); + while (true) { + String postPrefix = "/Posts/" + posts.size(); + String postId = getString(postPrefix + "/ID", null); + if (postId == null) { + break; + } + long postTime = getLong(postPrefix + "/Time", 0L); + String postText = getString(postPrefix + "/Text", null); + if (postAttributesAreInvalid(postTime, postText)) { + throw new InvalidPostFound(); + } + PostBuilder postBuilder = postBuilderFactory.newPostBuilder() + .withId(postId) + .from(sone.getId()) + .withTime(postTime) + .withText(postText); + String postRecipientId = + getString(postPrefix + "/Recipient", null); + if (postRecipientIsValid(postRecipientId)) { + postBuilder.to(postRecipientId); + } + posts.add(postBuilder.build()); + } + return posts; + } + + private boolean postAttributesAreInvalid(long postTime, String postText) { + return (postTime == 0) || (postText == null); + } + + private boolean postRecipientIsValid(String postRecipientId) { + return (postRecipientId != null) && (postRecipientId.length() == 43); + } + + public Set parsePostReplies( + PostReplyBuilderFactory postReplyBuilderFactory) { + Set replies = new HashSet(); + while (true) { + String replyPrefix = "/Replies/" + replies.size(); + String replyId = getString(replyPrefix + "/ID", null); + if (replyId == null) { + break; + } + String postId = getString(replyPrefix + "/Post/ID", null); + long replyTime = getLong(replyPrefix + "/Time", 0L); + String replyText = getString(replyPrefix + "/Text", null); + if ((postId == null) || (replyTime == 0) || (replyText == null)) { + throw new InvalidPostReplyFound(); + } + PostReplyBuilder postReplyBuilder = postReplyBuilderFactory + .newPostReplyBuilder() + .withId(replyId) + .from(sone.getId()) + .to(postId) + .withTime(replyTime) + .withText(replyText); + replies.add(postReplyBuilder.build()); + } + return replies; + } + + public Set parseLikedPostIds() { + Set likedPostIds = new HashSet(); + while (true) { + String likedPostId = + getString("/Likes/Post/" + likedPostIds.size() + "/ID", + null); + if (likedPostId == null) { + break; + } + likedPostIds.add(likedPostId); + } + return likedPostIds; + } + + public Set parseLikedPostReplyIds() { + Set likedPostReplyIds = new HashSet(); + while (true) { + String likedReplyId = getString( + "/Likes/Reply/" + likedPostReplyIds.size() + "/ID", null); + if (likedReplyId == null) { + break; + } + likedPostReplyIds.add(likedReplyId); + } + return likedPostReplyIds; + } + + public Set parseFriends() { + Set friends = new HashSet(); + while (true) { + String friendId = + getString("/Friends/" + friends.size() + "/ID", null); + if (friendId == null) { + break; + } + friends.add(friendId); + } + return friends; + } + + public List parseTopLevelAlbums( + AlbumBuilderFactory albumBuilderFactory) { + int albumCounter = 0; + while (true) { + String albumPrefix = "/Albums/" + albumCounter++; + String albumId = getString(albumPrefix + "/ID", null); + if (albumId == null) { + break; + } + String albumTitle = getString(albumPrefix + "/Title", null); + String albumDescription = + getString(albumPrefix + "/Description", null); + String albumParentId = getString(albumPrefix + "/Parent", null); + String albumImageId = + getString(albumPrefix + "/AlbumImage", null); + if ((albumTitle == null) || (albumDescription == null)) { + throw new InvalidAlbumFound(); + } + Album album = albumBuilderFactory.newAlbumBuilder() + .withId(albumId) + .by(sone) + .build() + .modify() + .setTitle(albumTitle) + .setDescription(albumDescription) + .setAlbumImage(albumImageId) + .update(); + if (albumParentId != null) { + Album parentAlbum = albums.get(albumParentId); + if (parentAlbum == null) { + throw new InvalidParentAlbumFound(albumParentId); + } + parentAlbum.addAlbum(album); + } else { + topLevelAlbums.add(album); + } + albums.put(albumId, album); + } + return topLevelAlbums; + } + + public Map getAlbums() { + return unmodifiableMap(albums); + } + + public void parseImages(ImageBuilderFactory imageBuilderFactory) { + int imageCounter = 0; + while (true) { + String imagePrefix = "/Images/" + imageCounter++; + String imageId = getString(imagePrefix + "/ID", null); + if (imageId == null) { + break; + } + String albumId = getString(imagePrefix + "/Album", null); + String key = getString(imagePrefix + "/Key", null); + String title = getString(imagePrefix + "/Title", null); + String description = + getString(imagePrefix + "/Description", null); + Long creationTime = getLong(imagePrefix + "/CreationTime", null); + Integer width = getInt(imagePrefix + "/Width", null); + Integer height = getInt(imagePrefix + "/Height", null); + if (albumAttributesAreInvalid(albumId, key, title, description, + creationTime, + width, height)) { + throw new InvalidImageFound(); + } + Album album = albums.get(albumId); + if (album == null) { + throw new InvalidParentAlbumFound(albumId); + } + Image image = imageBuilderFactory.newImageBuilder() + .withId(imageId) + .build() + .modify() + .setSone(sone) + .setCreationTime(creationTime) + .setKey(key) + .setTitle(title) + .setDescription(description) + .setWidth(width) + .setHeight(height) + .update(); + album.addImage(image); + images.put(image.getId(), image); + } + } + + public Map getImages() { + return images; + } + + private boolean albumAttributesAreInvalid(String albumId, String key, + String title, String description, Long creationTime, + Integer width, Integer height) { + return (albumId == null) || (key == null) || (title == null) || ( + description == null) || (creationTime == null) || (width + == null) || (height == null); + } + + public static class InvalidPostFound extends RuntimeException { } + + public static class InvalidPostReplyFound extends RuntimeException { } + + public static class InvalidAlbumFound extends RuntimeException { } + + public static class InvalidParentAlbumFound extends RuntimeException { + + private final String albumParentId; + + public InvalidParentAlbumFound(String albumParentId) { + this.albumParentId = albumParentId; + } + + public String getAlbumParentId() { + return albumParentId; + } + + } + + public static class InvalidImageFound extends RuntimeException { } + +} diff --git a/src/main/java/net/pterodactylus/sone/core/Core.java b/src/main/java/net/pterodactylus/sone/core/Core.java index ecb5857..1153c0f 100644 --- a/src/main/java/net/pterodactylus/sone/core/Core.java +++ b/src/main/java/net/pterodactylus/sone/core/Core.java @@ -17,10 +17,14 @@ package net.pterodactylus.sone.core; +import static com.google.common.base.Optional.fromNullable; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.primitives.Longs.tryParse; +import static java.lang.String.format; +import static java.util.logging.Level.WARNING; +import static java.util.logging.Logger.getLogger; -import java.net.MalformedURLException; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; @@ -36,9 +40,13 @@ import java.util.concurrent.TimeUnit; import java.util.logging.Level; 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.core.ConfigurationSoneParser.InvalidAlbumFound; +import net.pterodactylus.sone.core.ConfigurationSoneParser.InvalidImageFound; +import net.pterodactylus.sone.core.ConfigurationSoneParser.InvalidParentAlbumFound; +import net.pterodactylus.sone.core.ConfigurationSoneParser.InvalidPostFound; +import net.pterodactylus.sone.core.ConfigurationSoneParser.InvalidPostReplyFound; +import net.pterodactylus.sone.core.SoneChangeDetector.PostProcessor; +import net.pterodactylus.sone.core.SoneChangeDetector.PostReplyProcessor; import net.pterodactylus.sone.core.event.ImageInsertFinishedEvent; import net.pterodactylus.sone.core.event.MarkPostKnownEvent; import net.pterodactylus.sone.core.event.MarkPostReplyKnownEvent; @@ -62,17 +70,17 @@ import net.pterodactylus.sone.data.Reply; import net.pterodactylus.sone.data.Sone; import net.pterodactylus.sone.data.Sone.ShowCustomAvatars; import net.pterodactylus.sone.data.Sone.SoneStatus; -import net.pterodactylus.sone.data.SoneImpl; import net.pterodactylus.sone.data.TemporaryImage; +import net.pterodactylus.sone.database.AlbumBuilder; import net.pterodactylus.sone.database.Database; import net.pterodactylus.sone.database.DatabaseException; +import net.pterodactylus.sone.database.ImageBuilder; import net.pterodactylus.sone.database.PostBuilder; import net.pterodactylus.sone.database.PostProvider; import net.pterodactylus.sone.database.PostReplyBuilder; import net.pterodactylus.sone.database.PostReplyProvider; +import net.pterodactylus.sone.database.SoneBuilder; import net.pterodactylus.sone.database.SoneProvider; -import net.pterodactylus.sone.fcp.FcpInterface; -import net.pterodactylus.sone.fcp.FcpInterface.FullAccessRequired; import net.pterodactylus.sone.freenet.wot.Identity; import net.pterodactylus.sone.freenet.wot.IdentityManager; import net.pterodactylus.sone.freenet.wot.OwnIdentity; @@ -82,52 +90,45 @@ import net.pterodactylus.sone.freenet.wot.event.IdentityUpdatedEvent; import net.pterodactylus.sone.freenet.wot.event.OwnIdentityAddedEvent; import net.pterodactylus.sone.freenet.wot.event.OwnIdentityRemovedEvent; import net.pterodactylus.sone.main.SonePlugin; -import net.pterodactylus.sone.utils.IntegerRangePredicate; 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.NamedThreadFactory; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Function; import com.google.common.base.Optional; -import com.google.common.base.Predicate; -import com.google.common.base.Predicates; import com.google.common.collect.FluentIterable; import com.google.common.collect.HashMultimap; -import com.google.common.collect.ImmutableSet; import com.google.common.collect.Multimap; import com.google.common.collect.Multimaps; import com.google.common.eventbus.EventBus; import com.google.common.eventbus.Subscribe; import com.google.inject.Inject; - -import freenet.keys.FreenetURI; +import com.google.inject.Singleton; /** * The Sone core. * * @author David ‘Bombe’ Roden */ +@Singleton public class Core extends AbstractService implements SoneProvider, PostProvider, PostReplyProvider { /** The logger. */ - private static final Logger logger = Logging.getLogger(Core.class); + private static final Logger logger = getLogger("Sone.Core"); /** The start time. */ private final long startupTime = System.currentTimeMillis(); - /** The options. */ - private final Options options = new Options(); - /** The preferences. */ - private final Preferences preferences = new Preferences(options); + private final Preferences preferences; /** The event bus. */ private final EventBus eventBus; /** The configuration. */ - private Configuration configuration; + private final Configuration configuration; /** Whether we’re currently saving the configuration. */ private boolean storingConfiguration = false; @@ -153,9 +154,6 @@ public class Core extends AbstractService implements SoneProvider, PostProvider, /** The trust updater. */ private final WebOfTrustUpdater webOfTrustUpdater; - /** The FCP interface. */ - private volatile FcpInterface fcpInterface; - /** The times Sones were followed. */ private final Map soneFollowingTimes = new HashMap(); @@ -171,20 +169,12 @@ public class Core extends AbstractService implements SoneProvider, PostProvider, /* synchronize access on this on sones. */ private final Map soneRescuers = new HashMap(); - /** All Sones. */ - /* synchronize access on this on itself. */ - private final Map sones = new HashMap(); - /** All known Sones. */ private final Set knownSones = new HashSet(); /** The post database. */ private final Database database; - /** All bookmarked posts. */ - /* synchronize access on itself. */ - private final Set bookmarkedPosts = new HashSet(); - /** Trusted identities, sorted by own identities. */ private final Multimap trustedIdentities = Multimaps.synchronizedSetMultimap(HashMultimap.create()); @@ -219,12 +209,28 @@ public class Core extends AbstractService implements SoneProvider, PostProvider, this.configuration = configuration; this.freenetInterface = freenetInterface; this.identityManager = identityManager; - this.soneDownloader = new SoneDownloader(this, freenetInterface); - this.imageInserter = new ImageInserter(freenetInterface); + this.soneDownloader = new SoneDownloaderImpl(this, freenetInterface); + this.imageInserter = new ImageInserter(freenetInterface, freenetInterface.new InsertTokenSupplier()); this.updateChecker = new UpdateChecker(eventBus, freenetInterface); this.webOfTrustUpdater = webOfTrustUpdater; this.eventBus = eventBus; this.database = database; + preferences = new Preferences(eventBus); + } + + @VisibleForTesting + protected Core(Configuration configuration, FreenetInterface freenetInterface, IdentityManager identityManager, SoneDownloader soneDownloader, ImageInserter imageInserter, UpdateChecker updateChecker, WebOfTrustUpdater webOfTrustUpdater, EventBus eventBus, Database database) { + super("Sone Core"); + this.configuration = configuration; + this.freenetInterface = freenetInterface; + this.identityManager = identityManager; + this.soneDownloader = soneDownloader; + this.imageInserter = imageInserter; + this.updateChecker = updateChecker; + this.webOfTrustUpdater = webOfTrustUpdater; + this.eventBus = eventBus; + this.database = database; + preferences = new Preferences(eventBus); } // @@ -241,18 +247,6 @@ public class Core extends AbstractService implements SoneProvider, PostProvider, } /** - * Sets the configuration to use. This will automatically save the current - * configuration to the given configuration. - * - * @param configuration - * The new configuration to use - */ - public void setConfiguration(Configuration configuration) { - this.configuration = configuration; - touchConfiguration(); - } - - /** * Returns the options used by the core. * * @return The options of the core @@ -280,16 +274,6 @@ public class Core extends AbstractService implements SoneProvider, PostProvider, } /** - * Sets the FCP interface to use. - * - * @param fcpInterface - * The FCP interface to use - */ - public void setFcpInterface(FcpInterface fcpInterface) { - this.fcpInterface = fcpInterface; - } - - /** * Returns the Sone rescuer for the given local Sone. * * @param sone @@ -299,7 +283,7 @@ public class Core extends AbstractService implements SoneProvider, PostProvider, public SoneRescuer getSoneRescuer(Sone sone) { checkNotNull(sone, "sone must not be null"); checkArgument(sone.isLocal(), "sone must be local"); - synchronized (sones) { + synchronized (soneRescuers) { SoneRescuer soneRescuer = soneRescuers.get(sone); if (soneRescuer == null) { soneRescuer = new SoneRescuer(this, soneDownloader, sone); @@ -323,14 +307,21 @@ public class Core extends AbstractService implements SoneProvider, PostProvider, } } + public SoneBuilder soneBuilder() { + return database.newSoneBuilder(); + } + /** * {@inheritDocs} */ @Override public Collection getSones() { - synchronized (sones) { - return ImmutableSet.copyOf(sones.values()); - } + return database.getSones(); + } + + @Override + public Function> soneLoader() { + return database.soneLoader(); } /** @@ -344,9 +335,7 @@ public class Core extends AbstractService implements SoneProvider, PostProvider, */ @Override public Optional getSone(String id) { - synchronized (sones) { - return Optional.fromNullable(sones.get(id)); - } + return database.getSone(id); } /** @@ -354,15 +343,7 @@ public class Core extends AbstractService implements SoneProvider, PostProvider, */ @Override public Collection getLocalSones() { - synchronized (sones) { - return FluentIterable.from(sones.values()).filter(new Predicate() { - - @Override - public boolean apply(Sone sone) { - return sone.isLocal(); - } - }).toSet(); - } + return database.getLocalSones(); } /** @@ -370,24 +351,14 @@ public class Core extends AbstractService implements SoneProvider, PostProvider, * * @param id * The ID of the Sone - * @param create - * {@code true} to create a new Sone if none exists, - * {@code false} to return null if none exists * @return The Sone with the given ID, or {@code null} */ - public Sone getLocalSone(String id, boolean create) { - synchronized (sones) { - Sone sone = sones.get(id); - if ((sone == null) && create) { - sone = new SoneImpl(id, true); - sones.put(id, sone); - } - if ((sone != null) && !sone.isLocal()) { - sone = new SoneImpl(id, true); - sones.put(id, sone); - } - return sone; + public Sone getLocalSone(String id) { + Optional sone = database.getSone(id); + if (sone.isPresent() && sone.get().isLocal()) { + return sone.get(); } + return null; } /** @@ -395,36 +366,19 @@ public class Core extends AbstractService implements SoneProvider, PostProvider, */ @Override public Collection getRemoteSones() { - synchronized (sones) { - return FluentIterable.from(sones.values()).filter(new Predicate() { - - @Override - public boolean apply(Sone sone) { - return !sone.isLocal(); - } - }).toSet(); - } + return database.getRemoteSones(); } /** * Returns the remote Sone with the given ID. * + * * @param id * The ID of the remote Sone to get - * @param create - * {@code true} to always create a Sone, {@code false} to return - * {@code null} if no Sone with the given ID exists * @return The Sone with the given ID */ - public Sone getRemoteSone(String id, boolean create) { - synchronized (sones) { - Sone sone = sones.get(id); - if ((sone == null) && create && (id != null) && (id.length() == 43)) { - sone = new SoneImpl(id, false); - sones.put(id, sone); - } - return sone; - } + public Sone getRemoteSone(String id) { + return database.getSone(id).orNull(); } /** @@ -436,7 +390,7 @@ public class Core extends AbstractService implements SoneProvider, PostProvider, * {@code false} otherwise */ public boolean isModifiedSone(Sone sone) { - return (soneInserters.containsKey(sone)) ? soneInserters.get(sone).isModified() : false; + return soneInserters.containsKey(sone) && soneInserters.get(sone).isModified(); } /** @@ -454,22 +408,6 @@ public class Core extends AbstractService implements SoneProvider, PostProvider, } /** - * Returns whether the target Sone is trusted by the origin Sone. - * - * @param origin - * The origin Sone - * @param target - * The target Sone - * @return {@code true} if the target Sone is trusted by the origin Sone - */ - public boolean isSoneTrusted(Sone origin, Sone target) { - checkNotNull(origin, "origin must not be null"); - checkNotNull(target, "target must not be null"); - checkArgument(origin.getIdentity() instanceof OwnIdentity, "origin’s identity must be an OwnIdentity"); - return trustedIdentities.containsEntry(origin.getIdentity(), target.getIdentity()); - } - - /** * Returns a post builder. * * @return A new post builder @@ -571,21 +509,7 @@ public class Core extends AbstractService implements SoneProvider, PostProvider, * otherwise */ public boolean isBookmarked(Post post) { - return isPostBookmarked(post.getId()); - } - - /** - * Returns whether the post with the given ID is bookmarked. - * - * @param id - * The ID of the post to check - * @return {@code true} if the post with the given ID is bookmarked, - * {@code false} otherwise - */ - public boolean isPostBookmarked(String id) { - synchronized (bookmarkedPosts) { - return bookmarkedPosts.contains(id); - } + return database.isPostBookmarked(post); } /** @@ -594,28 +518,11 @@ public class Core extends AbstractService implements SoneProvider, PostProvider, * @return All bookmarked posts */ public Set getBookmarkedPosts() { - Set posts = new HashSet(); - synchronized (bookmarkedPosts) { - for (String bookmarkedPostId : bookmarkedPosts) { - Optional post = getPost(bookmarkedPostId); - if (post.isPresent()) { - posts.add(post.get()); - } - } - } - return posts; + return database.getBookmarkedPosts(); } - /** - * 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); + public AlbumBuilder albumBuilder() { + return database.newAlbumBuilder(); } /** @@ -624,23 +531,15 @@ public class Core extends AbstractService implements SoneProvider, PostProvider, * * @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} + * given ID exists */ - public Album getAlbum(String albumId, boolean create) { - Optional album = database.getAlbum(albumId); - if (album.isPresent()) { - return album.get(); - } - if (!create) { - return null; - } - Album newAlbum = database.newAlbumBuilder().withId(albumId).build(); - database.storeAlbum(newAlbum); - return newAlbum; + public Album getAlbum(String albumId) { + return database.getAlbum(albumId).orNull(); + } + + public ImageBuilder imageBuilder() { + return database.newImageBuilder(); } /** @@ -741,26 +640,19 @@ public class Core extends AbstractService implements SoneProvider, PostProvider, return null; } logger.info(String.format("Adding Sone from OwnIdentity: %s", ownIdentity)); - synchronized (sones) { - final Sone sone; - try { - sone = getLocalSone(ownIdentity.getId(), true).setIdentity(ownIdentity).setInsertUri(new FreenetURI(ownIdentity.getInsertUri())).setRequestUri(new FreenetURI(ownIdentity.getRequestUri())); - } catch (MalformedURLException mue1) { - logger.log(Level.SEVERE, String.format("Could not convert the Identity’s URIs to Freenet URIs: %s, %s", ownIdentity.getInsertUri(), ownIdentity.getRequestUri()), mue1); - return null; - } - sone.setLatestEdition(Numbers.safeParseLong(ownIdentity.getProperty("Sone.LatestEdition"), (long) 0)); - sone.setClient(new Client("Sone", SonePlugin.VERSION.toString())); - sone.setKnown(true); - /* TODO - load posts ’n stuff */ - sones.put(ownIdentity.getId(), sone); - final SoneInserter soneInserter = new SoneInserter(this, eventBus, freenetInterface, sone); + Sone sone = database.newSoneBuilder().local().from(ownIdentity).build(); + sone.setLatestEdition(fromNullable(tryParse(ownIdentity.getProperty("Sone.LatestEdition"))).or(0L)); + sone.setClient(new Client("Sone", SonePlugin.VERSION.toString())); + sone.setKnown(true); + SoneInserter soneInserter = new SoneInserter(this, eventBus, freenetInterface, ownIdentity.getId()); + eventBus.register(soneInserter); + synchronized (soneInserters) { soneInserters.put(sone, soneInserter); - sone.setStatus(SoneStatus.idle); - loadSone(sone); - soneInserter.start(); - return sone; } + loadSone(sone); + sone.setStatus(SoneStatus.idle); + soneInserter.start(); + return sone; } /** @@ -776,12 +668,6 @@ public class Core extends AbstractService implements SoneProvider, PostProvider, return null; } Sone sone = addLocalSone(ownIdentity); - sone.getOptions().addBooleanOption("AutoFollow", new DefaultOption(false)); - sone.getOptions().addBooleanOption("EnableSoneInsertNotifications", new DefaultOption(false)); - sone.getOptions().addBooleanOption("ShowNotification/NewSones", new DefaultOption(true)); - sone.getOptions().addBooleanOption("ShowNotification/NewPosts", new DefaultOption(true)); - sone.getOptions().addBooleanOption("ShowNotification/NewReplies", new DefaultOption(true)); - sone.getOptions().addEnumOption("ShowCustomAvatars", new DefaultOption(ShowCustomAvatars.NEVER)); followSone(sone, "nwa8lHa271k2QvJ8aa0Ov7IHAV-DFOCFgmDt3X6BpCI"); touchConfiguration(); @@ -800,41 +686,33 @@ public class Core extends AbstractService implements SoneProvider, PostProvider, logger.log(Level.WARNING, "Given Identity is null!"); return null; } - synchronized (sones) { - final Sone sone = getRemoteSone(identity.getId(), true); - if (sone.isLocal()) { - return sone; + final Long latestEdition = tryParse(fromNullable( + identity.getProperty("Sone.LatestEdition")).or("0")); + Optional existingSone = getSone(identity.getId()); + if (existingSone.isPresent() && existingSone.get().isLocal()) { + return existingSone.get(); + } + boolean newSone = !existingSone.isPresent(); + Sone sone = !newSone ? existingSone.get() : database.newSoneBuilder().from(identity).build(); + sone.setLatestEdition(latestEdition); + if (newSone) { + synchronized (knownSones) { + newSone = !knownSones.contains(sone.getId()); } - sone.setIdentity(identity); - boolean newSone = sone.getRequestUri() == null; - sone.setRequestUri(SoneUri.create(identity.getRequestUri())); - sone.setLatestEdition(Numbers.safeParseLong(identity.getProperty("Sone.LatestEdition"), (long) 0)); + sone.setKnown(!newSone); if (newSone) { - synchronized (knownSones) { - newSone = !knownSones.contains(sone.getId()); - } - sone.setKnown(!newSone); - if (newSone) { - eventBus.post(new NewSoneFoundEvent(sone)); - for (Sone localSone : getLocalSones()) { - if (localSone.getOptions().getBooleanOption("AutoFollow").get()) { - followSone(localSone, sone.getId()); - } + eventBus.post(new NewSoneFoundEvent(sone)); + for (Sone localSone : getLocalSones()) { + if (localSone.getOptions().isAutoFollow()) { + followSone(localSone, sone.getId()); } } } - soneDownloader.addSone(sone); - soneDownloaders.execute(new Runnable() { - - @Override - @SuppressWarnings("synthetic-access") - public void run() { - soneDownloader.fetchSone(sone, sone.getRequestUri()); - } - - }); - return sone; } + database.storeSone(sone); + soneDownloader.addSone(sone); + soneDownloaders.execute(soneDownloader.fetchSoneWithUriAction(sone)); + return sone; } /** @@ -848,7 +726,7 @@ public class Core extends AbstractService implements SoneProvider, PostProvider, public void followSone(Sone sone, String soneId) { checkNotNull(sone, "sone must not be null"); checkNotNull(soneId, "soneId must not be null"); - sone.addFriend(soneId); + database.addFriend(sone, soneId); synchronized (soneFollowingTimes) { if (!soneFollowingTimes.containsKey(soneId)) { long now = System.currentTimeMillis(); @@ -883,7 +761,7 @@ public class Core extends AbstractService implements SoneProvider, PostProvider, public void unfollowSone(Sone sone, String soneId) { checkNotNull(sone, "sone must not be null"); checkNotNull(soneId, "soneId must not be null"); - sone.removeFriend(soneId); + database.removeFriend(sone, soneId); boolean unfollowedSoneStillFollowed = false; for (Sone localSone : getLocalSones()) { unfollowedSoneStillFollowed |= localSone.hasFriend(soneId); @@ -986,75 +864,67 @@ public class Core extends AbstractService implements SoneProvider, PostProvider, * {@code true} if the stored Sone should be updated regardless * of the age of the given Sone */ - public void updateSone(Sone sone, boolean soneRescueMode) { + public void updateSone(final Sone sone, boolean soneRescueMode) { Optional storedSone = getSone(sone.getId()); if (storedSone.isPresent()) { if (!soneRescueMode && !(sone.getTime() > storedSone.get().getTime())) { logger.log(Level.FINE, String.format("Downloaded Sone %s is not newer than stored Sone %s.", sone, storedSone)); return; } - /* find removed posts. */ - Collection existingPosts = database.getPosts(sone.getId()); - for (Post oldPost : existingPosts) { - if (!sone.getPosts().contains(oldPost)) { - eventBus.post(new PostRemovedEvent(oldPost)); - } + List events = + collectEventsForChangesInSone(storedSone.get(), sone); + database.storeSone(sone); + for (Object event : events) { + eventBus.post(event); } - /* find new posts. */ - for (Post newPost : sone.getPosts()) { - if (existingPosts.contains(newPost)) { - continue; - } - if (newPost.getTime() < getSoneFollowingTime(sone)) { - newPost.setKnown(true); - } else if (!newPost.isKnown()) { - eventBus.post(new NewPostFoundEvent(newPost)); - } - } - /* store posts. */ - database.storePosts(sone, sone.getPosts()); - if (!soneRescueMode) { - for (PostReply reply : storedSone.get().getReplies()) { - if (!sone.getReplies().contains(reply)) { - eventBus.post(new PostReplyRemovedEvent(reply)); - } - } + sone.setOptions(storedSone.get().getOptions()); + sone.setKnown(storedSone.get().isKnown()); + sone.setStatus((sone.getTime() == 0) ? SoneStatus.unknown : SoneStatus.idle); + if (sone.isLocal()) { + touchConfiguration(); } - Set storedReplies = storedSone.get().getReplies(); - for (PostReply reply : sone.getReplies()) { - if (storedReplies.contains(reply)) { - continue; - } - if (reply.getTime() < getSoneFollowingTime(sone)) { - reply.setKnown(true); - } else if (!reply.isKnown()) { - eventBus.post(new NewPostReplyFoundEvent(reply)); + } + } + + private List collectEventsForChangesInSone(Sone oldSone, + final Sone newSone) { + final List events = new ArrayList(); + SoneChangeDetector soneChangeDetector = new SoneChangeDetector( + oldSone); + soneChangeDetector.onNewPosts(new PostProcessor() { + @Override + public void processPost(Post post) { + if (post.getTime() < getSoneFollowingTime(newSone)) { + post.setKnown(true); + } else if (!post.isKnown()) { + events.add(new NewPostFoundEvent(post)); } } - database.storePostReplies(sone, sone.getReplies()); - for (Album album : storedSone.get().getRootAlbum().getAlbums()) { - database.removeAlbum(album); - for (Image image : album.getImages()) { - database.removeImage(image); - } + }); + soneChangeDetector.onRemovedPosts(new PostProcessor() { + @Override + public void processPost(Post post) { + events.add(new PostRemovedEvent(post)); } - for (Album album : sone.getRootAlbum().getAlbums()) { - database.storeAlbum(album); - for (Image image : album.getImages()) { - database.storeImage(image); + }); + soneChangeDetector.onNewPostReplies(new PostReplyProcessor() { + @Override + public void processPostReply(PostReply postReply) { + if (postReply.getTime() < getSoneFollowingTime(newSone)) { + postReply.setKnown(true); + } else if (!postReply.isKnown()) { + events.add(new NewPostReplyFoundEvent(postReply)); } } - synchronized (sones) { - sone.setOptions(storedSone.get().getOptions()); - sone.setKnown(storedSone.get().isKnown()); - sone.setStatus((sone.getTime() == 0) ? SoneStatus.unknown : SoneStatus.idle); - if (sone.isLocal()) { - soneInserters.get(storedSone.get()).setSone(sone); - touchConfiguration(); - } - sones.put(sone.getId(), sone); + }); + soneChangeDetector.onRemovedPostReplies(new PostReplyProcessor() { + @Override + public void processPostReply(PostReply postReply) { + events.add(new PostReplyRemovedEvent(postReply)); } - } + }); + soneChangeDetector.detectChanges(newSone); + return events; } /** @@ -1070,15 +940,13 @@ public class Core extends AbstractService implements SoneProvider, PostProvider, logger.log(Level.WARNING, String.format("Tried to delete Sone of non-own identity: %s", sone)); return; } - synchronized (sones) { - if (!getLocalSones().contains(sone)) { - logger.log(Level.WARNING, String.format("Tried to delete non-local Sone: %s", sone)); - return; - } - sones.remove(sone.getId()); - SoneInserter soneInserter = soneInserters.remove(sone); - soneInserter.stop(); + if (!getLocalSones().contains(sone)) { + logger.log(Level.WARNING, String.format("Tried to delete non-local Sone: %s", sone)); + return; } + SoneInserter soneInserter = soneInserters.remove(sone); + soneInserter.stop(); + database.removeSone(sone); webOfTrustUpdater.removeContext((OwnIdentity) sone.getIdentity(), "Sone"); webOfTrustUpdater.removeProperty((OwnIdentity) sone.getIdentity(), "Sone.LatestEdition"); try { @@ -1120,14 +988,6 @@ public class Core extends AbstractService implements SoneProvider, PostProvider, } logger.info(String.format("Loading local Sone: %s", sone)); - /* initialize options. */ - sone.getOptions().addBooleanOption("AutoFollow", new DefaultOption(false)); - sone.getOptions().addBooleanOption("EnableSoneInsertNotifications", new DefaultOption(false)); - sone.getOptions().addBooleanOption("ShowNotification/NewSones", new DefaultOption(true)); - sone.getOptions().addBooleanOption("ShowNotification/NewPosts", new DefaultOption(true)); - sone.getOptions().addBooleanOption("ShowNotification/NewReplies", new DefaultOption(true)); - sone.getOptions().addEnumOption("ShowCustomAvatars", new DefaultOption(ShowCustomAvatars.NEVER)); - /* load Sone. */ String sonePrefix = "Sone/" + sone.getId(); Long soneTime = configuration.getLongValue(sonePrefix + "/Time").getValue(null); @@ -1138,169 +998,77 @@ public class Core extends AbstractService implements SoneProvider, PostProvider, String lastInsertFingerprint = configuration.getStringValue(sonePrefix + "/LastInsertFingerprint").getValue(""); /* load profile. */ - Profile profile = new Profile(sone); - profile.setFirstName(configuration.getStringValue(sonePrefix + "/Profile/FirstName").getValue(null)); - profile.setMiddleName(configuration.getStringValue(sonePrefix + "/Profile/MiddleName").getValue(null)); - profile.setLastName(configuration.getStringValue(sonePrefix + "/Profile/LastName").getValue(null)); - profile.setBirthDay(configuration.getIntValue(sonePrefix + "/Profile/BirthDay").getValue(null)); - profile.setBirthMonth(configuration.getIntValue(sonePrefix + "/Profile/BirthMonth").getValue(null)); - profile.setBirthYear(configuration.getIntValue(sonePrefix + "/Profile/BirthYear").getValue(null)); - - /* load profile fields. */ - while (true) { - String fieldPrefix = sonePrefix + "/Profile/Fields/" + profile.getFields().size(); - String fieldName = configuration.getStringValue(fieldPrefix + "/Name").getValue(null); - if (fieldName == null) { - break; - } - String fieldValue = configuration.getStringValue(fieldPrefix + "/Value").getValue(""); - profile.addField(fieldName).setValue(fieldValue); - } + ConfigurationSoneParser configurationSoneParser = new ConfigurationSoneParser(configuration, sone); + Profile profile = configurationSoneParser.parseProfile(); /* load posts. */ - Set posts = new HashSet(); - while (true) { - String postPrefix = sonePrefix + "/Posts/" + posts.size(); - String postId = configuration.getStringValue(postPrefix + "/ID").getValue(null); - if (postId == null) { - break; - } - String postRecipientId = configuration.getStringValue(postPrefix + "/Recipient").getValue(null); - long postTime = configuration.getLongValue(postPrefix + "/Time").getValue((long) 0); - String postText = configuration.getStringValue(postPrefix + "/Text").getValue(null); - if ((postTime == 0) || (postText == null)) { - logger.log(Level.WARNING, "Invalid post found, aborting load!"); - return; - } - PostBuilder postBuilder = postBuilder().withId(postId).from(sone.getId()).withTime(postTime).withText(postText); - if ((postRecipientId != null) && (postRecipientId.length() == 43)) { - postBuilder.to(postRecipientId); - } - posts.add(postBuilder.build()); + Collection posts; + try { + posts = configurationSoneParser.parsePosts(database); + } catch (InvalidPostFound ipf) { + logger.log(Level.WARNING, "Invalid post found, aborting load!"); + return; } /* load replies. */ - Set replies = new HashSet(); - while (true) { - String replyPrefix = sonePrefix + "/Replies/" + replies.size(); - String replyId = configuration.getStringValue(replyPrefix + "/ID").getValue(null); - if (replyId == null) { - break; - } - String postId = configuration.getStringValue(replyPrefix + "/Post/ID").getValue(null); - long replyTime = configuration.getLongValue(replyPrefix + "/Time").getValue((long) 0); - String replyText = configuration.getStringValue(replyPrefix + "/Text").getValue(null); - if ((postId == null) || (replyTime == 0) || (replyText == null)) { - logger.log(Level.WARNING, "Invalid reply found, aborting load!"); - return; - } - PostReplyBuilder postReplyBuilder = postReplyBuilder().withId(replyId).from(sone.getId()).to(postId).withTime(replyTime).withText(replyText); - replies.add(postReplyBuilder.build()); + Collection replies; + try { + replies = configurationSoneParser.parsePostReplies(database); + } catch (InvalidPostReplyFound iprf) { + logger.log(Level.WARNING, "Invalid reply found, aborting load!"); + return; } /* load post likes. */ - Set likedPostIds = new HashSet(); - while (true) { - String likedPostId = configuration.getStringValue(sonePrefix + "/Likes/Post/" + likedPostIds.size() + "/ID").getValue(null); - if (likedPostId == null) { - break; - } - likedPostIds.add(likedPostId); - } + Set likedPostIds = + configurationSoneParser.parseLikedPostIds(); /* load reply likes. */ - Set likedReplyIds = new HashSet(); - while (true) { - String likedReplyId = configuration.getStringValue(sonePrefix + "/Likes/Reply/" + likedReplyIds.size() + "/ID").getValue(null); - if (likedReplyId == null) { - break; - } - likedReplyIds.add(likedReplyId); - } - - /* load friends. */ - Set friends = new HashSet(); - while (true) { - String friendId = configuration.getStringValue(sonePrefix + "/Friends/" + friends.size() + "/ID").getValue(null); - if (friendId == null) { - break; - } - friends.add(friendId); - } + Set likedReplyIds = + configurationSoneParser.parseLikedPostReplyIds(); /* load albums. */ - List topLevelAlbums = new ArrayList(); - int albumCounter = 0; - while (true) { - String albumPrefix = sonePrefix + "/Albums/" + albumCounter++; - String albumId = configuration.getStringValue(albumPrefix + "/ID").getValue(null); - if (albumId == null) { - break; - } - String albumTitle = configuration.getStringValue(albumPrefix + "/Title").getValue(null); - String albumDescription = configuration.getStringValue(albumPrefix + "/Description").getValue(null); - String albumParentId = configuration.getStringValue(albumPrefix + "/Parent").getValue(null); - String albumImageId = configuration.getStringValue(albumPrefix + "/AlbumImage").getValue(null); - if ((albumTitle == null) || (albumDescription == null)) { - logger.log(Level.WARNING, "Invalid album found, aborting load!"); - return; - } - Album album = getAlbum(albumId).setSone(sone).modify().setTitle(albumTitle).setDescription(albumDescription).setAlbumImage(albumImageId).update(); - if (albumParentId != null) { - Album parentAlbum = getAlbum(albumParentId, false); - if (parentAlbum == null) { - logger.log(Level.WARNING, String.format("Invalid parent album ID: %s", albumParentId)); - return; - } - parentAlbum.addAlbum(album); - } else { - if (!topLevelAlbums.contains(album)) { - topLevelAlbums.add(album); - } - } + List topLevelAlbums; + try { + topLevelAlbums = + configurationSoneParser.parseTopLevelAlbums(database); + } catch (InvalidAlbumFound iaf) { + logger.log(Level.WARNING, "Invalid album found, aborting load!"); + return; + } catch (InvalidParentAlbumFound ipaf) { + logger.log(Level.WARNING, format("Invalid parent album ID: %s", + ipaf.getAlbumParentId())); + return; } /* 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).modify().setSone(sone).setCreationTime(creationTime).setKey(key).setTitle(title).setDescription(description).setWidth(width).setHeight(height).update(); - album.addImage(image); + try { + configurationSoneParser.parseImages(database); + } catch (InvalidImageFound iif) { + logger.log(WARNING, "Invalid image found, aborting load!"); + return; + } catch (InvalidParentAlbumFound ipaf) { + logger.log(Level.WARNING, + format("Invalid album image (%s) encountered, aborting load!", + ipaf.getAlbumParentId())); + return; } /* load avatar. */ String avatarId = configuration.getStringValue(sonePrefix + "/Profile/Avatar").getValue(null); if (avatarId != null) { - profile.setAvatar(getImage(avatarId, false)); + final Map images = + configurationSoneParser.getImages(); + profile.setAvatar(images.get(avatarId)); } /* load options. */ - sone.getOptions().getBooleanOption("AutoFollow").set(configuration.getBooleanValue(sonePrefix + "/Options/AutoFollow").getValue(null)); - sone.getOptions().getBooleanOption("EnableSoneInsertNotifications").set(configuration.getBooleanValue(sonePrefix + "/Options/EnableSoneInsertNotifications").getValue(null)); - sone.getOptions().getBooleanOption("ShowNotification/NewSones").set(configuration.getBooleanValue(sonePrefix + "/Options/ShowNotification/NewSones").getValue(null)); - sone.getOptions().getBooleanOption("ShowNotification/NewPosts").set(configuration.getBooleanValue(sonePrefix + "/Options/ShowNotification/NewPosts").getValue(null)); - sone.getOptions().getBooleanOption("ShowNotification/NewReplies").set(configuration.getBooleanValue(sonePrefix + "/Options/ShowNotification/NewReplies").getValue(null)); - sone.getOptions(). getEnumOption("ShowCustomAvatars").set(ShowCustomAvatars.valueOf(configuration.getStringValue(sonePrefix + "/Options/ShowCustomAvatars").getValue(ShowCustomAvatars.NEVER.name()))); + sone.getOptions().setAutoFollow(configuration.getBooleanValue(sonePrefix + "/Options/AutoFollow").getValue(null)); + sone.getOptions().setSoneInsertNotificationEnabled(configuration.getBooleanValue(sonePrefix + "/Options/EnableSoneInsertNotifications").getValue(null)); + sone.getOptions().setShowNewSoneNotifications(configuration.getBooleanValue(sonePrefix + "/Options/ShowNotification/NewSones").getValue(null)); + sone.getOptions().setShowNewPostNotifications(configuration.getBooleanValue(sonePrefix + "/Options/ShowNotification/NewPosts").getValue(null)); + sone.getOptions().setShowNewReplyNotifications(configuration.getBooleanValue(sonePrefix + "/Options/ShowNotification/NewReplies").getValue(null)); + sone.getOptions().setShowCustomAvatars(ShowCustomAvatars.valueOf(configuration.getStringValue(sonePrefix + "/Options/ShowCustomAvatars").getValue(ShowCustomAvatars.NEVER.name()))); /* if we’re still here, Sone was loaded successfully. */ synchronized (sone) { @@ -1310,27 +1078,20 @@ public class Core extends AbstractService implements SoneProvider, PostProvider, sone.setReplies(replies); sone.setLikePostIds(likedPostIds); sone.setLikeReplyIds(likedReplyIds); - for (String friendId : friends) { - followSone(sone, friendId); - } for (Album album : sone.getRootAlbum().getAlbums()) { sone.getRootAlbum().removeAlbum(album); } for (Album album : topLevelAlbums) { sone.getRootAlbum().addAlbum(album); } - soneInserters.get(sone).setLastInsertFingerprint(lastInsertFingerprint); - } - synchronized (knownSones) { - for (String friend : friends) { - knownSones.add(friend); + database.storeSone(sone); + synchronized (soneInserters) { + soneInserters.get(sone).setLastInsertFingerprint(lastInsertFingerprint); } } - database.storePosts(sone, posts); for (Post post : posts) { post.setKnown(true); } - database.storePostReplies(sone, replies); for (PostReply reply : replies) { reply.setKnown(true); } @@ -1343,34 +1104,6 @@ public class Core extends AbstractService implements SoneProvider, PostProvider, * * @param sone * The Sone that creates the post - * @param text - * The text of the post - * @return The created post - */ - public Post createPost(Sone sone, String text) { - return createPost(sone, System.currentTimeMillis(), text); - } - - /** - * Creates a new post. - * - * @param sone - * The Sone that creates the post - * @param time - * The time of the post - * @param text - * The text of the post - * @return The created post - */ - public Post createPost(Sone sone, long time, String text) { - return createPost(sone, null, time, text); - } - - /** - * Creates a new post. - * - * @param sone - * The Sone that creates the post * @param recipient * The recipient Sone, or {@code null} if this post does not have * a recipient @@ -1379,24 +1112,6 @@ public class Core extends AbstractService implements SoneProvider, PostProvider, * @return The created post */ public Post createPost(Sone sone, Optional recipient, String text) { - return createPost(sone, recipient, System.currentTimeMillis(), text); - } - - /** - * Creates a new post. - * - * @param sone - * The Sone that creates the post - * @param recipient - * The recipient Sone, or {@code null} if this post does not have - * a recipient - * @param time - * The time of the post - * @param text - * The text of the post - * @return The created post - */ - public Post createPost(Sone sone, Optional recipient, long time, String text) { checkNotNull(text, "text must not be null"); checkArgument(text.trim().length() > 0, "text must not be empty"); if (!sone.isLocal()) { @@ -1404,7 +1119,7 @@ public class Core extends AbstractService implements SoneProvider, PostProvider, return null; } PostBuilder postBuilder = database.newPostBuilder(); - postBuilder.from(sone.getId()).randomId().withTime(time).withText(text.trim()); + postBuilder.from(sone.getId()).randomId().currentTime().withText(text.trim()); if (recipient.isPresent()) { postBuilder.to(recipient.get().getId()); } @@ -1413,16 +1128,7 @@ public class Core extends AbstractService implements SoneProvider, PostProvider, eventBus.post(new NewPostFoundEvent(post)); sone.addPost(post); touchConfiguration(); - localElementTicker.schedule(new Runnable() { - - /** - * {@inheritDoc} - */ - @Override - public void run() { - markPostKnown(post); - } - }, 10, TimeUnit.SECONDS); + localElementTicker.schedule(new MarkPostKnown(post), 10, TimeUnit.SECONDS); return post; } @@ -1459,26 +1165,8 @@ public class Core extends AbstractService implements SoneProvider, PostProvider, } } - /** - * Bookmarks the given post. - * - * @param post - * The post to bookmark - */ - public void bookmark(Post post) { - bookmarkPost(post.getId()); - } - - /** - * Bookmarks the post with the given ID. - * - * @param id - * The ID of the post to bookmark - */ - public void bookmarkPost(String id) { - synchronized (bookmarkedPosts) { - bookmarkedPosts.add(id); - } + public void bookmarkPost(Post post) { + database.bookmarkPost(post); } /** @@ -1487,20 +1175,8 @@ public class Core extends AbstractService implements SoneProvider, PostProvider, * @param post * The post to unbookmark */ - public void unbookmark(Post post) { - unbookmarkPost(post.getId()); - } - - /** - * Removes the post with the given ID from the bookmarks. - * - * @param id - * The ID of the post to unbookmark - */ - public void unbookmarkPost(String id) { - synchronized (bookmarkedPosts) { - bookmarkedPosts.remove(id); - } + public void unbookmarkPost(Post post) { + database.unbookmarkPost(post); } /** @@ -1528,16 +1204,7 @@ public class Core extends AbstractService implements SoneProvider, PostProvider, eventBus.post(new NewPostReplyFoundEvent(reply)); sone.addReply(reply); touchConfiguration(); - localElementTicker.schedule(new Runnable() { - - /** - * {@inheritDoc} - */ - @Override - public void run() { - markReplyKnown(reply); - } - }, 10, TimeUnit.SECONDS); + localElementTicker.schedule(new MarkReplyKnown(reply), 10, TimeUnit.SECONDS); return reply; } @@ -1576,17 +1243,6 @@ public class Core extends AbstractService implements SoneProvider, PostProvider, } /** - * 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, sone.getRootAlbum()); - } - - /** * Creates a new album for the given Sone. * * @param sone @@ -1597,9 +1253,8 @@ public class Core extends AbstractService implements SoneProvider, PostProvider, * @return The new album */ public Album createAlbum(Sone sone, Album parent) { - Album album = database.newAlbumBuilder().randomId().build(); + Album album = database.newAlbumBuilder().randomId().by(sone).build(); database.storeAlbum(album); - album.setSone(sone); parent.addAlbum(album); return album; } @@ -1650,7 +1305,7 @@ public class Core extends AbstractService implements SoneProvider, PostProvider, * Deletes the given image. This method will also delete a matching * temporary image. * - * @see #deleteTemporaryImage(TemporaryImage) + * @see #deleteTemporaryImage(String) * @param image * The image to delete */ @@ -1682,17 +1337,6 @@ public class Core extends AbstractService implements SoneProvider, PostProvider, } /** - * Deletes the given temporary image. - * - * @param temporaryImage - * The temporary image to delete - */ - public void deleteTemporaryImage(TemporaryImage temporaryImage) { - checkNotNull(temporaryImage, "temporaryImage must not be null"); - deleteTemporaryImage(temporaryImage.getId()); - } - - /** * Deletes the temporary image with the given ID. * * @param imageId @@ -1760,10 +1404,15 @@ public class Core extends AbstractService implements SoneProvider, PostProvider, @Override public void serviceStop() { localElementTicker.shutdownNow(); - synchronized (sones) { + synchronized (soneInserters) { for (Entry soneInserter : soneInserters.entrySet()) { soneInserter.getValue().stop(); - saveSone(getLocalSone(soneInserter.getKey().getId(), false)); + saveSone(soneInserter.getKey()); + } + } + synchronized (soneRescuers) { + for (SoneRescuer soneRescuer : soneRescuers.values()) { + soneRescuer.stop(); } } saveConfiguration(); @@ -1858,13 +1507,6 @@ public class Core extends AbstractService implements SoneProvider, PostProvider, } 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 = FluentIterable.from(sone.getRootAlbum().getAlbums()).transformAndConcat(Album.FLATTENER).toList(); @@ -1900,12 +1542,12 @@ public class Core extends AbstractService implements SoneProvider, PostProvider, configuration.getStringValue(sonePrefix + "/Images/" + imageCounter + "/ID").setValue(null); /* save options. */ - configuration.getBooleanValue(sonePrefix + "/Options/AutoFollow").setValue(sone.getOptions().getBooleanOption("AutoFollow").getReal()); - configuration.getBooleanValue(sonePrefix + "/Options/ShowNotification/NewSones").setValue(sone.getOptions().getBooleanOption("ShowNotification/NewSones").getReal()); - configuration.getBooleanValue(sonePrefix + "/Options/ShowNotification/NewPosts").setValue(sone.getOptions().getBooleanOption("ShowNotification/NewPosts").getReal()); - configuration.getBooleanValue(sonePrefix + "/Options/ShowNotification/NewReplies").setValue(sone.getOptions().getBooleanOption("ShowNotification/NewReplies").getReal()); - configuration.getBooleanValue(sonePrefix + "/Options/EnableSoneInsertNotifications").setValue(sone.getOptions().getBooleanOption("EnableSoneInsertNotifications").getReal()); - configuration.getStringValue(sonePrefix + "/Options/ShowCustomAvatars").setValue(sone.getOptions(). getEnumOption("ShowCustomAvatars").get().name()); + configuration.getBooleanValue(sonePrefix + "/Options/AutoFollow").setValue(sone.getOptions().isAutoFollow()); + configuration.getBooleanValue(sonePrefix + "/Options/EnableSoneInsertNotifications").setValue(sone.getOptions().isSoneInsertNotificationEnabled()); + configuration.getBooleanValue(sonePrefix + "/Options/ShowNotification/NewSones").setValue(sone.getOptions().isShowNewSoneNotifications()); + configuration.getBooleanValue(sonePrefix + "/Options/ShowNotification/NewPosts").setValue(sone.getOptions().isShowNewPostNotifications()); + configuration.getBooleanValue(sonePrefix + "/Options/ShowNotification/NewReplies").setValue(sone.getOptions().isShowNewReplyNotifications()); + configuration.getStringValue(sonePrefix + "/Options/ShowCustomAvatars").setValue(sone.getOptions().getShowCustomAvatars().name()); configuration.save(); @@ -1931,18 +1573,7 @@ public class Core extends AbstractService implements SoneProvider, PostProvider, /* store the options first. */ try { - configuration.getIntValue("Option/ConfigurationVersion").setValue(0); - configuration.getIntValue("Option/InsertionDelay").setValue(options.getIntegerOption("InsertionDelay").getReal()); - configuration.getIntValue("Option/PostsPerPage").setValue(options.getIntegerOption("PostsPerPage").getReal()); - configuration.getIntValue("Option/ImagesPerPage").setValue(options.getIntegerOption("ImagesPerPage").getReal()); - configuration.getIntValue("Option/CharactersPerPost").setValue(options.getIntegerOption("CharactersPerPost").getReal()); - configuration.getIntValue("Option/PostCutOffLength").setValue(options.getIntegerOption("PostCutOffLength").getReal()); - configuration.getBooleanValue("Option/RequireFullAccess").setValue(options.getBooleanOption("RequireFullAccess").getReal()); - configuration.getIntValue("Option/PositiveTrust").setValue(options.getIntegerOption("PositiveTrust").getReal()); - configuration.getIntValue("Option/NegativeTrust").setValue(options.getIntegerOption("NegativeTrust").getReal()); - configuration.getStringValue("Option/TrustComment").setValue(options.getStringOption("TrustComment").getReal()); - configuration.getBooleanValue("Option/ActivateFcpInterface").setValue(options.getBooleanOption("ActivateFcpInterface").getReal()); - configuration.getIntValue("Option/FcpFullAccessRequired").setValue(options.getIntegerOption("FcpFullAccessRequired").getReal()); + preferences.saveTo(configuration); /* save known Sones. */ int soneCounter = 0; @@ -1967,15 +1598,6 @@ public class Core extends AbstractService implements SoneProvider, PostProvider, /* save known posts. */ database.save(); - /* save bookmarked posts. */ - int bookmarkedPostCounter = 0; - synchronized (bookmarkedPosts) { - for (String bookmarkedPostId : bookmarkedPosts) { - configuration.getStringValue("Bookmarks/Post/" + bookmarkedPostCounter++ + "/ID").setValue(bookmarkedPostId); - } - } - configuration.getStringValue("Bookmarks/Post/" + bookmarkedPostCounter++ + "/ID").setValue(null); - /* now save it. */ configuration.save(); @@ -1994,52 +1616,7 @@ public class Core extends AbstractService implements SoneProvider, PostProvider, * Loads the configuration. */ private void loadConfiguration() { - /* create options. */ - options.addIntegerOption("InsertionDelay", new DefaultOption(60, new IntegerRangePredicate(0, Integer.MAX_VALUE), new OptionWatcher() { - - @Override - public void optionChanged(Option option, Integer oldValue, Integer newValue) { - SoneInserter.setInsertionDelay(newValue); - } - - })); - options.addIntegerOption("PostsPerPage", new DefaultOption(10, new IntegerRangePredicate(1, Integer.MAX_VALUE))); - options.addIntegerOption("ImagesPerPage", new DefaultOption(9, new IntegerRangePredicate(1, Integer.MAX_VALUE))); - options.addIntegerOption("CharactersPerPost", new DefaultOption(400, Predicates. or(new IntegerRangePredicate(50, Integer.MAX_VALUE), Predicates.equalTo(-1)))); - options.addIntegerOption("PostCutOffLength", new DefaultOption(200, Predicates. or(new IntegerRangePredicate(50, Integer.MAX_VALUE), Predicates.equalTo(-1)))); - options.addBooleanOption("RequireFullAccess", new DefaultOption(false)); - options.addIntegerOption("PositiveTrust", new DefaultOption(75, new IntegerRangePredicate(0, 100))); - options.addIntegerOption("NegativeTrust", new DefaultOption(-25, new IntegerRangePredicate(-100, 100))); - options.addStringOption("TrustComment", new DefaultOption("Set from Sone Web Interface")); - options.addBooleanOption("ActivateFcpInterface", new DefaultOption(false, new OptionWatcher() { - - @Override - @SuppressWarnings("synthetic-access") - public void optionChanged(Option option, Boolean oldValue, Boolean newValue) { - fcpInterface.setActive(newValue); - } - })); - options.addIntegerOption("FcpFullAccessRequired", new DefaultOption(2, new OptionWatcher() { - - @Override - @SuppressWarnings("synthetic-access") - public void optionChanged(Option option, Integer oldValue, Integer newValue) { - fcpInterface.setFullAccessRequired(FullAccessRequired.values()[newValue]); - } - - })); - - loadConfigurationValue("InsertionDelay"); - loadConfigurationValue("PostsPerPage"); - loadConfigurationValue("ImagesPerPage"); - loadConfigurationValue("CharactersPerPost"); - loadConfigurationValue("PostCutOffLength"); - options.getBooleanOption("RequireFullAccess").set(configuration.getBooleanValue("Option/RequireFullAccess").getValue(null)); - loadConfigurationValue("PositiveTrust"); - loadConfigurationValue("NegativeTrust"); - options.getStringOption("TrustComment").set(configuration.getStringValue("Option/TrustComment").getValue(null)); - options.getBooleanOption("ActivateFcpInterface").set(configuration.getBooleanValue("Option/ActivateFcpInterface").getValue(null)); - options.getIntegerOption("FcpFullAccessRequired").set(configuration.getIntValue("Option/FcpFullAccessRequired").getValue(null)); + new PreferencesLoader(preferences).loadFrom(configuration); /* load known Sones. */ int soneCounter = 0; @@ -2066,34 +1643,6 @@ public class Core extends AbstractService implements SoneProvider, PostProvider, } ++soneCounter; } - - /* load bookmarked posts. */ - int bookmarkedPostCounter = 0; - while (true) { - String bookmarkedPostId = configuration.getStringValue("Bookmarks/Post/" + bookmarkedPostCounter++ + "/ID").getValue(null); - if (bookmarkedPostId == null) { - break; - } - synchronized (bookmarkedPosts) { - bookmarkedPosts.add(bookmarkedPostId); - } - } - - } - - /** - * Loads an {@link Integer} configuration value for the option with the - * given name, logging validation failures. - * - * @param optionName - * The name of the option to load - */ - private void loadConfigurationValue(String optionName) { - try { - options.getIntegerOption(optionName).set(configuration.getIntValue("Option/" + optionName).getValue(null)); - } catch (IllegalArgumentException iae1) { - logger.log(Level.WARNING, String.format("Invalid value for %s in configuration, using default.", optionName)); - } } /** @@ -2146,22 +1695,14 @@ public class Core extends AbstractService implements SoneProvider, PostProvider, */ @Subscribe public void identityUpdated(IdentityUpdatedEvent identityUpdatedEvent) { - final Identity identity = identityUpdatedEvent.identity(); - soneDownloaders.execute(new Runnable() { - - @Override - @SuppressWarnings("synthetic-access") - public void run() { - Sone sone = getRemoteSone(identity.getId(), false); - if (sone.isLocal()) { - return; - } - sone.setIdentity(identity); - sone.setLatestEdition(Numbers.safeParseLong(identity.getProperty("Sone.LatestEdition"), sone.getLatestEdition())); - soneDownloader.addSone(sone); - soneDownloader.fetchSone(sone); - } - }); + Identity identity = identityUpdatedEvent.identity(); + final Sone sone = getRemoteSone(identity.getId()); + if (sone.isLocal()) { + return; + } + sone.setLatestEdition(fromNullable(tryParse(identity.getProperty("Sone.LatestEdition"))).or(sone.getLatestEdition())); + soneDownloader.addSone(sone); + soneDownloaders.execute(soneDownloader.fetchSoneAction(sone)); } /** @@ -2175,35 +1716,20 @@ public class Core extends AbstractService implements SoneProvider, PostProvider, OwnIdentity ownIdentity = identityRemovedEvent.ownIdentity(); Identity identity = identityRemovedEvent.identity(); trustedIdentities.remove(ownIdentity, identity); - boolean foundIdentity = false; for (Entry> trustedIdentity : trustedIdentities.asMap().entrySet()) { if (trustedIdentity.getKey().equals(ownIdentity)) { continue; } if (trustedIdentity.getValue().contains(identity)) { - foundIdentity = true; + return; } } - if (foundIdentity) { - /* some local identity still trusts this identity, don’t remove. */ - return; - } Optional sone = getSone(identity.getId()); if (!sone.isPresent()) { /* TODO - we don’t have the Sone anymore. should this happen? */ return; } - database.removePosts(sone.get()); - for (Post post : sone.get().getPosts()) { - eventBus.post(new PostRemovedEvent(post)); - } - database.removePostReplies(sone.get()); - for (PostReply reply : sone.get().getReplies()) { - eventBus.post(new PostReplyRemovedEvent(reply)); - } - synchronized (sones) { - sones.remove(identity.getId()); - } + database.removeSone(sone.get()); eventBus.post(new SoneRemovedEvent(sone.get())); } @@ -2221,4 +1747,36 @@ public class Core extends AbstractService implements SoneProvider, PostProvider, touchConfiguration(); } + @VisibleForTesting + class MarkPostKnown implements Runnable { + + private final Post post; + + public MarkPostKnown(Post post) { + this.post = post; + } + + @Override + public void run() { + markPostKnown(post); + } + + } + + @VisibleForTesting + class MarkReplyKnown implements Runnable { + + private final PostReply postReply; + + public MarkReplyKnown(PostReply postReply) { + this.postReply = postReply; + } + + @Override + public void run() { + markReplyKnown(postReply); + } + + } + } diff --git a/src/main/java/net/pterodactylus/sone/core/FreenetInterface.java b/src/main/java/net/pterodactylus/sone/core/FreenetInterface.java index e92cf8e..e802ba2 100644 --- a/src/main/java/net/pterodactylus/sone/core/FreenetInterface.java +++ b/src/main/java/net/pterodactylus/sone/core/FreenetInterface.java @@ -17,11 +17,16 @@ package net.pterodactylus.sone.core; +import static freenet.keys.USK.create; +import static java.lang.String.format; +import static java.util.logging.Level.WARNING; +import static java.util.logging.Logger.getLogger; +import static net.pterodactylus.sone.freenet.Key.routingKey; + import java.net.MalformedURLException; import java.util.Collections; import java.util.HashMap; import java.util.Map; -import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; @@ -32,17 +37,17 @@ import net.pterodactylus.sone.core.event.ImageInsertStartedEvent; import net.pterodactylus.sone.data.Image; import net.pterodactylus.sone.data.Sone; import net.pterodactylus.sone.data.TemporaryImage; -import net.pterodactylus.util.logging.Logging; -import com.db4o.ObjectContainer; +import com.google.common.base.Function; import com.google.common.eventbus.EventBus; import com.google.inject.Inject; +import com.google.inject.Singleton; import freenet.client.ClientMetadata; import freenet.client.FetchException; +import freenet.client.FetchException.FetchExceptionMode; import freenet.client.FetchResult; import freenet.client.HighLevelSimpleClient; -import freenet.client.HighLevelSimpleClientImpl; import freenet.client.InsertBlock; import freenet.client.InsertContext; import freenet.client.InsertException; @@ -55,19 +60,23 @@ import freenet.keys.FreenetURI; import freenet.keys.InsertableClientSSK; import freenet.keys.USK; import freenet.node.Node; +import freenet.node.RequestClient; import freenet.node.RequestStarter; import freenet.support.api.Bucket; +import freenet.support.api.RandomAccessBucket; import freenet.support.io.ArrayBucket; +import freenet.support.io.ResumeFailedException; /** * Contains all necessary functionality for interacting with the Freenet node. * * @author David ‘Bombe’ Roden */ +@Singleton public class FreenetInterface { /** The logger. */ - private static final Logger logger = Logging.getLogger(FreenetInterface.class); + private static final Logger logger = getLogger("Sone.FreenetInterface"); /** The event bus. */ private final EventBus eventBus; @@ -84,6 +93,18 @@ public class FreenetInterface { /** The not-Sone-related USK callbacks. */ private final Map uriUskCallbacks = Collections.synchronizedMap(new HashMap()); + private final RequestClient imageInserts = new RequestClient() { + @Override + public boolean persistent() { + return false; + } + + @Override + public boolean realTimeFlag() { + return true; + } + }; + /** * Creates a new Freenet interface. * @@ -111,14 +132,13 @@ public class FreenetInterface { * @return The result of the fetch, or {@code null} if an error occured */ public Fetched fetchUri(FreenetURI uri) { - FetchResult fetchResult = null; FreenetURI currentUri = new FreenetURI(uri); while (true) { try { - fetchResult = client.fetch(currentUri); + FetchResult fetchResult = client.fetch(currentUri); return new Fetched(currentUri, fetchResult); } catch (FetchException fe1) { - if (fe1.getMode() == FetchException.PERMANENT_REDIRECT) { + if (fe1.getMode() == FetchExceptionMode.PERMANENT_REDIRECT) { currentUri = fe1.newURI; continue; } @@ -129,16 +149,6 @@ public class FreenetInterface { } /** - * Creates a key pair. - * - * @return The request key at index 0, the insert key at index 1 - */ - public String[] generateKeyPair() { - FreenetURI[] keyPair = client.generateKeyPair(""); - return new String[] { keyPair[1].toString(), keyPair[0].toString() }; - } - - /** * Inserts the image data of the given {@link TemporaryImage} and returns * the given insert token that can be used to add listeners or cancel the * insert. @@ -157,11 +167,12 @@ public class FreenetInterface { InsertableClientSSK key = InsertableClientSSK.createRandom(node.random, ""); FreenetURI targetUri = key.getInsertURI().setDocName(filenameHint); InsertContext insertContext = client.getInsertContext(true); - Bucket bucket = new ArrayBucket(temporaryImage.getImageData()); + RandomAccessBucket bucket = new ArrayBucket(temporaryImage.getImageData()); + insertToken.setBucket(bucket); ClientMetadata metadata = new ClientMetadata(temporaryImage.getMimeType()); InsertBlock insertBlock = new InsertBlock(bucket, metadata, targetUri); try { - ClientPutter clientPutter = client.insert(insertBlock, false, null, false, insertContext, insertToken, RequestStarter.INTERACTIVE_PRIORITY_CLASS); + ClientPutter clientPutter = client.insert(insertBlock, null, false, insertContext, insertToken, RequestStarter.INTERACTIVE_PRIORITY_CLASS); insertToken.setClientPutter(clientPutter); } catch (InsertException ie1) { throw new SoneInsertException("Could not start image insert.", ie1); @@ -189,51 +200,30 @@ public class FreenetInterface { } } - /** - * Registers the USK for the given Sone and notifies the given - * {@link SoneDownloader} if an update was found. - * - * @param sone - * The Sone to watch - * @param soneDownloader - * The Sone download to notify on updates - */ - public void registerUsk(final Sone sone, final SoneDownloader soneDownloader) { + public void registerActiveUsk(FreenetURI requestUri, + USKCallback uskCallback) { try { - logger.log(Level.FINE, String.format("Registering Sone “%s” for USK updates at %s…", sone, sone.getRequestUri().setMetaString(new String[] { "sone.xml" }))); - USKCallback uskCallback = new USKCallback() { - - @Override - @SuppressWarnings("synthetic-access") - public void onFoundEdition(long edition, USK key, ObjectContainer objectContainer, ClientContext clientContext, boolean metadata, short codec, byte[] data, boolean newKnownGood, boolean newSlotToo) { - logger.log(Level.FINE, String.format("Found USK update for Sone “%s” at %s, new known good: %s, new slot too: %s.", sone, key, newKnownGood, newSlotToo)); - if (edition > sone.getLatestEdition()) { - sone.setLatestEdition(edition); - new Thread(new Runnable() { - - @Override - public void run() { - soneDownloader.fetchSone(sone); - } - }, "Sone Downloader").start(); - } - } - - @Override - public short getPollingPriorityProgress() { - return RequestStarter.INTERACTIVE_PRIORITY_CLASS; - } + soneUskCallbacks.put(routingKey(requestUri), uskCallback); + node.clientCore.uskManager.subscribe(create(requestUri), + uskCallback, true, (RequestClient) client); + } catch (MalformedURLException mue1) { + logger.log(WARNING, format("Could not subscribe USK “%s”!", + requestUri), mue1); + } + } - @Override - public short getPollingPriorityNormal() { - return RequestStarter.INTERACTIVE_PRIORITY_CLASS; - } - }; - soneUskCallbacks.put(sone.getId(), uskCallback); - boolean runBackgroundFetch = (System.currentTimeMillis() - sone.getTime()) < TimeUnit.DAYS.toMillis(7); - node.clientCore.uskManager.subscribe(USK.create(sone.getRequestUri()), uskCallback, runBackgroundFetch, (HighLevelSimpleClientImpl) client); + public void registerPassiveUsk(FreenetURI requestUri, + USKCallback uskCallback) { + try { + soneUskCallbacks.put(routingKey(requestUri), uskCallback); + node.clientCore + .uskManager + .subscribe(create(requestUri), uskCallback, false, + (RequestClient) client); } catch (MalformedURLException mue1) { - logger.log(Level.WARNING, String.format("Could not subscribe USK “%s”!", sone.getRequestUri()), mue1); + logger.log(WARNING, + format("Could not subscribe USK “%s”!", requestUri), + mue1); } } @@ -269,7 +259,7 @@ public class FreenetInterface { USKCallback uskCallback = new USKCallback() { @Override - public void onFoundEdition(long edition, USK key, ObjectContainer objectContainer, ClientContext clientContext, boolean metadata, short codec, byte[] data, boolean newKnownGood, boolean newSlotToo) { + public void onFoundEdition(long edition, USK key, ClientContext clientContext, boolean metadata, short codec, byte[] data, boolean newKnownGood, boolean newSlotToo) { callback.editionFound(key.getURI(), edition, newKnownGood, newSlotToo); } @@ -285,7 +275,7 @@ public class FreenetInterface { }; try { - node.clientCore.uskManager.subscribe(USK.create(uri), uskCallback, true, (HighLevelSimpleClientImpl) client); + node.clientCore.uskManager.subscribe(USK.create(uri), uskCallback, true, (RequestClient) client); uriUskCallbacks.put(uri, uskCallback); } catch (MalformedURLException mue1) { logger.log(Level.WARNING, String.format("Could not subscribe to USK: %s", uri), mue1); @@ -401,6 +391,7 @@ public class FreenetInterface { /** The client putter. */ private ClientPutter clientPutter; + private Bucket bucket; /** The final URI. */ private volatile FreenetURI resultingUri; @@ -432,6 +423,10 @@ public class FreenetInterface { eventBus.post(new ImageInsertStartedEvent(image)); } + public void setBucket(Bucket bucket) { + this.bucket = bucket; + } + // // ACTIONS // @@ -441,20 +436,23 @@ public class FreenetInterface { */ @SuppressWarnings("synthetic-access") public void cancel() { - clientPutter.cancel(null, node.clientCore.clientContext); + clientPutter.cancel(node.clientCore.clientContext); eventBus.post(new ImageInsertAbortedEvent(image)); + bucket.free(); } // // INTERFACE ClientPutCallback // - /** - * {@inheritDoc} - */ @Override - public void onMajorProgress(ObjectContainer objectContainer) { - /* ignore, we don’t care. */ + public RequestClient getRequestClient() { + return imageInserts; + } + + @Override + public void onResume(ClientContext context) throws ResumeFailedException { + /* ignore. */ } /** @@ -462,19 +460,20 @@ public class FreenetInterface { */ @Override @SuppressWarnings("synthetic-access") - public void onFailure(InsertException insertException, BaseClientPutter clientPutter, ObjectContainer objectContainer) { + public void onFailure(InsertException insertException, BaseClientPutter clientPutter) { if ((insertException != null) && ("Cancelled by user".equals(insertException.getMessage()))) { eventBus.post(new ImageInsertAbortedEvent(image)); } else { eventBus.post(new ImageInsertFailedEvent(image, insertException)); } + bucket.free(); } /** * {@inheritDoc} */ @Override - public void onFetchable(BaseClientPutter clientPutter, ObjectContainer objectContainer) { + public void onFetchable(BaseClientPutter clientPutter) { /* ignore, we don’t care. */ } @@ -482,7 +481,7 @@ public class FreenetInterface { * {@inheritDoc} */ @Override - public void onGeneratedMetadata(Bucket metadata, BaseClientPutter clientPutter, ObjectContainer objectContainer) { + public void onGeneratedMetadata(Bucket metadata, BaseClientPutter clientPutter) { /* ignore, we don’t care. */ } @@ -490,7 +489,7 @@ public class FreenetInterface { * {@inheritDoc} */ @Override - public void onGeneratedURI(FreenetURI generatedUri, BaseClientPutter clientPutter, ObjectContainer objectContainer) { + public void onGeneratedURI(FreenetURI generatedUri, BaseClientPutter clientPutter) { resultingUri = generatedUri; } @@ -499,8 +498,18 @@ public class FreenetInterface { */ @Override @SuppressWarnings("synthetic-access") - public void onSuccess(BaseClientPutter clientPutter, ObjectContainer objectContainer) { + public void onSuccess(BaseClientPutter clientPutter) { eventBus.post(new ImageInsertFinishedEvent(image, resultingUri)); + bucket.free(); + } + + } + + public class InsertTokenSupplier implements Function { + + @Override + public InsertToken apply(Image image) { + return new InsertToken(image); } } diff --git a/src/main/java/net/pterodactylus/sone/core/ImageInserter.java b/src/main/java/net/pterodactylus/sone/core/ImageInserter.java index 791663f..25362a6 100644 --- a/src/main/java/net/pterodactylus/sone/core/ImageInserter.java +++ b/src/main/java/net/pterodactylus/sone/core/ImageInserter.java @@ -19,6 +19,7 @@ package net.pterodactylus.sone.core; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; +import static java.util.logging.Logger.getLogger; import java.util.Collections; import java.util.HashMap; @@ -29,7 +30,8 @@ import java.util.logging.Logger; import net.pterodactylus.sone.core.FreenetInterface.InsertToken; import net.pterodactylus.sone.data.Image; import net.pterodactylus.sone.data.TemporaryImage; -import net.pterodactylus.util.logging.Logging; + +import com.google.common.base.Function; /** * The image inserter is responsible for inserting images using @@ -42,10 +44,11 @@ import net.pterodactylus.util.logging.Logging; public class ImageInserter { /** The logger. */ - private static final Logger logger = Logging.getLogger(ImageInserter.class); + private static final Logger logger = getLogger("Sone.Image.Inserter"); /** The freenet interface. */ private final FreenetInterface freenetInterface; + private final Function insertTokenSupplier; /** The tokens of running inserts. */ private final Map insertTokens = Collections.synchronizedMap(new HashMap()); @@ -55,9 +58,12 @@ public class ImageInserter { * * @param freenetInterface * The freenet interface + * @param insertTokenSupplier + * The supplier for insert tokens */ - public ImageInserter(FreenetInterface freenetInterface) { + public ImageInserter(FreenetInterface freenetInterface, Function insertTokenSupplier) { this.freenetInterface = freenetInterface; + this.insertTokenSupplier = insertTokenSupplier; } /** @@ -73,7 +79,7 @@ public class ImageInserter { checkNotNull(image, "image must not be null"); checkArgument(image.getId().equals(temporaryImage.getId()), "image IDs must match"); try { - InsertToken insertToken = freenetInterface.new InsertToken(image); + InsertToken insertToken = insertTokenSupplier.apply(image); insertTokens.put(image.getId(), insertToken); freenetInterface.insertImage(temporaryImage, image, insertToken); } catch (SoneException se1) { diff --git a/src/main/java/net/pterodactylus/sone/core/Options.java b/src/main/java/net/pterodactylus/sone/core/Options.java index c8e3589..9e79fca 100644 --- a/src/main/java/net/pterodactylus/sone/core/Options.java +++ b/src/main/java/net/pterodactylus/sone/core/Options.java @@ -21,6 +21,8 @@ import java.util.Collections; import java.util.HashMap; import java.util.Map; +import net.pterodactylus.sone.utils.Option; + import com.google.common.base.Predicate; /** @@ -30,211 +32,6 @@ import com.google.common.base.Predicate; */ public class Options { - /** - * Contains current and default value of an option. - * - * @param - * The type of the option - * @author David ‘Bombe’ Roden - */ - public static interface Option { - - /** - * Returns the default value of the option. - * - * @return The default value of the option - */ - public T getDefault(); - - /** - * Returns the current value of the option. If the current value is not - * set (usually {@code null}), the default value is returned. - * - * @return The current value of the option - */ - public T get(); - - /** - * Returns the real value of the option. This will also return an unset - * value (usually {@code null})! - * - * @return The real value of the option - */ - public T getReal(); - - /** - * Validates the given value. Note that {@code null} is always a valid - * value! - * - * @param value - * The value to validate - * @return {@code true} if this option does not have a validator, or the - * validator validates this object, {@code false} otherwise - */ - public boolean validate(T value); - - /** - * Sets the current value of the option. - * - * @param value - * The new value of the option - * @throws IllegalArgumentException - * if the value is not valid for this option - */ - public void set(T value) throws IllegalArgumentException; - - } - - /** - * Interface for objects that want to be notified when an option changes its - * value. - * - * @param - * The type of the option - * @author David ‘Bombe’ Roden - */ - public static interface OptionWatcher { - - /** - * Notifies an object that an option has been changed. - * - * @param option - * The option that has changed - * @param oldValue - * The old value of the option - * @param newValue - * The new value of the option - */ - public void optionChanged(Option option, T oldValue, T newValue); - - } - - /** - * Basic implementation of an {@link Option} that notifies an - * {@link OptionWatcher} if the value changes. - * - * @param - * The type of the option - * @author David ‘Bombe’ Roden - */ - public static class DefaultOption implements Option { - - /** The default value. */ - private final T defaultValue; - - /** The current value. */ - private volatile T value; - - /** The validator. */ - private Predicate validator; - - /** The option watcher. */ - private final OptionWatcher optionWatcher; - - /** - * Creates a new default option. - * - * @param defaultValue - * The default value of the option - */ - public DefaultOption(T defaultValue) { - this(defaultValue, (OptionWatcher) null); - } - - /** - * Creates a new default option. - * - * @param defaultValue - * The default value of the option - * @param validator - * The validator for value validation (may be {@code null}) - */ - public DefaultOption(T defaultValue, Predicate validator) { - this(defaultValue, validator, null); - } - - /** - * Creates a new default option. - * - * @param defaultValue - * The default value of the option - * @param optionWatchers - * The option watchers (may be {@code null}) - */ - public DefaultOption(T defaultValue, OptionWatcher optionWatchers) { - this(defaultValue, null, optionWatchers); - } - - /** - * Creates a new default option. - * - * @param defaultValue - * The default value of the option - * @param validator - * The validator for value validation (may be {@code null}) - * @param optionWatcher - * The option watcher (may be {@code null}) - */ - public DefaultOption(T defaultValue, Predicate validator, OptionWatcher optionWatcher) { - this.defaultValue = defaultValue; - this.validator = validator; - this.optionWatcher = optionWatcher; - } - - /** - * {@inheritDoc} - */ - @Override - public T getDefault() { - return defaultValue; - } - - /** - * {@inheritDoc} - */ - @Override - public T get() { - return (value != null) ? value : defaultValue; - } - - /** - * Returns the real value of the option. This will also return an unset - * value (usually {@code null})! - * - * @return The real value of the option - */ - @Override - public T getReal() { - return value; - } - - /** - * {@inheritDoc} - */ - @Override - public boolean validate(T value) { - return (validator == null) || (value == null) || validator.apply(value); - } - - /** - * {@inheritDoc} - */ - @Override - public void set(T value) { - if ((value != null) && (validator != null) && (!validator.apply(value))) { - throw new IllegalArgumentException("New Value (" + value + ") could not be validated."); - } - T oldValue = this.value; - this.value = value; - if (!get().equals(oldValue)) { - if (optionWatcher != null) { - optionWatcher.optionChanged(this, oldValue, get()); - } - } - } - - } - /** Holds all {@link Boolean} {@link Option}s. */ private final Map> booleanOptions = Collections.synchronizedMap(new HashMap>()); diff --git a/src/main/java/net/pterodactylus/sone/core/Preferences.java b/src/main/java/net/pterodactylus/sone/core/Preferences.java index 16d9453..56bfa73 100644 --- a/src/main/java/net/pterodactylus/sone/core/Preferences.java +++ b/src/main/java/net/pterodactylus/sone/core/Preferences.java @@ -17,8 +17,24 @@ package net.pterodactylus.sone.core; +import static com.google.common.base.Predicates.equalTo; +import static java.lang.Integer.MAX_VALUE; +import static net.pterodactylus.sone.fcp.FcpInterface.FullAccessRequired.ALWAYS; +import static net.pterodactylus.sone.utils.IntegerRangePredicate.range; + +import net.pterodactylus.sone.core.event.InsertionDelayChangedEvent; import net.pterodactylus.sone.fcp.FcpInterface; import net.pterodactylus.sone.fcp.FcpInterface.FullAccessRequired; +import net.pterodactylus.sone.fcp.event.FcpInterfaceActivatedEvent; +import net.pterodactylus.sone.fcp.event.FcpInterfaceDeactivatedEvent; +import net.pterodactylus.sone.fcp.event.FullAccessRequiredChanged; +import net.pterodactylus.sone.utils.DefaultOption; +import net.pterodactylus.sone.utils.Option; +import net.pterodactylus.util.config.Configuration; +import net.pterodactylus.util.config.ConfigurationException; + +import com.google.common.base.Predicates; +import com.google.common.eventbus.EventBus; /** * Convenience interface for external classes that want to access the core’s @@ -28,17 +44,33 @@ import net.pterodactylus.sone.fcp.FcpInterface.FullAccessRequired; */ public class Preferences { - /** The wrapped options. */ - private final Options options; - - /** - * Creates a new preferences object wrapped around the given options. - * - * @param options - * The options to wrap - */ - public Preferences(Options options) { - this.options = options; + private final EventBus eventBus; + private final Option insertionDelay = + new DefaultOption(60, range(0, MAX_VALUE)); + private final Option postsPerPage = + new DefaultOption(10, range(1, MAX_VALUE)); + private final Option imagesPerPage = + new DefaultOption(9, range(1, MAX_VALUE)); + private final Option charactersPerPost = + new DefaultOption(400, Predicates.or( + range(50, MAX_VALUE), equalTo(-1))); + private final Option postCutOffLength = + new DefaultOption(200, range(50, MAX_VALUE)); + private final Option requireFullAccess = + new DefaultOption(false); + private final Option positiveTrust = + new DefaultOption(75, range(0, 100)); + private final Option negativeTrust = + new DefaultOption(-25, range(-100, 100)); + private final Option trustComment = + new DefaultOption("Set from Sone Web Interface"); + private final Option activateFcpInterface = + new DefaultOption(false); + private final Option fcpFullAccessRequired = + new DefaultOption(ALWAYS); + + public Preferences(EventBus eventBus) { + this.eventBus = eventBus; } /** @@ -47,7 +79,7 @@ public class Preferences { * @return The insertion delay */ public int getInsertionDelay() { - return options.getIntegerOption("InsertionDelay").get(); + return insertionDelay.get(); } /** @@ -59,7 +91,7 @@ public class Preferences { * {@code false} otherwise */ public boolean validateInsertionDelay(Integer insertionDelay) { - return options.getIntegerOption("InsertionDelay").validate(insertionDelay); + return this.insertionDelay.validate(insertionDelay); } /** @@ -71,7 +103,8 @@ public class Preferences { * @return This preferences */ public Preferences setInsertionDelay(Integer insertionDelay) { - options.getIntegerOption("InsertionDelay").set(insertionDelay); + this.insertionDelay.set(insertionDelay); + eventBus.post(new InsertionDelayChangedEvent(getInsertionDelay())); return this; } @@ -81,7 +114,7 @@ public class Preferences { * @return The number of posts to show per page */ public int getPostsPerPage() { - return options.getIntegerOption("PostsPerPage").get(); + return postsPerPage.get(); } /** @@ -93,7 +126,7 @@ public class Preferences { * {@code false} otherwise */ public boolean validatePostsPerPage(Integer postsPerPage) { - return options.getIntegerOption("PostsPerPage").validate(postsPerPage); + return this.postsPerPage.validate(postsPerPage); } /** @@ -104,7 +137,7 @@ public class Preferences { * @return This preferences object */ public Preferences setPostsPerPage(Integer postsPerPage) { - options.getIntegerOption("PostsPerPage").set(postsPerPage); + this.postsPerPage.set(postsPerPage); return this; } @@ -114,7 +147,7 @@ public class Preferences { * @return The number of images to show per page */ public int getImagesPerPage() { - return options.getIntegerOption("ImagesPerPage").get(); + return imagesPerPage.get(); } /** @@ -126,7 +159,7 @@ public class Preferences { * {@code false} otherwise */ public boolean validateImagesPerPage(Integer imagesPerPage) { - return options.getIntegerOption("ImagesPerPage").validate(imagesPerPage); + return this.imagesPerPage.validate(imagesPerPage); } /** @@ -137,7 +170,7 @@ public class Preferences { * @return This preferences object */ public Preferences setImagesPerPage(Integer imagesPerPage) { - options.getIntegerOption("ImagesPerPage").set(imagesPerPage); + this.imagesPerPage.set(imagesPerPage); return this; } @@ -148,7 +181,7 @@ public class Preferences { * @return The numbers of characters per post */ public int getCharactersPerPost() { - return options.getIntegerOption("CharactersPerPost").get(); + return charactersPerPost.get(); } /** @@ -160,7 +193,7 @@ public class Preferences { * {@code false} otherwise */ public boolean validateCharactersPerPost(Integer charactersPerPost) { - return options.getIntegerOption("CharactersPerPost").validate(charactersPerPost); + return this.charactersPerPost.validate(charactersPerPost); } /** @@ -172,7 +205,7 @@ public class Preferences { * @return This preferences objects */ public Preferences setCharactersPerPost(Integer charactersPerPost) { - options.getIntegerOption("CharactersPerPost").set(charactersPerPost); + this.charactersPerPost.set(charactersPerPost); return this; } @@ -182,7 +215,7 @@ public class Preferences { * @return The number of characters of the snippet */ public int getPostCutOffLength() { - return options.getIntegerOption("PostCutOffLength").get(); + return postCutOffLength.get(); } /** @@ -194,7 +227,7 @@ public class Preferences { * valid, {@code false} otherwise */ public boolean validatePostCutOffLength(Integer postCutOffLength) { - return options.getIntegerOption("PostCutOffLength").validate(postCutOffLength); + return this.postCutOffLength.validate(postCutOffLength); } /** @@ -205,7 +238,7 @@ public class Preferences { * @return This preferences */ public Preferences setPostCutOffLength(Integer postCutOffLength) { - options.getIntegerOption("PostCutOffLength").set(postCutOffLength); + this.postCutOffLength.set(postCutOffLength); return this; } @@ -216,7 +249,7 @@ public class Preferences { * otherwise */ public boolean isRequireFullAccess() { - return options.getBooleanOption("RequireFullAccess").get(); + return requireFullAccess.get(); } /** @@ -227,7 +260,7 @@ public class Preferences { * otherwise */ public void setRequireFullAccess(Boolean requireFullAccess) { - options.getBooleanOption("RequireFullAccess").set(requireFullAccess); + this.requireFullAccess.set(requireFullAccess); } /** @@ -236,7 +269,7 @@ public class Preferences { * @return The positive trust */ public int getPositiveTrust() { - return options.getIntegerOption("PositiveTrust").get(); + return positiveTrust.get(); } /** @@ -248,7 +281,7 @@ public class Preferences { * otherwise */ public boolean validatePositiveTrust(Integer positiveTrust) { - return options.getIntegerOption("PositiveTrust").validate(positiveTrust); + return this.positiveTrust.validate(positiveTrust); } /** @@ -260,7 +293,7 @@ public class Preferences { * @return This preferences */ public Preferences setPositiveTrust(Integer positiveTrust) { - options.getIntegerOption("PositiveTrust").set(positiveTrust); + this.positiveTrust.set(positiveTrust); return this; } @@ -270,7 +303,7 @@ public class Preferences { * @return The negative trust */ public int getNegativeTrust() { - return options.getIntegerOption("NegativeTrust").get(); + return negativeTrust.get(); } /** @@ -282,7 +315,7 @@ public class Preferences { * otherwise */ public boolean validateNegativeTrust(Integer negativeTrust) { - return options.getIntegerOption("NegativeTrust").validate(negativeTrust); + return this.negativeTrust.validate(negativeTrust); } /** @@ -294,7 +327,7 @@ public class Preferences { * @return The preferences */ public Preferences setNegativeTrust(Integer negativeTrust) { - options.getIntegerOption("NegativeTrust").set(negativeTrust); + this.negativeTrust.set(negativeTrust); return this; } @@ -305,7 +338,7 @@ public class Preferences { * @return The trust comment */ public String getTrustComment() { - return options.getStringOption("TrustComment").get(); + return trustComment.get(); } /** @@ -317,7 +350,7 @@ public class Preferences { * @return This preferences */ public Preferences setTrustComment(String trustComment) { - options.getStringOption("TrustComment").set(trustComment); + this.trustComment.set(trustComment); return this; } @@ -330,7 +363,7 @@ public class Preferences { * {@code false} otherwise */ public boolean isFcpInterfaceActive() { - return options.getBooleanOption("ActivateFcpInterface").get(); + return activateFcpInterface.get(); } /** @@ -343,8 +376,13 @@ public class Preferences { * to deactivate the FCP interface * @return This preferences object */ - public Preferences setFcpInterfaceActive(boolean fcpInterfaceActive) { - options.getBooleanOption("ActivateFcpInterface").set(fcpInterfaceActive); + public Preferences setFcpInterfaceActive(Boolean fcpInterfaceActive) { + this.activateFcpInterface.set(fcpInterfaceActive); + if (isFcpInterfaceActive()) { + eventBus.post(new FcpInterfaceActivatedEvent()); + } else { + eventBus.post(new FcpInterfaceDeactivatedEvent()); + } return this; } @@ -356,7 +394,7 @@ public class Preferences { * is required */ public FullAccessRequired getFcpFullAccessRequired() { - return FullAccessRequired.values()[options.getIntegerOption("FcpFullAccessRequired").get()]; + return fcpFullAccessRequired.get(); } /** @@ -367,9 +405,30 @@ public class Preferences { * The action level * @return This preferences */ - public Preferences setFcpFullAccessRequired(FullAccessRequired fcpFullAccessRequired) { - options.getIntegerOption("FcpFullAccessRequired").set((fcpFullAccessRequired != null) ? fcpFullAccessRequired.ordinal() : null); + public Preferences setFcpFullAccessRequired( + FullAccessRequired fcpFullAccessRequired) { + this.fcpFullAccessRequired.set(fcpFullAccessRequired); + eventBus.post(new FullAccessRequiredChanged(getFcpFullAccessRequired())); return this; } + public void saveTo(Configuration configuration) throws ConfigurationException { + configuration.getIntValue("Option/ConfigurationVersion").setValue(0); + configuration.getIntValue("Option/InsertionDelay").setValue(insertionDelay.getReal()); + configuration.getIntValue("Option/PostsPerPage").setValue(postsPerPage.getReal()); + configuration.getIntValue("Option/ImagesPerPage").setValue(imagesPerPage.getReal()); + configuration.getIntValue("Option/CharactersPerPost").setValue(charactersPerPost.getReal()); + configuration.getIntValue("Option/PostCutOffLength").setValue(postCutOffLength.getReal()); + configuration.getBooleanValue("Option/RequireFullAccess").setValue(requireFullAccess.getReal()); + configuration.getIntValue("Option/PositiveTrust").setValue(positiveTrust.getReal()); + configuration.getIntValue("Option/NegativeTrust").setValue(negativeTrust.getReal()); + configuration.getStringValue("Option/TrustComment").setValue(trustComment.getReal()); + configuration.getBooleanValue("Option/ActivateFcpInterface").setValue(activateFcpInterface.getReal()); + configuration.getIntValue("Option/FcpFullAccessRequired").setValue(toInt(fcpFullAccessRequired.getReal())); + } + + private Integer toInt(FullAccessRequired fullAccessRequired) { + return (fullAccessRequired == null) ? null : fullAccessRequired.ordinal(); + } + } diff --git a/src/main/java/net/pterodactylus/sone/core/PreferencesLoader.java b/src/main/java/net/pterodactylus/sone/core/PreferencesLoader.java new file mode 100644 index 0000000..0ae13ba --- /dev/null +++ b/src/main/java/net/pterodactylus/sone/core/PreferencesLoader.java @@ -0,0 +1,105 @@ +package net.pterodactylus.sone.core; + +import net.pterodactylus.sone.fcp.FcpInterface.FullAccessRequired; +import net.pterodactylus.util.config.Configuration; +import net.pterodactylus.util.config.ConfigurationException; + +/** + * Loads preferences stored in a {@link Configuration} into a {@link + * Preferences} object. + * + * @author David ‘Bombe’ Roden + */ +public class PreferencesLoader { + + private final Preferences preferences; + + public PreferencesLoader(Preferences preferences) { + this.preferences = preferences; + } + + public void loadFrom(Configuration configuration) { + loadInsertionDelay(configuration); + loadPostsPerPage(configuration); + loadImagesPerPage(configuration); + loadCharactersPerPost(configuration); + loadPostCutOffLength(configuration); + loadRequireFullAccess(configuration); + loadPositiveTrust(configuration); + loadNegativeTrust(configuration); + loadTrustComment(configuration); + loadFcpInterfaceActive(configuration); + loadFcpFullAccessRequired(configuration); + } + + private void loadInsertionDelay(Configuration configuration) { + preferences.setInsertionDelay(configuration.getIntValue( + "Option/InsertionDelay").getValue(null)); + } + + private void loadPostsPerPage(Configuration configuration) { + preferences.setPostsPerPage( + configuration.getIntValue("Option/PostsPerPage") + .getValue(null)); + } + + private void loadImagesPerPage(Configuration configuration) { + preferences.setImagesPerPage( + configuration.getIntValue("Option/ImagesPerPage") + .getValue(null)); + } + + private void loadCharactersPerPost(Configuration configuration) { + preferences.setCharactersPerPost( + configuration.getIntValue("Option/CharactersPerPost") + .getValue(null)); + } + + private void loadPostCutOffLength(Configuration configuration) { + try { + preferences.setPostCutOffLength( + configuration.getIntValue("Option/PostCutOffLength") + .getValue(null)); + } catch (IllegalArgumentException iae1) { + /* previous versions allowed -1, ignore and use default. */ + } + } + + private void loadRequireFullAccess(Configuration configuration) { + preferences.setRequireFullAccess( + configuration.getBooleanValue("Option/RequireFullAccess") + .getValue(null)); + } + + private void loadPositiveTrust(Configuration configuration) { + preferences.setPositiveTrust( + configuration.getIntValue("Option/PositiveTrust") + .getValue(null)); + } + + private void loadNegativeTrust(Configuration configuration) { + preferences.setNegativeTrust( + configuration.getIntValue("Option/NegativeTrust") + .getValue(null)); + } + + private void loadTrustComment(Configuration configuration) { + preferences.setTrustComment( + configuration.getStringValue("Option/TrustComment") + .getValue(null)); + } + + private void loadFcpInterfaceActive(Configuration configuration) { + preferences.setFcpInterfaceActive(configuration.getBooleanValue( + "Option/ActivateFcpInterface").getValue(null)); + } + + private void loadFcpFullAccessRequired(Configuration configuration) { + Integer fullAccessRequiredInteger = configuration + .getIntValue("Option/FcpFullAccessRequired").getValue(null); + preferences.setFcpFullAccessRequired( + (fullAccessRequiredInteger == null) ? null : + FullAccessRequired.values()[fullAccessRequiredInteger]); + } + +} diff --git a/src/main/java/net/pterodactylus/sone/core/SoneChangeDetector.java b/src/main/java/net/pterodactylus/sone/core/SoneChangeDetector.java new file mode 100644 index 0000000..efcdc6e --- /dev/null +++ b/src/main/java/net/pterodactylus/sone/core/SoneChangeDetector.java @@ -0,0 +1,114 @@ +package net.pterodactylus.sone.core; + +import static com.google.common.base.Optional.absent; +import static com.google.common.base.Optional.fromNullable; +import static com.google.common.collect.FluentIterable.from; + +import java.util.Collection; + +import net.pterodactylus.sone.data.Post; +import net.pterodactylus.sone.data.PostReply; +import net.pterodactylus.sone.data.Sone; + +import com.google.common.base.Optional; +import com.google.common.base.Predicate; +import com.google.common.collect.FluentIterable; + +/** + * Compares the contents of two {@link Sone}s and fires events for new and + * removed elements. + * + * @author David ‘Bombe’ Roden + */ +public class SoneChangeDetector { + + private final Sone oldSone; + private Optional newPostProcessor = absent(); + private Optional removedPostProcessor = absent(); + private Optional newPostReplyProcessor = absent(); + private Optional removedPostReplyProcessor = absent(); + + public SoneChangeDetector(Sone oldSone) { + this.oldSone = oldSone; + } + + public void onNewPosts(PostProcessor newPostProcessor) { + this.newPostProcessor = fromNullable(newPostProcessor); + } + + public void onRemovedPosts(PostProcessor removedPostProcessor) { + this.removedPostProcessor = fromNullable(removedPostProcessor); + } + + public void onNewPostReplies(PostReplyProcessor newPostReplyProcessor) { + this.newPostReplyProcessor = fromNullable(newPostReplyProcessor); + } + + public void onRemovedPostReplies( + PostReplyProcessor removedPostReplyProcessor) { + this.removedPostReplyProcessor = fromNullable(removedPostReplyProcessor); + } + + public void detectChanges(Sone newSone) { + processPosts(from(newSone.getPosts()).filter( + notContainedIn(oldSone.getPosts())), newPostProcessor); + processPosts(from(oldSone.getPosts()).filter( + notContainedIn(newSone.getPosts())), removedPostProcessor); + processPostReplies(from(newSone.getReplies()).filter( + notContainedIn(oldSone.getReplies())), newPostReplyProcessor); + processPostReplies(from(oldSone.getReplies()).filter( + notContainedIn(newSone.getReplies())), removedPostReplyProcessor); + } + + private void processPostReplies(FluentIterable postReplies, + Optional postReplyProcessor) { + for (PostReply postReply : postReplies) { + notifyPostReplyProcessor(postReplyProcessor, postReply); + } + } + + private void notifyPostReplyProcessor( + Optional postReplyProcessor, + PostReply postReply) { + if (postReplyProcessor.isPresent()) { + postReplyProcessor.get() + .processPostReply(postReply); + } + } + + private void processPosts(FluentIterable posts, + Optional newPostProcessor) { + for (Post post : posts) { + notifyPostProcessor(newPostProcessor, post); + } + } + + private void notifyPostProcessor(Optional postProcessor, + Post newPost) { + if (postProcessor.isPresent()) { + postProcessor.get().processPost(newPost); + } + } + + private Predicate notContainedIn(final Collection posts) { + return new Predicate() { + @Override + public boolean apply(T element) { + return !posts.contains(element); + } + }; + } + + public interface PostProcessor { + + void processPost(Post post); + + } + + public interface PostReplyProcessor { + + void processPostReply(PostReply postReply); + + } + +} diff --git a/src/main/java/net/pterodactylus/sone/core/SoneDownloader.java b/src/main/java/net/pterodactylus/sone/core/SoneDownloader.java index 53eef16..be0be02 100644 --- a/src/main/java/net/pterodactylus/sone/core/SoneDownloader.java +++ b/src/main/java/net/pterodactylus/sone/core/SoneDownloader.java @@ -1,536 +1,23 @@ -/* - * Sone - SoneDownloader.java - Copyright © 2010–2013 David Roden - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - package net.pterodactylus.sone.core; -import java.io.InputStream; -import java.net.MalformedURLException; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.logging.Level; -import java.util.logging.Logger; - -import net.pterodactylus.sone.core.FreenetInterface.Fetched; -import net.pterodactylus.sone.data.Album; -import net.pterodactylus.sone.data.Client; -import net.pterodactylus.sone.data.Image; -import net.pterodactylus.sone.data.Post; -import net.pterodactylus.sone.data.PostReply; -import net.pterodactylus.sone.data.Profile; import net.pterodactylus.sone.data.Sone; -import net.pterodactylus.sone.data.Sone.SoneStatus; -import net.pterodactylus.sone.data.SoneImpl; -import net.pterodactylus.sone.database.PostBuilder; -import net.pterodactylus.sone.database.PostReplyBuilder; -import net.pterodactylus.util.io.Closer; -import net.pterodactylus.util.logging.Logging; -import net.pterodactylus.util.number.Numbers; -import net.pterodactylus.util.service.AbstractService; -import net.pterodactylus.util.xml.SimpleXML; -import net.pterodactylus.util.xml.XML; +import net.pterodactylus.util.service.Service; -import org.w3c.dom.Document; - -import freenet.client.FetchResult; import freenet.keys.FreenetURI; -import freenet.support.api.Bucket; /** - * The Sone downloader is responsible for download Sones as they are updated. + * Downloads and parses Sone and {@link Core#updateSone(Sone) updates the + * core}. * * @author David ‘Bombe’ Roden */ -public class SoneDownloader extends AbstractService { - - /** The logger. */ - private static final Logger logger = Logging.getLogger(SoneDownloader.class); - - /** The maximum protocol version. */ - private static final int MAX_PROTOCOL_VERSION = 0; - - /** The core. */ - private final Core core; - - /** The Freenet interface. */ - private final FreenetInterface freenetInterface; - - /** The sones to update. */ - private final Set sones = new HashSet(); - - /** - * Creates a new Sone downloader. - * - * @param core - * The core - * @param freenetInterface - * The Freenet interface - */ - public SoneDownloader(Core core, FreenetInterface freenetInterface) { - super("Sone Downloader", false); - this.core = core; - this.freenetInterface = freenetInterface; - } - - // - // ACTIONS - // - - /** - * Adds the given Sone to the set of Sones that will be watched for updates. - * - * @param sone - * The Sone to add - */ - public void addSone(Sone sone) { - if (!sones.add(sone)) { - freenetInterface.unregisterUsk(sone); - } - freenetInterface.registerUsk(sone, this); - } - - /** - * Removes the given Sone from the downloader. - * - * @param sone - * The Sone to stop watching - */ - public void removeSone(Sone sone) { - if (sones.remove(sone)) { - freenetInterface.unregisterUsk(sone); - } - } - - /** - * Fetches the updated Sone. This method is a callback method for - * {@link FreenetInterface#registerUsk(Sone, SoneDownloader)}. - * - * @param sone - * The Sone to fetch - */ - public void fetchSone(Sone sone) { - fetchSone(sone, sone.getRequestUri().sskForUSK()); - } - - /** - * Fetches the updated Sone. This method can be used to fetch a Sone from a - * specific URI. - * - * @param sone - * The Sone to fetch - * @param soneUri - * The URI to fetch the Sone from - */ - public void fetchSone(Sone sone, FreenetURI soneUri) { - fetchSone(sone, soneUri, false); - } - - /** - * Fetches the Sone from the given URI. - * - * @param sone - * The Sone to fetch - * @param soneUri - * The URI of the Sone to fetch - * @param fetchOnly - * {@code true} to only fetch and parse the Sone, {@code false} - * to {@link Core#updateSone(Sone) update} it in the core - * @return The downloaded Sone, or {@code null} if the Sone could not be - * downloaded - */ - public Sone fetchSone(Sone sone, FreenetURI soneUri, boolean fetchOnly) { - logger.log(Level.FINE, String.format("Starting fetch for Sone “%s” from %s…", sone, soneUri)); - FreenetURI requestUri = soneUri.setMetaString(new String[] { "sone.xml" }); - sone.setStatus(SoneStatus.downloading); - try { - Fetched fetchResults = freenetInterface.fetchUri(requestUri); - if (fetchResults == null) { - /* TODO - mark Sone as bad. */ - return null; - } - logger.log(Level.FINEST, String.format("Got %d bytes back.", fetchResults.getFetchResult().size())); - Sone parsedSone = parseSone(sone, fetchResults.getFetchResult(), fetchResults.getFreenetUri()); - if (parsedSone != null) { - if (!fetchOnly) { - parsedSone.setStatus((parsedSone.getTime() == 0) ? SoneStatus.unknown : SoneStatus.idle); - core.updateSone(parsedSone); - addSone(parsedSone); - } - } - return parsedSone; - } finally { - sone.setStatus((sone.getTime() == 0) ? SoneStatus.unknown : SoneStatus.idle); - } - } - - /** - * Parses a Sone from a fetch result. - * - * @param originalSone - * The sone to parse, or {@code null} if the Sone is yet unknown - * @param fetchResult - * The fetch result - * @param requestUri - * The requested URI - * @return The parsed Sone, or {@code null} if the Sone could not be parsed - */ - public Sone parseSone(Sone originalSone, FetchResult fetchResult, FreenetURI requestUri) { - logger.log(Level.FINEST, String.format("Parsing FetchResult (%d bytes, %s) for %s…", fetchResult.size(), fetchResult.getMimeType(), originalSone)); - Bucket soneBucket = fetchResult.asBucket(); - InputStream soneInputStream = null; - try { - soneInputStream = soneBucket.getInputStream(); - Sone parsedSone = parseSone(originalSone, soneInputStream); - if (parsedSone != null) { - parsedSone.setLatestEdition(requestUri.getEdition()); - if (requestUri.getKeyType().equals("USK")) { - parsedSone.setRequestUri(requestUri.setMetaString(new String[0])); - } else { - parsedSone.setRequestUri(requestUri.setKeyType("USK").setDocName("Sone").setMetaString(new String[0])); - } - } - return parsedSone; - } catch (Exception e1) { - logger.log(Level.WARNING, String.format("Could not parse Sone from %s!", requestUri), e1); - } finally { - Closer.close(soneInputStream); - soneBucket.free(); - } - return null; - } - - /** - * Parses a Sone from the given input stream and creates a new Sone from the - * parsed data. - * - * @param originalSone - * The Sone to update - * @param soneInputStream - * The input stream to parse the Sone from - * @return The parsed Sone - * @throws SoneException - * if a parse error occurs, or the protocol is invalid - */ - public Sone parseSone(Sone originalSone, InputStream soneInputStream) throws SoneException { - /* TODO - impose a size limit? */ - - Document document; - /* XML parsing is not thread-safe. */ - synchronized (this) { - document = XML.transformToDocument(soneInputStream); - } - if (document == null) { - /* TODO - mark Sone as bad. */ - logger.log(Level.WARNING, String.format("Could not parse XML for Sone %s!", originalSone)); - return null; - } - - Sone sone = new SoneImpl(originalSone.getId(), originalSone.isLocal()).setIdentity(originalSone.getIdentity()); - - SimpleXML soneXml; - try { - soneXml = SimpleXML.fromDocument(document); - } catch (NullPointerException npe1) { - /* for some reason, invalid XML can cause NPEs. */ - logger.log(Level.WARNING, String.format("XML for Sone %s can not be parsed!", sone), npe1); - return null; - } - - Integer protocolVersion = null; - String soneProtocolVersion = soneXml.getValue("protocol-version", null); - if (soneProtocolVersion != null) { - protocolVersion = Numbers.safeParseInteger(soneProtocolVersion); - } - if (protocolVersion == null) { - logger.log(Level.INFO, "No protocol version found, assuming 0."); - protocolVersion = 0; - } - - if (protocolVersion < 0) { - logger.log(Level.WARNING, String.format("Invalid protocol version: %d! Not parsing Sone.", protocolVersion)); - return null; - } - - /* check for valid versions. */ - if (protocolVersion > MAX_PROTOCOL_VERSION) { - logger.log(Level.WARNING, String.format("Unknown protocol version: %d! Not parsing Sone.", protocolVersion)); - return null; - } - - String soneTime = soneXml.getValue("time", null); - if (soneTime == null) { - /* TODO - mark Sone as bad. */ - logger.log(Level.WARNING, String.format("Downloaded time for Sone %s was null!", sone)); - return null; - } - try { - sone.setTime(Long.parseLong(soneTime)); - } catch (NumberFormatException nfe1) { - /* TODO - mark Sone as bad. */ - logger.log(Level.WARNING, String.format("Downloaded Sone %s with invalid time: %s", sone, soneTime)); - return null; - } - - SimpleXML clientXml = soneXml.getNode("client"); - if (clientXml != null) { - String clientName = clientXml.getValue("name", null); - String clientVersion = clientXml.getValue("version", null); - if ((clientName == null) || (clientVersion == null)) { - logger.log(Level.WARNING, String.format("Download Sone %s with client XML but missing name or version!", sone)); - return null; - } - sone.setClient(new Client(clientName, clientVersion)); - } - - String soneRequestUri = soneXml.getValue("request-uri", null); - if (soneRequestUri != null) { - try { - sone.setRequestUri(new FreenetURI(soneRequestUri)); - } catch (MalformedURLException mue1) { - /* TODO - mark Sone as bad. */ - logger.log(Level.WARNING, String.format("Downloaded Sone %s has invalid request URI: %s", sone, soneRequestUri), mue1); - return null; - } - } - - if (originalSone.getInsertUri() != null) { - sone.setInsertUri(originalSone.getInsertUri()); - } - - SimpleXML profileXml = soneXml.getNode("profile"); - if (profileXml == null) { - /* TODO - mark Sone as bad. */ - logger.log(Level.WARNING, String.format("Downloaded Sone %s has no profile!", sone)); - return null; - } - - /* parse profile. */ - String profileFirstName = profileXml.getValue("first-name", null); - String profileMiddleName = profileXml.getValue("middle-name", null); - String profileLastName = profileXml.getValue("last-name", null); - Integer profileBirthDay = Numbers.safeParseInteger(profileXml.getValue("birth-day", null)); - Integer profileBirthMonth = Numbers.safeParseInteger(profileXml.getValue("birth-month", null)); - Integer profileBirthYear = Numbers.safeParseInteger(profileXml.getValue("birth-year", null)); - Profile profile = new Profile(sone).setFirstName(profileFirstName).setMiddleName(profileMiddleName).setLastName(profileLastName); - profile.setBirthDay(profileBirthDay).setBirthMonth(profileBirthMonth).setBirthYear(profileBirthYear); - /* avatar is processed after images are loaded. */ - String avatarId = profileXml.getValue("avatar", null); - - /* parse profile fields. */ - SimpleXML profileFieldsXml = profileXml.getNode("fields"); - if (profileFieldsXml != null) { - for (SimpleXML fieldXml : profileFieldsXml.getNodes("field")) { - String fieldName = fieldXml.getValue("field-name", null); - String fieldValue = fieldXml.getValue("field-value", ""); - if (fieldName == null) { - logger.log(Level.WARNING, String.format("Downloaded profile field for Sone %s with missing data! Name: %s, Value: %s", sone, fieldName, fieldValue)); - return null; - } - try { - profile.addField(fieldName).setValue(fieldValue); - } catch (IllegalArgumentException iae1) { - logger.log(Level.WARNING, String.format("Duplicate field: %s", fieldName), iae1); - return null; - } - } - } - - /* parse posts. */ - SimpleXML postsXml = soneXml.getNode("posts"); - Set posts = new HashSet(); - if (postsXml == null) { - /* TODO - mark Sone as bad. */ - logger.log(Level.WARNING, String.format("Downloaded Sone %s has no posts!", sone)); - } else { - for (SimpleXML postXml : postsXml.getNodes("post")) { - String postId = postXml.getValue("id", null); - String postRecipientId = postXml.getValue("recipient", null); - String postTime = postXml.getValue("time", null); - String postText = postXml.getValue("text", null); - if ((postId == null) || (postTime == null) || (postText == null)) { - /* TODO - mark Sone as bad. */ - logger.log(Level.WARNING, String.format("Downloaded post for Sone %s with missing data! ID: %s, Time: %s, Text: %s", sone, postId, postTime, postText)); - return null; - } - try { - PostBuilder postBuilder = core.postBuilder(); - /* TODO - parse time correctly. */ - postBuilder.withId(postId).from(sone.getId()).withTime(Long.parseLong(postTime)).withText(postText); - if ((postRecipientId != null) && (postRecipientId.length() == 43)) { - postBuilder.to(postRecipientId); - } - posts.add(postBuilder.build()); - } catch (NumberFormatException nfe1) { - /* TODO - mark Sone as bad. */ - logger.log(Level.WARNING, String.format("Downloaded post for Sone %s with invalid time: %s", sone, postTime)); - return null; - } - } - } - - /* parse replies. */ - SimpleXML repliesXml = soneXml.getNode("replies"); - Set replies = new HashSet(); - if (repliesXml == null) { - /* TODO - mark Sone as bad. */ - logger.log(Level.WARNING, String.format("Downloaded Sone %s has no replies!", sone)); - } else { - for (SimpleXML replyXml : repliesXml.getNodes("reply")) { - String replyId = replyXml.getValue("id", null); - String replyPostId = replyXml.getValue("post-id", null); - String replyTime = replyXml.getValue("time", null); - String replyText = replyXml.getValue("text", null); - if ((replyId == null) || (replyPostId == null) || (replyTime == null) || (replyText == null)) { - /* TODO - mark Sone as bad. */ - logger.log(Level.WARNING, String.format("Downloaded reply for Sone %s with missing data! ID: %s, Post: %s, Time: %s, Text: %s", sone, replyId, replyPostId, replyTime, replyText)); - return null; - } - try { - PostReplyBuilder postReplyBuilder = core.postReplyBuilder(); - /* TODO - parse time correctly. */ - postReplyBuilder.withId(replyId).from(sone.getId()).to(replyPostId).withTime(Long.parseLong(replyTime)).withText(replyText); - replies.add(postReplyBuilder.build()); - } catch (NumberFormatException nfe1) { - /* TODO - mark Sone as bad. */ - logger.log(Level.WARNING, String.format("Downloaded reply for Sone %s with invalid time: %s", sone, replyTime)); - return null; - } - } - } - - /* parse liked post IDs. */ - SimpleXML likePostIdsXml = soneXml.getNode("post-likes"); - Set likedPostIds = new HashSet(); - if (likePostIdsXml == null) { - /* TODO - mark Sone as bad. */ - logger.log(Level.WARNING, String.format("Downloaded Sone %s has no post likes!", sone)); - } else { - for (SimpleXML likedPostIdXml : likePostIdsXml.getNodes("post-like")) { - String postId = likedPostIdXml.getValue(); - likedPostIds.add(postId); - } - } - - /* parse liked reply IDs. */ - SimpleXML likeReplyIdsXml = soneXml.getNode("reply-likes"); - Set likedReplyIds = new HashSet(); - if (likeReplyIdsXml == null) { - /* TODO - mark Sone as bad. */ - logger.log(Level.WARNING, String.format("Downloaded Sone %s has no reply likes!", sone)); - } else { - for (SimpleXML likedReplyIdXml : likeReplyIdsXml.getNodes("reply-like")) { - String replyId = likedReplyIdXml.getValue(); - likedReplyIds.add(replyId); - } - } - - /* parse albums. */ - SimpleXML albumsXml = soneXml.getNode("albums"); - List topLevelAlbums = new ArrayList(); - if (albumsXml != null) { - for (SimpleXML albumXml : albumsXml.getNodes("album")) { - String id = albumXml.getValue("id", null); - String parentId = albumXml.getValue("parent", null); - String title = albumXml.getValue("title", null); - String description = albumXml.getValue("description", ""); - String albumImageId = albumXml.getValue("album-image", null); - if ((id == null) || (title == null) || (description == null)) { - logger.log(Level.WARNING, String.format("Downloaded Sone %s contains invalid album!", sone)); - return null; - } - Album parent = null; - if (parentId != null) { - parent = core.getAlbum(parentId, false); - if (parent == null) { - logger.log(Level.WARNING, String.format("Downloaded Sone %s has album with invalid parent!", sone)); - return null; - } - } - Album album = core.getAlbum(id).setSone(sone).modify().setTitle(title).setDescription(description).update(); - if (parent != null) { - parent.addAlbum(album); - } else { - topLevelAlbums.add(album); - } - SimpleXML imagesXml = albumXml.getNode("images"); - if (imagesXml != null) { - for (SimpleXML imageXml : imagesXml.getNodes("image")) { - String imageId = imageXml.getValue("id", null); - String imageCreationTimeString = imageXml.getValue("creation-time", null); - String imageKey = imageXml.getValue("key", null); - String imageTitle = imageXml.getValue("title", null); - String imageDescription = imageXml.getValue("description", ""); - String imageWidthString = imageXml.getValue("width", null); - String imageHeightString = imageXml.getValue("height", null); - if ((imageId == null) || (imageCreationTimeString == null) || (imageKey == null) || (imageTitle == null) || (imageWidthString == null) || (imageHeightString == null)) { - logger.log(Level.WARNING, String.format("Downloaded Sone %s contains invalid images!", sone)); - return null; - } - long creationTime = Numbers.safeParseLong(imageCreationTimeString, 0L); - int imageWidth = Numbers.safeParseInteger(imageWidthString, 0); - int imageHeight = Numbers.safeParseInteger(imageHeightString, 0); - if ((imageWidth < 1) || (imageHeight < 1)) { - logger.log(Level.WARNING, String.format("Downloaded Sone %s contains image %s with invalid dimensions (%s, %s)!", sone, imageId, imageWidthString, imageHeightString)); - return null; - } - Image image = core.getImage(imageId).modify().setSone(sone).setKey(imageKey).setCreationTime(creationTime).update(); - image = image.modify().setTitle(imageTitle).setDescription(imageDescription).update(); - image = image.modify().setWidth(imageWidth).setHeight(imageHeight).update(); - album.addImage(image); - } - } - album.modify().setAlbumImage(albumImageId).update(); - } - } - - /* process avatar. */ - if (avatarId != null) { - profile.setAvatar(core.getImage(avatarId, false)); - } - - /* okay, apparently everything was parsed correctly. Now import. */ - /* atomic setter operation on the Sone. */ - synchronized (sone) { - sone.setProfile(profile); - sone.setPosts(posts); - sone.setReplies(replies); - sone.setLikePostIds(likedPostIds); - sone.setLikeReplyIds(likedReplyIds); - for (Album album : topLevelAlbums) { - sone.getRootAlbum().addAlbum(album); - } - } - - return sone; - } +public interface SoneDownloader extends Service { - // - // SERVICE METHODS - // + void addSone(Sone sone); + void fetchSone(Sone sone, FreenetURI soneUri); + Sone fetchSone(Sone sone, FreenetURI soneUri, boolean fetchOnly); - /** - * {@inheritDoc} - */ - @Override - protected void serviceStop() { - for (Sone sone : sones) { - freenetInterface.unregisterUsk(sone); - } - } + Runnable fetchSoneWithUriAction(Sone sone); + Runnable fetchSoneAction(Sone sone); } diff --git a/src/main/java/net/pterodactylus/sone/core/SoneDownloaderImpl.java b/src/main/java/net/pterodactylus/sone/core/SoneDownloaderImpl.java new file mode 100644 index 0000000..b36c2d3 --- /dev/null +++ b/src/main/java/net/pterodactylus/sone/core/SoneDownloaderImpl.java @@ -0,0 +1,274 @@ +/* + * Sone - SoneDownloader.java - Copyright © 2010–2013 David Roden + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.pterodactylus.sone.core; + +import static freenet.support.io.Closer.close; +import static java.lang.String.format; +import static java.lang.System.currentTimeMillis; +import static java.util.concurrent.TimeUnit.DAYS; +import static java.util.logging.Logger.getLogger; + +import java.io.InputStream; +import java.util.HashSet; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; + +import net.pterodactylus.sone.core.FreenetInterface.Fetched; +import net.pterodactylus.sone.data.Sone; +import net.pterodactylus.sone.data.Sone.SoneStatus; +import net.pterodactylus.util.service.AbstractService; + +import freenet.client.FetchResult; +import freenet.client.async.ClientContext; +import freenet.client.async.USKCallback; +import freenet.keys.FreenetURI; +import freenet.keys.USK; +import freenet.node.RequestStarter; +import freenet.support.api.Bucket; +import freenet.support.io.Closer; +import com.db4o.ObjectContainer; + +import com.google.common.annotations.VisibleForTesting; + +/** + * The Sone downloader is responsible for download Sones as they are updated. + * + * @author David ‘Bombe’ Roden + */ +public class SoneDownloaderImpl extends AbstractService implements SoneDownloader { + + /** The logger. */ + private static final Logger logger = getLogger("Sone.Downloader"); + + /** The maximum protocol version. */ + private static final int MAX_PROTOCOL_VERSION = 0; + + /** The core. */ + private final Core core; + private final SoneParser soneParser; + + /** The Freenet interface. */ + private final FreenetInterface freenetInterface; + + /** The sones to update. */ + private final Set sones = new HashSet(); + + /** + * Creates a new Sone downloader. + * + * @param core + * The core + * @param freenetInterface + * The Freenet interface + */ + public SoneDownloaderImpl(Core core, FreenetInterface freenetInterface) { + this(core, freenetInterface, new SoneParser(core)); + } + + /** + * Creates a new Sone downloader. + * + * @param core + * The core + * @param freenetInterface + * The Freenet interface + * @param soneParser + */ + @VisibleForTesting + SoneDownloaderImpl(Core core, FreenetInterface freenetInterface, SoneParser soneParser) { + super("Sone Downloader", false); + this.core = core; + this.freenetInterface = freenetInterface; + this.soneParser = soneParser; + } + + // + // ACTIONS + // + + /** + * Adds the given Sone to the set of Sones that will be watched for updates. + * + * @param sone + * The Sone to add + */ + @Override + public void addSone(final Sone sone) { + if (!sones.add(sone)) { + freenetInterface.unregisterUsk(sone); + } + final USKCallback uskCallback = new USKCallback() { + + @Override + @SuppressWarnings("synthetic-access") + public void onFoundEdition(long edition, USK key, + ClientContext clientContext, boolean metadata, + short codec, byte[] data, boolean newKnownGood, + boolean newSlotToo) { + logger.log(Level.FINE, format( + "Found USK update for Sone “%s” at %s, new known good: %s, new slot too: %s.", + sone, key, newKnownGood, newSlotToo)); + if (edition > sone.getLatestEdition()) { + sone.setLatestEdition(edition); + new Thread(fetchSoneAction(sone), + "Sone Downloader").start(); + } + } + + @Override + public short getPollingPriorityProgress() { + return RequestStarter.INTERACTIVE_PRIORITY_CLASS; + } + + @Override + public short getPollingPriorityNormal() { + return RequestStarter.INTERACTIVE_PRIORITY_CLASS; + } + }; + if (soneHasBeenActiveRecently(sone)) { + freenetInterface.registerActiveUsk(sone.getRequestUri(), + uskCallback); + } else { + freenetInterface.registerPassiveUsk(sone.getRequestUri(), + uskCallback); + } + } + + private boolean soneHasBeenActiveRecently(Sone sone) { + return (currentTimeMillis() - sone.getTime()) < DAYS.toMillis(7); + } + + private void fetchSone(Sone sone) { + fetchSone(sone, sone.getRequestUri().sskForUSK()); + } + + /** + * Fetches the updated Sone. This method can be used to fetch a Sone from a + * specific URI. + * + * @param sone + * The Sone to fetch + * @param soneUri + * The URI to fetch the Sone from + */ + @Override + public void fetchSone(Sone sone, FreenetURI soneUri) { + fetchSone(sone, soneUri, false); + } + + /** + * Fetches the Sone from the given URI. + * + * @param sone + * The Sone to fetch + * @param soneUri + * The URI of the Sone to fetch + * @param fetchOnly + * {@code true} to only fetch and parse the Sone, {@code false} + * to {@link Core#updateSone(Sone) update} it in the core + * @return The downloaded Sone, or {@code null} if the Sone could not be + * downloaded + */ + @Override + public Sone fetchSone(Sone sone, FreenetURI soneUri, boolean fetchOnly) { + logger.log(Level.FINE, String.format("Starting fetch for Sone “%s” from %s…", sone, soneUri)); + FreenetURI requestUri = soneUri.setMetaString(new String[] { "sone.xml" }); + sone.setStatus(SoneStatus.downloading); + try { + Fetched fetchResults = freenetInterface.fetchUri(requestUri); + if (fetchResults == null) { + /* TODO - mark Sone as bad. */ + return null; + } + logger.log(Level.FINEST, String.format("Got %d bytes back.", fetchResults.getFetchResult().size())); + Sone parsedSone = parseSone(sone, fetchResults.getFetchResult(), fetchResults.getFreenetUri()); + if (parsedSone != null) { + if (!fetchOnly) { + parsedSone.setStatus((parsedSone.getTime() == 0) ? SoneStatus.unknown : SoneStatus.idle); + core.updateSone(parsedSone); + addSone(parsedSone); + } + } + return parsedSone; + } finally { + sone.setStatus((sone.getTime() == 0) ? SoneStatus.unknown : SoneStatus.idle); + } + } + + /** + * Parses a Sone from a fetch result. + * + * @param originalSone + * The sone to parse, or {@code null} if the Sone is yet unknown + * @param fetchResult + * The fetch result + * @param requestUri + * The requested URI + * @return The parsed Sone, or {@code null} if the Sone could not be parsed + */ + private Sone parseSone(Sone originalSone, FetchResult fetchResult, FreenetURI requestUri) { + logger.log(Level.FINEST, String.format("Parsing FetchResult (%d bytes, %s) for %s…", fetchResult.size(), fetchResult.getMimeType(), originalSone)); + Bucket soneBucket = fetchResult.asBucket(); + InputStream soneInputStream = null; + try { + soneInputStream = soneBucket.getInputStream(); + Sone parsedSone = soneParser.parseSone(originalSone, + soneInputStream); + if (parsedSone != null) { + parsedSone.setLatestEdition(requestUri.getEdition()); + } + return parsedSone; + } catch (Exception e1) { + logger.log(Level.WARNING, String.format("Could not parse Sone from %s!", requestUri), e1); + } finally { + close(soneInputStream); + close(soneBucket); + } + return null; + } + + @Override + public Runnable fetchSoneWithUriAction(final Sone sone) { + return new Runnable() { + @Override + public void run() { + fetchSone(sone, sone.getRequestUri()); + } + }; + } + + @Override + public Runnable fetchSoneAction(final Sone sone) { + return new Runnable() { + @Override + public void run() { + fetchSone(sone); + } + }; + } + + /** {@inheritDoc} */ + @Override + protected void serviceStop() { + for (Sone sone : sones) { + freenetInterface.unregisterUsk(sone); + } + } + +} diff --git a/src/main/java/net/pterodactylus/sone/core/SoneException.java b/src/main/java/net/pterodactylus/sone/core/SoneException.java index f1af354..2b1f1de 100644 --- a/src/main/java/net/pterodactylus/sone/core/SoneException.java +++ b/src/main/java/net/pterodactylus/sone/core/SoneException.java @@ -26,23 +26,6 @@ public class SoneException extends Exception { /** * Creates a new Sone exception. - */ - public SoneException() { - super(); - } - - /** - * Creates a new Sone exception. - * - * @param message - * The message of the exception - */ - public SoneException(String message) { - super(message); - } - - /** - * Creates a new Sone exception. * * @param cause * The cause of the exception diff --git a/src/main/java/net/pterodactylus/sone/core/SoneInsertException.java b/src/main/java/net/pterodactylus/sone/core/SoneInsertException.java index 5c3f7cd..f67f093 100644 --- a/src/main/java/net/pterodactylus/sone/core/SoneInsertException.java +++ b/src/main/java/net/pterodactylus/sone/core/SoneInsertException.java @@ -26,33 +26,6 @@ public class SoneInsertException extends SoneException { /** * Creates a new Sone insert exception. - */ - public SoneInsertException() { - super(); - } - - /** - * Creates a new Sone insert exception. - * - * @param message - * The message of the exception - */ - public SoneInsertException(String message) { - super(message); - } - - /** - * Creates a new Sone insert exception. - * - * @param cause - * The cause of the exception - */ - public SoneInsertException(Throwable cause) { - super(cause); - } - - /** - * Creates a new Sone insert exception. * * @param message * The message of the exception diff --git a/src/main/java/net/pterodactylus/sone/core/SoneInserter.java b/src/main/java/net/pterodactylus/sone/core/SoneInserter.java index a1bb903..86bf049 100644 --- a/src/main/java/net/pterodactylus/sone/core/SoneInserter.java +++ b/src/main/java/net/pterodactylus/sone/core/SoneInserter.java @@ -17,18 +17,26 @@ package net.pterodactylus.sone.core; -import static com.google.common.base.Preconditions.checkArgument; +import static java.lang.String.format; +import static java.lang.System.currentTimeMillis; +import static java.util.logging.Logger.getLogger; import static net.pterodactylus.sone.data.Album.NOT_EMPTY; +import java.io.Closeable; +import java.io.InputStream; import java.io.InputStreamReader; import java.io.StringWriter; import java.nio.charset.Charset; import java.util.HashMap; import java.util.HashSet; import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; import java.util.logging.Level; import java.util.logging.Logger; +import net.pterodactylus.sone.core.SoneModificationDetector.LockableFingerprintProvider; +import net.pterodactylus.sone.core.event.InsertionDelayChangedEvent; import net.pterodactylus.sone.core.event.SoneInsertAbortedEvent; import net.pterodactylus.sone.core.event.SoneInsertedEvent; import net.pterodactylus.sone.core.event.SoneInsertingEvent; @@ -37,10 +45,8 @@ import net.pterodactylus.sone.data.Post; import net.pterodactylus.sone.data.Reply; import net.pterodactylus.sone.data.Sone; import net.pterodactylus.sone.data.Sone.SoneStatus; -import net.pterodactylus.sone.freenet.StringBucket; import net.pterodactylus.sone.main.SonePlugin; import net.pterodactylus.util.io.Closer; -import net.pterodactylus.util.logging.Logging; import net.pterodactylus.util.service.AbstractService; import net.pterodactylus.util.template.HtmlFilter; import net.pterodactylus.util.template.ReflectionAccessor; @@ -51,12 +57,19 @@ import net.pterodactylus.util.template.TemplateException; import net.pterodactylus.util.template.TemplateParser; import net.pterodactylus.util.template.XmlFilter; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Charsets; +import com.google.common.base.Optional; import com.google.common.collect.FluentIterable; import com.google.common.collect.Ordering; import com.google.common.eventbus.EventBus; +import com.google.common.eventbus.Subscribe; -import freenet.client.async.ManifestElement; import freenet.keys.FreenetURI; +import freenet.support.api.Bucket; +import freenet.support.api.ManifestElement; +import freenet.support.api.RandomAccessBucket; +import freenet.support.io.ArrayBucket; /** * A Sone inserter is responsible for inserting a Sone if it has changed. @@ -66,10 +79,10 @@ import freenet.keys.FreenetURI; public class SoneInserter extends AbstractService { /** The logger. */ - private static final Logger logger = Logging.getLogger(SoneInserter.class); + private static final Logger logger = getLogger("Sone.Inserter"); /** The insertion delay (in seconds). */ - private static volatile int insertionDelay = 60; + private static final AtomicInteger insertionDelay = new AtomicInteger(60); /** The template factory used to create the templates. */ private static final TemplateContextFactory templateContextFactory = new TemplateContextFactory(); @@ -92,14 +105,9 @@ public class SoneInserter extends AbstractService { /** The Freenet interface. */ private final FreenetInterface freenetInterface; - /** The Sone to insert. */ - private volatile Sone sone; - - /** Whether a modification has been detected. */ - private volatile boolean modified = false; - - /** The fingerprint of the last insert. */ - private volatile String lastInsertFingerprint; + private final SoneModificationDetector soneModificationDetector; + private final long delay; + private final String soneId; /** * Creates a new Sone inserter. @@ -110,32 +118,49 @@ public class SoneInserter extends AbstractService { * The event bus * @param freenetInterface * The freenet interface - * @param sone - * The Sone to insert + * @param soneId + * The ID of the Sone to insert */ - public SoneInserter(Core core, EventBus eventBus, FreenetInterface freenetInterface, Sone sone) { - super("Sone Inserter for “" + sone.getName() + "”", false); + public SoneInserter(final Core core, EventBus eventBus, FreenetInterface freenetInterface, final String soneId) { + this(core, eventBus, freenetInterface, soneId, new SoneModificationDetector(new LockableFingerprintProvider() { + @Override + public boolean isLocked() { + final Optional sone = core.getSone(soneId); + if (!sone.isPresent()) { + return false; + } + return core.isLocked(sone.get()); + } + + @Override + public String getFingerprint() { + final Optional sone = core.getSone(soneId); + if (!sone.isPresent()) { + return null; + } + return sone.get().getFingerprint(); + } + }, insertionDelay), 1000); + } + + @VisibleForTesting + SoneInserter(Core core, EventBus eventBus, FreenetInterface freenetInterface, String soneId, SoneModificationDetector soneModificationDetector, long delay) { + super("Sone Inserter for “" + soneId + "”", false); this.core = core; this.eventBus = eventBus; this.freenetInterface = freenetInterface; - this.sone = sone; + this.soneId = soneId; + this.soneModificationDetector = soneModificationDetector; + this.delay = delay; } // // ACCESSORS // - /** - * Sets the Sone to insert. - * - * @param sone - * The Sone to insert - * @return This Sone inserter - */ - public SoneInserter setSone(Sone sone) { - checkArgument((this.sone == null) || sone.equals(this.sone), "Sone to insert can not be set to a different Sone"); - this.sone = sone; - return this; + @VisibleForTesting + static AtomicInteger getInsertionDelay() { + return insertionDelay; } /** @@ -145,8 +170,8 @@ public class SoneInserter extends AbstractService { * @param insertionDelay * The insertion delay (in seconds) */ - public static void setInsertionDelay(int insertionDelay) { - SoneInserter.insertionDelay = insertionDelay; + private static void setInsertionDelay(int insertionDelay) { + SoneInserter.insertionDelay.set(insertionDelay); } /** @@ -155,7 +180,7 @@ public class SoneInserter extends AbstractService { * @return The fingerprint of the last insert */ public String getLastInsertFingerprint() { - return lastInsertFingerprint; + return soneModificationDetector.getOriginalFingerprint(); } /** @@ -165,7 +190,7 @@ public class SoneInserter extends AbstractService { * The fingerprint of the last insert */ public void setLastInsertFingerprint(String lastInsertFingerprint) { - this.lastInsertFingerprint = lastInsertFingerprint; + soneModificationDetector.setFingerprint(lastInsertFingerprint); } /** @@ -176,7 +201,7 @@ public class SoneInserter extends AbstractService { * otherwise */ public boolean isModified() { - return modified; + return soneModificationDetector.isModified(); } // @@ -188,59 +213,28 @@ public class SoneInserter extends AbstractService { */ @Override protected void serviceRun() { - long lastModificationTime = 0; - String lastInsertedFingerprint = lastInsertFingerprint; - String lastFingerprint = ""; - Sone sone; while (!shouldStop()) { try { - /* check every seconds. */ - sleep(1000); - - /* don’t insert locked Sones. */ - sone = this.sone; - if (core.isLocked(sone)) { - /* trigger redetection when the Sone is unlocked. */ - synchronized (sone) { - modified = !sone.getFingerprint().equals(lastInsertedFingerprint); + /* check every second. */ + sleep(delay); + + if (soneModificationDetector.isEligibleForInsert()) { + Optional soneOptional = core.getSone(soneId); + if (!soneOptional.isPresent()) { + logger.log(Level.WARNING, format("Sone %s has disappeared, exiting inserter.", soneId)); + return; } - lastFingerprint = ""; - lastModificationTime = 0; - continue; - } - - InsertInformation insertInformation = null; - synchronized (sone) { - String fingerprint = sone.getFingerprint(); - if (!fingerprint.equals(lastFingerprint)) { - if (fingerprint.equals(lastInsertedFingerprint)) { - modified = false; - lastModificationTime = 0; - logger.log(Level.FINE, String.format("Sone %s has been reverted to last insert state.", sone)); - } else { - lastModificationTime = System.currentTimeMillis(); - modified = true; - logger.log(Level.FINE, String.format("Sone %s has been modified, waiting %d seconds before inserting.", sone.getName(), insertionDelay)); - } - lastFingerprint = fingerprint; - } - if (modified && (lastModificationTime > 0) && ((System.currentTimeMillis() - lastModificationTime) > (insertionDelay * 1000))) { - lastInsertedFingerprint = fingerprint; - insertInformation = new InsertInformation(sone); - } - } - - if (insertInformation != null) { + Sone sone = soneOptional.get(); + InsertInformation insertInformation = new InsertInformation(sone); logger.log(Level.INFO, String.format("Inserting Sone “%s”…", sone.getName())); boolean success = false; try { sone.setStatus(SoneStatus.inserting); - long insertTime = System.currentTimeMillis(); - insertInformation.setTime(insertTime); + long insertTime = currentTimeMillis(); eventBus.post(new SoneInsertingEvent(sone)); - FreenetURI finalUri = freenetInterface.insertDirectory(insertInformation.getInsertUri(), insertInformation.generateManifestEntries(), "index.html"); - eventBus.post(new SoneInsertedEvent(sone, System.currentTimeMillis() - insertTime)); + FreenetURI finalUri = freenetInterface.insertDirectory(sone.getInsertUri(), insertInformation.generateManifestEntries(), "index.html"); + eventBus.post(new SoneInsertedEvent(sone, currentTimeMillis() - insertTime, insertInformation.getFingerprint())); /* at this point we might already be stopped. */ if (shouldStop()) { /* if so, bail out, don’t change anything. */ @@ -255,6 +249,7 @@ public class SoneInserter extends AbstractService { eventBus.post(new SoneInsertAbortedEvent(sone, se1)); logger.log(Level.WARNING, String.format("Could not insert Sone “%s”!", sone.getName()), se1); } finally { + insertInformation.close(); sone.setStatus(SoneStatus.idle); } @@ -264,12 +259,10 @@ public class SoneInserter extends AbstractService { */ if (success) { synchronized (sone) { - if (lastInsertedFingerprint.equals(sone.getFingerprint())) { + if (insertInformation.getFingerprint().equals(sone.getFingerprint())) { logger.log(Level.FINE, String.format("Sone “%s” was not modified further, resetting counter…", sone)); - lastModificationTime = 0; - lastInsertFingerprint = lastInsertedFingerprint; + soneModificationDetector.setFingerprint(insertInformation.getFingerprint()); core.touchConfiguration(); - modified = false; } } } @@ -280,6 +273,11 @@ public class SoneInserter extends AbstractService { } } + @Subscribe + public void insertionDelayChanged(InsertionDelayChangedEvent insertionDelayChangedEvent) { + setInsertionDelay(insertionDelayChangedEvent.getInsertionDelay()); + } + /** * Container for information that are required to insert a Sone. This * container merely exists to copy all relevant data without holding a lock @@ -287,10 +285,13 @@ public class SoneInserter extends AbstractService { * * @author David ‘Bombe’ Roden */ - private class InsertInformation { + @VisibleForTesting + class InsertInformation implements Closeable { /** All properties of the Sone, copied for thread safety. */ private final Map soneProperties = new HashMap(); + private final String fingerprint; + private final ManifestCreator manifestCreator; /** * Creates a new insert information container. @@ -299,40 +300,28 @@ public class SoneInserter extends AbstractService { * The sone to insert */ public InsertInformation(Sone sone) { + this.fingerprint = sone.getFingerprint(); + Map soneProperties = new HashMap(); soneProperties.put("id", sone.getId()); soneProperties.put("name", sone.getName()); - soneProperties.put("time", sone.getTime()); + soneProperties.put("time", currentTimeMillis()); soneProperties.put("requestUri", sone.getRequestUri()); - soneProperties.put("insertUri", sone.getInsertUri()); soneProperties.put("profile", sone.getProfile()); soneProperties.put("posts", Ordering.from(Post.TIME_COMPARATOR).sortedCopy(sone.getPosts())); soneProperties.put("replies", Ordering.from(Reply.TIME_COMPARATOR).reverse().sortedCopy(sone.getReplies())); soneProperties.put("likedPostIds", new HashSet(sone.getLikedPostIds())); soneProperties.put("likedReplyIds", new HashSet(sone.getLikedReplyIds())); soneProperties.put("albums", FluentIterable.from(sone.getRootAlbum().getAlbums()).transformAndConcat(Album.FLATTENER).filter(NOT_EMPTY).toList()); + manifestCreator = new ManifestCreator(core, soneProperties); } // // ACCESSORS // - /** - * Returns the insert URI of the Sone. - * - * @return The insert URI of the Sone - */ - public FreenetURI getInsertUri() { - return (FreenetURI) soneProperties.get("insertUri"); - } - - /** - * Sets the time of the Sone at the time of the insert. - * - * @param time - * The time of the Sone - */ - public void setTime(long time) { - soneProperties.put("time", time); + @VisibleForTesting + String getFingerprint() { + return fingerprint; } // @@ -348,41 +337,56 @@ public class SoneInserter extends AbstractService { HashMap manifestEntries = new HashMap(); /* first, create an index.html. */ - manifestEntries.put("index.html", createManifestElement("index.html", "text/html; charset=utf-8", "/templates/insert/index.html")); + manifestEntries.put("index.html", manifestCreator.createManifestElement( + "index.html", "text/html; charset=utf-8", + "/templates/insert/index.html")); /* now, store the sone. */ - manifestEntries.put("sone.xml", createManifestElement("sone.xml", "text/xml; charset=utf-8", "/templates/insert/sone.xml")); + manifestEntries.put("sone.xml", manifestCreator.createManifestElement( + "sone.xml", "text/xml; charset=utf-8", + "/templates/insert/sone.xml")); return manifestEntries; } - // - // PRIVATE METHODS - // + @Override + public void close() { + manifestCreator.close(); + } - /** - * Creates a new manifest element. - * - * @param name - * The name of the file - * @param contentType - * The content type of the file - * @param templateName - * The name of the template to render - * @return The manifest element - */ - @SuppressWarnings("synthetic-access") - private ManifestElement createManifestElement(String name, String contentType, String templateName) { + } + + /** + * Creates manifest elements for an insert by rendering a template. + * + * @author David ‘Bombe’ Roden + */ + @VisibleForTesting + static class ManifestCreator implements Closeable { + + private final Core core; + private final Map soneProperties; + private final Set buckets = new HashSet(); + + ManifestCreator(Core core, Map soneProperties) { + this.core = core; + this.soneProperties = soneProperties; + } + + public ManifestElement createManifestElement(String name, String contentType, String templateName) { InputStreamReader templateInputStreamReader = null; + InputStream templateInputStream = null; Template template; try { - templateInputStreamReader = new InputStreamReader(getClass().getResourceAsStream(templateName), utf8Charset); + templateInputStream = getClass().getResourceAsStream(templateName); + templateInputStreamReader = new InputStreamReader(templateInputStream, utf8Charset); template = TemplateParser.parse(templateInputStreamReader); } catch (TemplateException te1) { logger.log(Level.SEVERE, String.format("Could not parse template “%s”!", templateName), te1); return null; } finally { Closer.close(templateInputStreamReader); + Closer.close(templateInputStream); } TemplateContext templateContext = templateContextFactory.createTemplateContext(); @@ -391,19 +395,22 @@ public class SoneInserter extends AbstractService { templateContext.set("currentEdition", core.getUpdateChecker().getLatestEdition()); templateContext.set("version", SonePlugin.VERSION); StringWriter writer = new StringWriter(); - StringBucket bucket = null; try { template.render(templateContext, writer); - bucket = new StringBucket(writer.toString(), utf8Charset); + RandomAccessBucket bucket = new ArrayBucket(writer.toString().getBytes(Charsets.UTF_8)); + buckets.add(bucket); return new ManifestElement(name, bucket, contentType, bucket.size()); } catch (TemplateException te1) { logger.log(Level.SEVERE, String.format("Could not render template “%s”!", templateName), te1); return null; } finally { Closer.close(writer); - if (bucket != null) { - bucket.free(); - } + } + } + + public void close() { + for (Bucket bucket : buckets) { + bucket.free(); } } diff --git a/src/main/java/net/pterodactylus/sone/core/SoneModificationDetector.java b/src/main/java/net/pterodactylus/sone/core/SoneModificationDetector.java new file mode 100644 index 0000000..290fcbe --- /dev/null +++ b/src/main/java/net/pterodactylus/sone/core/SoneModificationDetector.java @@ -0,0 +1,95 @@ +package net.pterodactylus.sone.core; + +import static com.google.common.base.Optional.absent; +import static com.google.common.base.Optional.of; +import static com.google.common.base.Ticker.systemTicker; +import static java.util.concurrent.TimeUnit.NANOSECONDS; + +import java.util.concurrent.atomic.AtomicInteger; + +import net.pterodactylus.sone.data.Sone; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Optional; +import com.google.common.base.Ticker; + +/** + * Class that detects {@link Sone} modifications (as per their {@link + * Sone#getFingerprint() fingerprints} and determines when a modified Sone may + * be inserted. + * + * @author David ‘Bombe’ Roden + */ +class SoneModificationDetector { + + private final Ticker ticker; + private final LockableFingerprintProvider lockableFingerprintProvider; + private final AtomicInteger insertionDelay; + private Optional lastModificationTime; + private String originalFingerprint; + private String lastFingerprint; + + SoneModificationDetector(LockableFingerprintProvider lockableFingerprintProvider, AtomicInteger insertionDelay) { + this(systemTicker(), lockableFingerprintProvider, insertionDelay); + } + + @VisibleForTesting + SoneModificationDetector(Ticker ticker, LockableFingerprintProvider lockableFingerprintProvider, AtomicInteger insertionDelay) { + this.ticker = ticker; + this.lockableFingerprintProvider = lockableFingerprintProvider; + this.insertionDelay = insertionDelay; + lastFingerprint = originalFingerprint; + } + + public boolean isEligibleForInsert() { + if (lockableFingerprintProvider.isLocked()) { + lastModificationTime = absent(); + lastFingerprint = ""; + return false; + } + String fingerprint = lockableFingerprintProvider.getFingerprint(); + if (originalFingerprint.equals(fingerprint)) { + lastModificationTime = absent(); + lastFingerprint = fingerprint; + return false; + } + if (!lastFingerprint.equals(fingerprint)) { + lastModificationTime = of(ticker.read()); + lastFingerprint = fingerprint; + return false; + } + return insertionDelayHasPassed(); + } + + public String getOriginalFingerprint() { + return originalFingerprint; + } + + public void setFingerprint(String fingerprint) { + originalFingerprint = fingerprint; + lastFingerprint = originalFingerprint; + lastModificationTime = absent(); + } + + private boolean insertionDelayHasPassed() { + return NANOSECONDS.toSeconds(ticker.read() - lastModificationTime.get()) >= insertionDelay.get(); + } + + public boolean isModified() { + return !lockableFingerprintProvider.getFingerprint().equals(originalFingerprint); + } + + /** + * Provider for a fingerprint and the information if a {@link Sone} is locked. This + * prevents us from having to lug a Sone object around. + * + * @author David ‘Bombe’ Roden + */ + static interface LockableFingerprintProvider { + + boolean isLocked(); + String getFingerprint(); + + } + +} diff --git a/src/main/java/net/pterodactylus/sone/core/SoneParser.java b/src/main/java/net/pterodactylus/sone/core/SoneParser.java new file mode 100644 index 0000000..ae600a6 --- /dev/null +++ b/src/main/java/net/pterodactylus/sone/core/SoneParser.java @@ -0,0 +1,344 @@ +package net.pterodactylus.sone.core; + +import static java.util.logging.Logger.getLogger; +import static net.pterodactylus.sone.utils.NumberParsers.parseInt; +import static net.pterodactylus.sone.utils.NumberParsers.parseLong; + +import java.io.InputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; + +import net.pterodactylus.sone.data.Album; +import net.pterodactylus.sone.data.Client; +import net.pterodactylus.sone.data.Image; +import net.pterodactylus.sone.data.Post; +import net.pterodactylus.sone.data.PostReply; +import net.pterodactylus.sone.data.Profile; +import net.pterodactylus.sone.data.Profile.DuplicateField; +import net.pterodactylus.sone.data.Profile.EmptyFieldName; +import net.pterodactylus.sone.data.Sone; +import net.pterodactylus.sone.database.PostBuilder; +import net.pterodactylus.sone.database.PostReplyBuilder; +import net.pterodactylus.sone.database.SoneBuilder; +import net.pterodactylus.util.xml.SimpleXML; +import net.pterodactylus.util.xml.XML; + +import org.w3c.dom.Document; + +/** + * Parses a {@link Sone} from an XML {@link InputStream}. + * + * @author David ‘Bombe’ Roden + */ +public class SoneParser { + + private static final Logger logger = getLogger("Sone.Parser"); + private static final int MAX_PROTOCOL_VERSION = 0; + private final Core core; + + public SoneParser(Core core) { + this.core = core; + } + + public Sone parseSone(Sone originalSone, InputStream soneInputStream) throws SoneException { + /* TODO - impose a size limit? */ + + Document document; + /* XML parsing is not thread-safe. */ + synchronized (this) { + document = XML.transformToDocument(soneInputStream); + } + if (document == null) { + /* TODO - mark Sone as bad. */ + logger.log(Level.WARNING, String.format("Could not parse XML for Sone %s!", originalSone)); + return null; + } + + SoneBuilder soneBuilder = core.soneBuilder().from(originalSone.getIdentity()); + if (originalSone.isLocal()) { + soneBuilder = soneBuilder.local(); + } + Sone sone = soneBuilder.build(); + + SimpleXML soneXml; + try { + soneXml = SimpleXML.fromDocument(document); + } catch (NullPointerException npe1) { + /* for some reason, invalid XML can cause NPEs. */ + logger.log(Level.WARNING, String.format("XML for Sone %s can not be parsed!", sone), npe1); + return null; + } + + Integer protocolVersion = null; + String soneProtocolVersion = soneXml.getValue("protocol-version", null); + if (soneProtocolVersion != null) { + protocolVersion = parseInt(soneProtocolVersion, null); + } + if (protocolVersion == null) { + logger.log(Level.INFO, "No protocol version found, assuming 0."); + protocolVersion = 0; + } + + if (protocolVersion < 0) { + logger.log(Level.WARNING, String.format("Invalid protocol version: %d! Not parsing Sone.", protocolVersion)); + return null; + } + + /* check for valid versions. */ + if (protocolVersion > MAX_PROTOCOL_VERSION) { + logger.log(Level.WARNING, String.format("Unknown protocol version: %d! Not parsing Sone.", protocolVersion)); + return null; + } + + String soneTime = soneXml.getValue("time", null); + if (soneTime == null) { + /* TODO - mark Sone as bad. */ + logger.log(Level.WARNING, String.format("Downloaded time for Sone %s was null!", sone)); + return null; + } + try { + sone.setTime(Long.parseLong(soneTime)); + } catch (NumberFormatException nfe1) { + /* TODO - mark Sone as bad. */ + logger.log(Level.WARNING, String.format("Downloaded Sone %s with invalid time: %s", sone, soneTime)); + return null; + } + + SimpleXML clientXml = soneXml.getNode("client"); + if (clientXml != null) { + String clientName = clientXml.getValue("name", null); + String clientVersion = clientXml.getValue("version", null); + if ((clientName == null) || (clientVersion == null)) { + logger.log(Level.WARNING, String.format("Download Sone %s with client XML but missing name or version!", sone)); + return null; + } + sone.setClient(new Client(clientName, clientVersion)); + } + + SimpleXML profileXml = soneXml.getNode("profile"); + if (profileXml == null) { + /* TODO - mark Sone as bad. */ + logger.log(Level.WARNING, String.format("Downloaded Sone %s has no profile!", sone)); + return null; + } + + /* parse profile. */ + String profileFirstName = profileXml.getValue("first-name", null); + String profileMiddleName = profileXml.getValue("middle-name", null); + String profileLastName = profileXml.getValue("last-name", null); + Integer profileBirthDay = parseInt(profileXml.getValue("birth-day", ""), null); + Integer profileBirthMonth = parseInt(profileXml.getValue("birth-month", ""), null); + Integer profileBirthYear = parseInt(profileXml.getValue("birth-year", ""), null); + Profile profile = new Profile(sone).setFirstName(profileFirstName).setMiddleName(profileMiddleName).setLastName(profileLastName); + profile.setBirthDay(profileBirthDay).setBirthMonth(profileBirthMonth).setBirthYear(profileBirthYear); + /* avatar is processed after images are loaded. */ + String avatarId = profileXml.getValue("avatar", null); + + /* parse profile fields. */ + SimpleXML profileFieldsXml = profileXml.getNode("fields"); + if (profileFieldsXml != null) { + for (SimpleXML fieldXml : profileFieldsXml.getNodes("field")) { + String fieldName = fieldXml.getValue("field-name", null); + String fieldValue = fieldXml.getValue("field-value", ""); + if (fieldName == null) { + logger.log(Level.WARNING, String.format("Downloaded profile field for Sone %s with missing data! Name: %s, Value: %s", sone, fieldName, fieldValue)); + return null; + } + try { + profile.addField(fieldName.trim()).setValue(fieldValue); + } catch (EmptyFieldName efn1) { + logger.log(Level.WARNING, "Empty field name!", efn1); + return null; + } catch (DuplicateField df1) { + logger.log(Level.WARNING, String.format("Duplicate field: %s", fieldName), df1); + return null; + } + } + } + + /* parse posts. */ + SimpleXML postsXml = soneXml.getNode("posts"); + Set posts = new HashSet(); + if (postsXml == null) { + /* TODO - mark Sone as bad. */ + logger.log(Level.WARNING, String.format("Downloaded Sone %s has no posts!", sone)); + } else { + for (SimpleXML postXml : postsXml.getNodes("post")) { + String postId = postXml.getValue("id", null); + String postRecipientId = postXml.getValue("recipient", null); + String postTime = postXml.getValue("time", null); + String postText = postXml.getValue("text", null); + if ((postId == null) || (postTime == null) || (postText == null)) { + /* TODO - mark Sone as bad. */ + logger.log(Level.WARNING, String.format("Downloaded post for Sone %s with missing data! ID: %s, Time: %s, Text: %s", sone, postId, postTime, postText)); + return null; + } + try { + PostBuilder postBuilder = core.postBuilder(); + /* TODO - parse time correctly. */ + postBuilder.withId(postId).from(sone.getId()).withTime(Long.parseLong(postTime)).withText(postText); + if ((postRecipientId != null) && (postRecipientId.length() == 43)) { + postBuilder.to(postRecipientId); + } + posts.add(postBuilder.build()); + } catch (NumberFormatException nfe1) { + /* TODO - mark Sone as bad. */ + logger.log(Level.WARNING, String.format("Downloaded post for Sone %s with invalid time: %s", sone, postTime)); + return null; + } + } + } + + /* parse replies. */ + SimpleXML repliesXml = soneXml.getNode("replies"); + Set replies = new HashSet(); + if (repliesXml == null) { + /* TODO - mark Sone as bad. */ + logger.log(Level.WARNING, String.format("Downloaded Sone %s has no replies!", sone)); + } else { + for (SimpleXML replyXml : repliesXml.getNodes("reply")) { + String replyId = replyXml.getValue("id", null); + String replyPostId = replyXml.getValue("post-id", null); + String replyTime = replyXml.getValue("time", null); + String replyText = replyXml.getValue("text", null); + if ((replyId == null) || (replyPostId == null) || (replyTime == null) || (replyText == null)) { + /* TODO - mark Sone as bad. */ + logger.log(Level.WARNING, String.format("Downloaded reply for Sone %s with missing data! ID: %s, Post: %s, Time: %s, Text: %s", sone, replyId, replyPostId, replyTime, replyText)); + return null; + } + try { + PostReplyBuilder postReplyBuilder = core.postReplyBuilder(); + /* TODO - parse time correctly. */ + postReplyBuilder.withId(replyId).from(sone.getId()).to(replyPostId).withTime(Long.parseLong(replyTime)).withText(replyText); + replies.add(postReplyBuilder.build()); + } catch (NumberFormatException nfe1) { + /* TODO - mark Sone as bad. */ + logger.log(Level.WARNING, String.format("Downloaded reply for Sone %s with invalid time: %s", sone, replyTime)); + return null; + } + } + } + + /* parse liked post IDs. */ + SimpleXML likePostIdsXml = soneXml.getNode("post-likes"); + Set likedPostIds = new HashSet(); + if (likePostIdsXml == null) { + /* TODO - mark Sone as bad. */ + logger.log(Level.WARNING, String.format("Downloaded Sone %s has no post likes!", sone)); + } else { + for (SimpleXML likedPostIdXml : likePostIdsXml.getNodes("post-like")) { + String postId = likedPostIdXml.getValue(); + likedPostIds.add(postId); + } + } + + /* parse liked reply IDs. */ + SimpleXML likeReplyIdsXml = soneXml.getNode("reply-likes"); + Set likedReplyIds = new HashSet(); + if (likeReplyIdsXml == null) { + /* TODO - mark Sone as bad. */ + logger.log(Level.WARNING, String.format("Downloaded Sone %s has no reply likes!", sone)); + } else { + for (SimpleXML likedReplyIdXml : likeReplyIdsXml.getNodes("reply-like")) { + String replyId = likedReplyIdXml.getValue(); + likedReplyIds.add(replyId); + } + } + + /* parse albums. */ + SimpleXML albumsXml = soneXml.getNode("albums"); + Map allImages = new HashMap(); + List topLevelAlbums = new ArrayList(); + if (albumsXml != null) { + for (SimpleXML albumXml : albumsXml.getNodes("album")) { + String id = albumXml.getValue("id", null); + String parentId = albumXml.getValue("parent", null); + String title = albumXml.getValue("title", null); + String description = albumXml.getValue("description", ""); + String albumImageId = albumXml.getValue("album-image", null); + if ((id == null) || (title == null)) { + logger.log(Level.WARNING, String.format("Downloaded Sone %s contains invalid album!", sone)); + return null; + } + Album parent = null; + if (parentId != null) { + parent = core.getAlbum(parentId); + if (parent == null) { + logger.log(Level.WARNING, String.format("Downloaded Sone %s has album with invalid parent!", sone)); + return null; + } + } + Album album = core.albumBuilder() + .withId(id) + .by(sone) + .build() + .modify() + .setTitle(title) + .setDescription(description) + .update(); + if (parent != null) { + parent.addAlbum(album); + } else { + topLevelAlbums.add(album); + } + SimpleXML imagesXml = albumXml.getNode("images"); + if (imagesXml != null) { + for (SimpleXML imageXml : imagesXml.getNodes("image")) { + String imageId = imageXml.getValue("id", null); + String imageCreationTimeString = imageXml.getValue("creation-time", null); + String imageKey = imageXml.getValue("key", null); + String imageTitle = imageXml.getValue("title", null); + String imageDescription = imageXml.getValue("description", ""); + String imageWidthString = imageXml.getValue("width", null); + String imageHeightString = imageXml.getValue("height", null); + if ((imageId == null) || (imageCreationTimeString == null) || (imageKey == null) || (imageTitle == null) || (imageWidthString == null) || (imageHeightString == null)) { + logger.log(Level.WARNING, String.format("Downloaded Sone %s contains invalid images!", sone)); + return null; + } + long creationTime = parseLong(imageCreationTimeString, 0L); + int imageWidth = parseInt(imageWidthString, 0); + int imageHeight = parseInt(imageHeightString, 0); + if ((imageWidth < 1) || (imageHeight < 1)) { + logger.log(Level.WARNING, String.format("Downloaded Sone %s contains image %s with invalid dimensions (%s, %s)!", sone, imageId, imageWidthString, imageHeightString)); + return null; + } + Image image = core.imageBuilder().withId(imageId).build().modify().setSone(sone).setKey(imageKey).setCreationTime(creationTime).update(); + image = image.modify().setTitle(imageTitle).setDescription(imageDescription).update(); + image = image.modify().setWidth(imageWidth).setHeight(imageHeight).update(); + album.addImage(image); + allImages.put(imageId, image); + } + } + album.modify().setAlbumImage(albumImageId).update(); + } + } + + /* process avatar. */ + if (avatarId != null) { + profile.setAvatar(allImages.get(avatarId)); + } + + /* okay, apparently everything was parsed correctly. Now import. */ + /* atomic setter operation on the Sone. */ + synchronized (sone) { + sone.setProfile(profile); + sone.setPosts(posts); + sone.setReplies(replies); + sone.setLikePostIds(likedPostIds); + sone.setLikeReplyIds(likedReplyIds); + for (Album album : topLevelAlbums) { + sone.getRootAlbum().addAlbum(album); + } + } + + return sone; + + } + +} diff --git a/src/main/java/net/pterodactylus/sone/core/SoneRescuer.java b/src/main/java/net/pterodactylus/sone/core/SoneRescuer.java index ed3d819..f45e351 100644 --- a/src/main/java/net/pterodactylus/sone/core/SoneRescuer.java +++ b/src/main/java/net/pterodactylus/sone/core/SoneRescuer.java @@ -74,6 +74,7 @@ public class SoneRescuer extends AbstractService { * * @return {@code true} if the Sone rescuer is currently fetching a Sone */ + @SuppressWarnings("unused") // used in rescue.html public boolean isFetching() { return fetching; } @@ -83,6 +84,7 @@ public class SoneRescuer extends AbstractService { * * @return The edition that is currently being downloaded */ + @SuppressWarnings("unused") // used in rescue.html public long getCurrentEdition() { return currentEdition; } @@ -102,6 +104,7 @@ public class SoneRescuer extends AbstractService { * * @return The next edition the Sone rescuer can download */ + @SuppressWarnings("unused") // used in rescue.html public long getNextEdition() { return currentEdition - 1; } @@ -124,6 +127,7 @@ public class SoneRescuer extends AbstractService { * @return {@code true} if the last fetch was successful, {@code false} * otherwise */ + @SuppressWarnings("unused") // used in rescue.html public boolean isLastFetchSuccessful() { return lastFetchSuccessful; } diff --git a/src/main/java/net/pterodactylus/sone/core/SoneUri.java b/src/main/java/net/pterodactylus/sone/core/SoneUri.java index 6c5bf16..ea84b39 100644 --- a/src/main/java/net/pterodactylus/sone/core/SoneUri.java +++ b/src/main/java/net/pterodactylus/sone/core/SoneUri.java @@ -17,11 +17,12 @@ package net.pterodactylus.sone.core; +import static java.util.logging.Logger.getLogger; + import java.net.MalformedURLException; import java.util.logging.Level; import java.util.logging.Logger; -import net.pterodactylus.util.logging.Logging; import freenet.keys.FreenetURI; /** @@ -33,7 +34,7 @@ import freenet.keys.FreenetURI; public class SoneUri { /** The logger. */ - private static final Logger logger = Logging.getLogger(SoneUri.class); + private static final Logger logger = getLogger("Sone.Data"); /** * Generate a Sone URI from the given URI. diff --git a/src/main/java/net/pterodactylus/sone/core/UpdateChecker.java b/src/main/java/net/pterodactylus/sone/core/UpdateChecker.java index 307025a..85c5841 100644 --- a/src/main/java/net/pterodactylus/sone/core/UpdateChecker.java +++ b/src/main/java/net/pterodactylus/sone/core/UpdateChecker.java @@ -17,6 +17,8 @@ package net.pterodactylus.sone.core; +import static java.util.logging.Logger.getLogger; + import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; @@ -30,7 +32,6 @@ import net.pterodactylus.sone.core.FreenetInterface.Fetched; import net.pterodactylus.sone.core.event.UpdateFoundEvent; import net.pterodactylus.sone.main.SonePlugin; import net.pterodactylus.util.io.Closer; -import net.pterodactylus.util.logging.Logging; import net.pterodactylus.util.version.Version; import com.google.common.eventbus.EventBus; @@ -47,13 +48,13 @@ import freenet.support.api.Bucket; public class UpdateChecker { /** The logger. */ - private static final Logger logger = Logging.getLogger(UpdateChecker.class); + private static final Logger logger = getLogger("Sone.UpdateChecker"); /** The key of the Sone homepage. */ private static final String SONE_HOMEPAGE = "USK@nwa8lHa271k2QvJ8aa0Ov7IHAV-DFOCFgmDt3X6BpCI,DuQSUZiI~agF8c-6tjsFFGuZ8eICrzWCILB60nT8KKo,AQACAAE/sone/"; /** The current latest known edition. */ - private static final int LATEST_EDITION = 62; + private static final int LATEST_EDITION = 65; /** The event bus. */ private final EventBus eventBus; diff --git a/src/main/java/net/pterodactylus/sone/core/WebOfTrustUpdater.java b/src/main/java/net/pterodactylus/sone/core/WebOfTrustUpdater.java index dd3de58..908bd96 100644 --- a/src/main/java/net/pterodactylus/sone/core/WebOfTrustUpdater.java +++ b/src/main/java/net/pterodactylus/sone/core/WebOfTrustUpdater.java @@ -1,621 +1,23 @@ -/* - * Sone - WebOfTrustUpdater.java - Copyright © 2013 David Roden - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - package net.pterodactylus.sone.core; -import static com.google.common.base.Preconditions.checkNotNull; - -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.logging.Level; -import java.util.logging.Logger; - -import net.pterodactylus.sone.freenet.plugin.PluginException; -import net.pterodactylus.sone.freenet.wot.DefaultIdentity; import net.pterodactylus.sone.freenet.wot.Identity; import net.pterodactylus.sone.freenet.wot.OwnIdentity; -import net.pterodactylus.sone.freenet.wot.Trust; -import net.pterodactylus.sone.freenet.wot.WebOfTrustConnector; -import net.pterodactylus.sone.freenet.wot.WebOfTrustException; -import net.pterodactylus.util.logging.Logging; -import net.pterodactylus.util.service.AbstractService; +import net.pterodactylus.util.service.Service; -import com.google.inject.Inject; +import com.google.inject.ImplementedBy; /** - * Updates WebOfTrust identity data in a background thread because communicating - * with the WebOfTrust plugin can potentially last quite long. + * Updates WebOfTrust identity data. * * @author David ‘Bombe’ Roden */ -public class WebOfTrustUpdater extends AbstractService { - - /** The logger. */ - private static final Logger logger = Logging.getLogger(WebOfTrustUpdater.class); - - /** Stop job. */ - @SuppressWarnings("synthetic-access") - private final WebOfTrustUpdateJob stopJob = new WebOfTrustUpdateJob(); - - /** The web of trust connector. */ - private final WebOfTrustConnector webOfTrustConnector; - - /** The queue for jobs. */ - private final BlockingQueue updateJobs = new LinkedBlockingQueue(); - - /** - * Creates a new trust updater. - * - * @param webOfTrustConnector - * The web of trust connector - */ - @Inject - public WebOfTrustUpdater(WebOfTrustConnector webOfTrustConnector) { - super("Trust Updater"); - this.webOfTrustConnector = webOfTrustConnector; - } - - // - // ACTIONS - // - - /** - * Updates the trust relation between the truster and the trustee. This method - * will return immediately and perform a trust update in the background. - * - * @param truster - * The identity giving the trust - * @param trustee - * The identity receiving the trust - * @param score - * The new level of trust (from -100 to 100, may be {@code null} to remove - * the trust completely) - * @param comment - * The comment of the trust relation - */ - public void setTrust(OwnIdentity truster, Identity trustee, Integer score, String comment) { - SetTrustJob setTrustJob = new SetTrustJob(truster, trustee, score, comment); - if (updateJobs.contains(setTrustJob)) { - updateJobs.remove(setTrustJob); - } - logger.log(Level.FINER, "Adding Trust Update Job: " + setTrustJob); - try { - updateJobs.put(setTrustJob); - } catch (InterruptedException e) { - /* the queue is unbounded so it should never block. */ - } - } - - /** - * Adds the given context to the given own identity. - * - * @param ownIdentity - * The own identity to add the context to - * @param context - * The context to add - */ - public void addContext(OwnIdentity ownIdentity, String context) { - addContextWait(ownIdentity, context, false); - } - - /** - * Adds the given context to the given own identity, waiting for completion of - * the operation. - * - * @param ownIdentity - * The own identity to add the context to - * @param context - * The context to add - * @return {@code true} if the context was added successfully, {@code false} - * otherwise - */ - public boolean addContextWait(OwnIdentity ownIdentity, String context) { - return addContextWait(ownIdentity, context, true); - } - - /** - * Adds the given context to the given own identity, waiting for completion of - * the operation. - * - * @param ownIdentity - * The own identity to add the context to - * @param context - * The context to add - * @param wait - * {@code true} to wait for the end of the operation, {@code false} to return - * immediately - * @return {@code true} if the context was added successfully, {@code false} if - * the context was not added successfully, or if the job should not - * wait for completion - */ - private boolean addContextWait(OwnIdentity ownIdentity, String context, boolean wait) { - AddContextJob addContextJob = new AddContextJob(ownIdentity, context); - if (!updateJobs.contains(addContextJob)) { - logger.log(Level.FINER, "Adding Context Job: " + addContextJob); - try { - updateJobs.put(addContextJob); - } catch (InterruptedException ie1) { - /* the queue is unbounded so it should never block. */ - } - if (wait) { - return addContextJob.waitForCompletion(); - } - } else if (wait) { - for (WebOfTrustUpdateJob updateJob : updateJobs) { - if (updateJob.equals(addContextJob)) { - return updateJob.waitForCompletion(); - } - } - } - return false; - } - - /** - * Removes the given context from the given own identity. - * - * @param ownIdentity - * The own identity to remove the context from - * @param context - * The context to remove - */ - public void removeContext(OwnIdentity ownIdentity, String context) { - RemoveContextJob removeContextJob = new RemoveContextJob(ownIdentity, context); - if (!updateJobs.contains(removeContextJob)) { - logger.log(Level.FINER, "Adding Context Job: " + removeContextJob); - try { - updateJobs.put(removeContextJob); - } catch (InterruptedException ie1) { - /* the queue is unbounded so it should never block. */ - } - } - } - - /** - * Sets a property on the given own identity. - * - * @param ownIdentity - * The own identity to set the property on - * @param propertyName - * The name of the property to set - * @param propertyValue - * The value of the property to set - */ - public void setProperty(OwnIdentity ownIdentity, String propertyName, String propertyValue) { - SetPropertyJob setPropertyJob = new SetPropertyJob(ownIdentity, propertyName, propertyValue); - if (updateJobs.contains(setPropertyJob)) { - updateJobs.remove(setPropertyJob); - } - logger.log(Level.FINER, "Adding Property Job: " + setPropertyJob); - try { - updateJobs.put(setPropertyJob); - } catch (InterruptedException e) { - /* the queue is unbounded so it should never block. */ - } - } - - /** - * Removes a property from the given own identity. - * - * @param ownIdentity - * The own identity to remove the property from - * @param propertyName - * The name of the property to remove - */ - public void removeProperty(OwnIdentity ownIdentity, String propertyName) { - setProperty(ownIdentity, propertyName, null); - } - - // - // SERVICE METHODS - // - - /** {@inheritDoc} */ - @Override - protected void serviceRun() { - while (!shouldStop()) { - try { - WebOfTrustUpdateJob updateJob = updateJobs.take(); - if (shouldStop() || (updateJob == stopJob)) { - break; - } - logger.log(Level.FINE, "Running Trust Update Job: " + updateJob); - long startTime = System.currentTimeMillis(); - updateJob.run(); - long endTime = System.currentTimeMillis(); - logger.log(Level.FINE, "Trust Update Job finished, took " + (endTime - startTime) + " ms."); - } catch (InterruptedException ie1) { - /* happens, ignore, loop. */ - } - } - } - - /** {@inheritDoc} */ - @Override - protected void serviceStop() { - try { - updateJobs.put(stopJob); - } catch (InterruptedException ie1) { - /* the queue is unbounded so it should never block. */ - } - } - - /** - * Base class for WebOfTrust update jobs. - * - * @author David ‘Bombe’ Roden - */ - private class WebOfTrustUpdateJob { - - /** Object for synchronization. */ - @SuppressWarnings("hiding") - private final Object syncObject = new Object(); - - /** Whether the job has finished. */ - private boolean finished; - - /** Whether the job was successful. */ - private boolean success; - - // - // ACTIONS - // - - /** - * Performs the actual update operation. - *

- * The implementation of this class does nothing. - */ - public void run() { - /* does nothing. */ - } - - /** - * Waits for completion of this job or stopping of the WebOfTrust updater. - * - * @return {@code true} if this job finished successfully, {@code false} - * otherwise - * @see WebOfTrustUpdater#stop() - */ - @SuppressWarnings("synthetic-access") - public boolean waitForCompletion() { - synchronized (syncObject) { - while (!finished && !shouldStop()) { - try { - syncObject.wait(); - } catch (InterruptedException ie1) { - /* we’re looping, ignore. */ - } - } - return success; - } - } - - // - // PROTECTED METHODS - // - - /** - * Signals that this job has finished. - * - * @param success - * {@code true} if this job finished successfully, {@code false} otherwise - */ - protected void finish(boolean success) { - synchronized (syncObject) { - finished = true; - this.success = success; - syncObject.notifyAll(); - } - } - - } - - /** - * Update job that sets the trust relation between two identities. - * - * @author David ‘Bombe’ Roden - */ - private class SetTrustJob extends WebOfTrustUpdateJob { - - /** The identity giving the trust. */ - private final OwnIdentity truster; - - /** The identity receiving the trust. */ - private final Identity trustee; - - /** The score of the relation. */ - private final Integer score; - - /** The comment of the relation. */ - private final String comment; - - /** - * Creates a new set trust job. - * - * @param truster - * The identity giving the trust - * @param trustee - * The identity receiving the trust - * @param score - * The score of the trust (from -100 to 100, may be {@code null} to remote - * the trust relation completely) - * @param comment - * The comment of the trust relation - */ - public SetTrustJob(OwnIdentity truster, Identity trustee, Integer score, String comment) { - this.truster = truster; - this.trustee = trustee; - this.score = score; - this.comment = comment; - } - - /** {@inheritDoc} */ - @Override - @SuppressWarnings("synthetic-access") - public void run() { - try { - if (score != null) { - if (trustee instanceof DefaultIdentity) { - ((DefaultIdentity) trustee).setTrust(truster, new Trust(score, null, 0)); - } - webOfTrustConnector.setTrust(truster, trustee, score, comment); - } else { - if (trustee instanceof DefaultIdentity) { - ((DefaultIdentity) trustee).setTrust(truster, null); - } - webOfTrustConnector.removeTrust(truster, trustee); - } - finish(true); - } catch (WebOfTrustException wote1) { - logger.log(Level.WARNING, "Could not set Trust value for " + truster + " -> " + trustee + " to " + score + " (" + comment + ")!", wote1); - finish(false); - } - } - - // - // OBJECT METHODS - // - - /** {@inheritDoc} */ - @Override - public boolean equals(Object object) { - if ((object == null) || !object.getClass().equals(getClass())) { - return false; - } - SetTrustJob updateJob = (SetTrustJob) object; - return ((truster == null) ? (updateJob.truster == null) : updateJob.truster.equals(truster)) && ((trustee == null) ? (updateJob.trustee == null) : updateJob.trustee.equals(trustee)); - } - - /** {@inheritDoc} */ - @Override - public int hashCode() { - return getClass().hashCode() ^ ((truster == null) ? 0 : truster.hashCode()) ^ ((trustee == null) ? 0 : trustee.hashCode()); - } - - /** {@inheritDoc} */ - @Override - public String toString() { - return String.format("%s[truster=%s,trustee=%s]", getClass().getSimpleName(), (truster == null) ? null : truster.getId(), (trustee == null) ? null : trustee.getId()); - } - - } - - /** - * Base class for context updates of an {@link OwnIdentity}. - * - * @author David ‘Bombe’ Roden - */ - private class WebOfTrustContextUpdateJob extends WebOfTrustUpdateJob { - - /** The own identity whose contexts to manage. */ - protected final OwnIdentity ownIdentity; - - /** The context to update. */ - protected final String context; - - /** - * Creates a new context update job. - * - * @param ownIdentity - * The own identity to update - * @param context - * The context to update - */ - @SuppressWarnings("synthetic-access") - public WebOfTrustContextUpdateJob(OwnIdentity ownIdentity, String context) { - this.ownIdentity = checkNotNull(ownIdentity, "ownIdentity must not be null"); - this.context = checkNotNull(context, "context must not be null"); - } - - // - // OBJECT METHODS - // - - /** {@inheritDoc} */ - @Override - public boolean equals(Object object) { - if ((object == null) || !object.getClass().equals(getClass())) { - return false; - } - WebOfTrustContextUpdateJob updateJob = (WebOfTrustContextUpdateJob) object; - return updateJob.ownIdentity.equals(ownIdentity) && updateJob.context.equals(context); - } - - /** {@inheritDoc} */ - @Override - public int hashCode() { - return getClass().hashCode() ^ ownIdentity.hashCode() ^ context.hashCode(); - } - - /** {@inheritDoc} */ - @Override - public String toString() { - return String.format("%s[ownIdentity=%s,context=%s]", getClass().getSimpleName(), ownIdentity, context); - } - - } - - /** - * Job that adds a context to an {@link OwnIdentity}. - * - * @author David ‘Bombe’ Roden - */ - private class AddContextJob extends WebOfTrustContextUpdateJob { - - /** - * Creates a new add-context job. - * - * @param ownIdentity - * The own identity whose contexts to manage - * @param context - * The context to add - */ - public AddContextJob(OwnIdentity ownIdentity, String context) { - super(ownIdentity, context); - } - - /** {@inheritDoc} */ - @Override - @SuppressWarnings("synthetic-access") - public void run() { - try { - webOfTrustConnector.addContext(ownIdentity, context); - ownIdentity.addContext(context); - finish(true); - } catch (PluginException pe1) { - logger.log(Level.WARNING, String.format("Could not add Context “%2$s” to Own Identity %1$s!", ownIdentity, context), pe1); - finish(false); - } - } - - } - - /** - * Job that removes a context from an {@link OwnIdentity}. - * - * @author David ‘Bombe’ Roden - */ - private class RemoveContextJob extends WebOfTrustContextUpdateJob { - - /** - * Creates a new remove-context job. - * - * @param ownIdentity - * The own identity whose contexts to manage - * @param context - * The context to remove - */ - public RemoveContextJob(OwnIdentity ownIdentity, String context) { - super(ownIdentity, context); - } - - /** {@inheritDoc} */ - @Override - @SuppressWarnings("synthetic-access") - public void run() { - try { - webOfTrustConnector.removeContext(ownIdentity, context); - ownIdentity.removeContext(context); - finish(true); - } catch (PluginException pe1) { - logger.log(Level.WARNING, String.format("Could not remove Context “%2$s” to Own Identity %1$s!", ownIdentity, context), pe1); - finish(false); - } - } - - } - - /** - * WebOfTrust update job that sets a property on an {@link OwnIdentity}. - * - * @author David ‘Bombe’ Roden - */ - private class SetPropertyJob extends WebOfTrustUpdateJob { - - /** The own identity to update properties on. */ - private final OwnIdentity ownIdentity; - - /** The name of the property to update. */ - private final String propertyName; - - /** The value of the property to set. */ - private final String propertyValue; - - /** - * Creates a new set-property job. - * - * @param ownIdentity - * The own identity to set the property on - * @param propertyName - * The name of the property to set - * @param propertyValue - * The value of the property to set - */ - public SetPropertyJob(OwnIdentity ownIdentity, String propertyName, String propertyValue) { - this.ownIdentity = ownIdentity; - this.propertyName = propertyName; - this.propertyValue = propertyValue; - } - - /** {@inheritDoc} */ - @Override - @SuppressWarnings("synthetic-access") - public void run() { - try { - if (propertyValue == null) { - webOfTrustConnector.removeProperty(ownIdentity, propertyName); - ownIdentity.removeProperty(propertyName); - } else { - webOfTrustConnector.setProperty(ownIdentity, propertyName, propertyValue); - ownIdentity.setProperty(propertyName, propertyValue); - } - finish(true); - } catch (PluginException pe1) { - logger.log(Level.WARNING, String.format("Could not set Property “%2$s” to “%3$s” on Own Identity %1$s!", ownIdentity, propertyName, propertyValue), pe1); - finish(false); - } - } - - // - // OBJECT METHODS - // - - /** {@inheritDoc} */ - @Override - public boolean equals(Object object) { - if ((object == null) || !object.getClass().equals(getClass())) { - return false; - } - SetPropertyJob updateJob = (SetPropertyJob) object; - return updateJob.ownIdentity.equals(ownIdentity) && updateJob.propertyName.equals(propertyName); - } - - /** {@inheritDoc} */ - @Override - public int hashCode() { - return getClass().hashCode() ^ ownIdentity.hashCode() ^ propertyName.hashCode(); - } - - /** {@inheritDoc} */ - @Override - public String toString() { - return String.format("%s[ownIdentity=%s,propertyName=%s]", getClass().getSimpleName(), ownIdentity, propertyName); - } - - } +@ImplementedBy(WebOfTrustUpdaterImpl.class) +public interface WebOfTrustUpdater extends Service { + + void setTrust(OwnIdentity truster, Identity trustee, Integer score, String comment); + boolean addContextWait(OwnIdentity ownIdentity, String context); + void removeContext(OwnIdentity ownIdentity, String context); + void setProperty(OwnIdentity ownIdentity, String propertyName, String propertyValue); + void removeProperty(OwnIdentity ownIdentity, String propertyName); } diff --git a/src/main/java/net/pterodactylus/sone/core/WebOfTrustUpdaterImpl.java b/src/main/java/net/pterodactylus/sone/core/WebOfTrustUpdaterImpl.java new file mode 100644 index 0000000..56bd8c9 --- /dev/null +++ b/src/main/java/net/pterodactylus/sone/core/WebOfTrustUpdaterImpl.java @@ -0,0 +1,598 @@ +/* + * Sone - WebOfTrustUpdater.java - Copyright © 2013 David Roden + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.pterodactylus.sone.core; + +import static com.google.common.base.Preconditions.checkNotNull; +import static java.util.logging.Logger.getLogger; + +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.logging.Level; +import java.util.logging.Logger; + +import net.pterodactylus.sone.freenet.plugin.PluginException; +import net.pterodactylus.sone.freenet.wot.Identity; +import net.pterodactylus.sone.freenet.wot.OwnIdentity; +import net.pterodactylus.sone.freenet.wot.Trust; +import net.pterodactylus.sone.freenet.wot.WebOfTrustConnector; +import net.pterodactylus.sone.freenet.wot.WebOfTrustException; +import net.pterodactylus.util.service.AbstractService; + +import com.google.common.annotations.VisibleForTesting; +import com.google.inject.Inject; +import com.google.inject.Singleton; + +/** + * Updates WebOfTrust identity data in a background thread because communicating + * with the WebOfTrust plugin can potentially last quite long. + * + * @author David ‘Bombe’ Roden + */ +@Singleton +public class WebOfTrustUpdaterImpl extends AbstractService implements WebOfTrustUpdater { + + /** The logger. */ + private static final Logger logger = getLogger("Sone.WoT.Updater"); + + /** Stop job. */ + @SuppressWarnings("synthetic-access") + private final WebOfTrustUpdateJob stopJob = new WebOfTrustUpdateJob(); + + /** The web of trust connector. */ + private final WebOfTrustConnector webOfTrustConnector; + + /** The queue for jobs. */ + private final BlockingQueue updateJobs = new LinkedBlockingQueue(); + + /** + * Creates a new trust updater. + * + * @param webOfTrustConnector + * The web of trust connector + */ + @Inject + public WebOfTrustUpdaterImpl(WebOfTrustConnector webOfTrustConnector) { + super("Trust Updater"); + this.webOfTrustConnector = webOfTrustConnector; + } + + // + // ACTIONS + // + + /** + * Updates the trust relation between the truster and the trustee. This method + * will return immediately and perform a trust update in the background. + * + * @param truster + * The identity giving the trust + * @param trustee + * The identity receiving the trust + * @param score + * The new level of trust (from -100 to 100, may be {@code null} to remove + * the trust completely) + * @param comment + * The comment of the trust relation + */ + @Override + public void setTrust(OwnIdentity truster, Identity trustee, Integer score, String comment) { + SetTrustJob setTrustJob = new SetTrustJob(truster, trustee, score, comment); + if (updateJobs.contains(setTrustJob)) { + updateJobs.remove(setTrustJob); + } + logger.log(Level.FINER, "Adding Trust Update Job: " + setTrustJob); + try { + updateJobs.put(setTrustJob); + } catch (InterruptedException e) { + /* the queue is unbounded so it should never block. */ + } + } + + /** + * Adds the given context to the given own identity, waiting for completion of + * the operation. + * + * @param ownIdentity + * The own identity to add the context to + * @param context + * The context to add + * @return {@code true} if the context was added successfully, {@code false} + * otherwise + */ + @Override + public boolean addContextWait(OwnIdentity ownIdentity, String context) { + AddContextJob addContextJob = new AddContextJob(ownIdentity, context); + if (!updateJobs.contains(addContextJob)) { + logger.log(Level.FINER, "Adding Context Job: " + addContextJob); + try { + updateJobs.put(addContextJob); + } catch (InterruptedException ie1) { + /* the queue is unbounded so it should never block. */ + } + return addContextJob.waitForCompletion(); + } else { + for (WebOfTrustUpdateJob updateJob : updateJobs) { + if (updateJob.equals(addContextJob)) { + return updateJob.waitForCompletion(); + } + } + } + return false; + } + + /** + * Removes the given context from the given own identity. + * + * @param ownIdentity + * The own identity to remove the context from + * @param context + * The context to remove + */ + @Override + public void removeContext(OwnIdentity ownIdentity, String context) { + RemoveContextJob removeContextJob = new RemoveContextJob(ownIdentity, context); + if (!updateJobs.contains(removeContextJob)) { + logger.log(Level.FINER, "Adding Context Job: " + removeContextJob); + try { + updateJobs.put(removeContextJob); + } catch (InterruptedException ie1) { + /* the queue is unbounded so it should never block. */ + } + } + } + + /** + * Sets a property on the given own identity. + * + * @param ownIdentity + * The own identity to set the property on + * @param propertyName + * The name of the property to set + * @param propertyValue + * The value of the property to set + */ + @Override + public void setProperty(OwnIdentity ownIdentity, String propertyName, String propertyValue) { + SetPropertyJob setPropertyJob = new SetPropertyJob(ownIdentity, propertyName, propertyValue); + if (updateJobs.contains(setPropertyJob)) { + updateJobs.remove(setPropertyJob); + } + logger.log(Level.FINER, "Adding Property Job: " + setPropertyJob); + try { + updateJobs.put(setPropertyJob); + } catch (InterruptedException e) { + /* the queue is unbounded so it should never block. */ + } + } + + /** + * Removes a property from the given own identity. + * + * @param ownIdentity + * The own identity to remove the property from + * @param propertyName + * The name of the property to remove + */ + @Override + public void removeProperty(OwnIdentity ownIdentity, String propertyName) { + setProperty(ownIdentity, propertyName, null); + } + + // + // SERVICE METHODS + // + + /** {@inheritDoc} */ + @Override + protected void serviceRun() { + while (!shouldStop()) { + try { + WebOfTrustUpdateJob updateJob = updateJobs.take(); + if (shouldStop()) { + break; + } + logger.log(Level.FINE, "Running Trust Update Job: " + updateJob); + long startTime = System.currentTimeMillis(); + updateJob.run(); + long endTime = System.currentTimeMillis(); + logger.log(Level.FINE, "Trust Update Job finished, took " + (endTime - startTime) + " ms."); + } catch (InterruptedException ie1) { + /* happens, ignore, loop. */ + } + } + } + + /** {@inheritDoc} */ + @Override + protected void serviceStop() { + try { + updateJobs.put(stopJob); + } catch (InterruptedException ie1) { + /* the queue is unbounded so it should never block. */ + } + } + + /** + * Base class for WebOfTrust update jobs. + * + * @author David ‘Bombe’ Roden + */ + @VisibleForTesting + class WebOfTrustUpdateJob implements Runnable { + + /** Object for synchronization. */ + @SuppressWarnings("hiding") + private final Object syncObject = new Object(); + + /** Whether the job has finished. */ + private boolean finished; + + /** Whether the job was successful. */ + private boolean success; + + // + // ACTIONS + // + + /** + * Performs the actual update operation. + *

+ * The implementation of this class does nothing. + */ + @Override + public void run() { + /* does nothing. */ + } + + /** + * Waits for completion of this job or stopping of the WebOfTrust updater. + * + * @return {@code true} if this job finished successfully, {@code false} + * otherwise + * @see WebOfTrustUpdaterImpl#stop() + */ + @SuppressWarnings("synthetic-access") + public boolean waitForCompletion() { + synchronized (syncObject) { + while (!finished && !shouldStop()) { + try { + syncObject.wait(); + } catch (InterruptedException ie1) { + /* we’re looping, ignore. */ + } + } + return success; + } + } + + // + // PROTECTED METHODS + // + + /** + * Signals that this job has finished. + * + * @param success + * {@code true} if this job finished successfully, {@code false} otherwise + */ + protected void finish(boolean success) { + synchronized (syncObject) { + finished = true; + this.success = success; + syncObject.notifyAll(); + } + } + + } + + /** + * Update job that sets the trust relation between two identities. + * + * @author David ‘Bombe’ Roden + */ + @VisibleForTesting + class SetTrustJob extends WebOfTrustUpdateJob { + + /** The identity giving the trust. */ + private final OwnIdentity truster; + + /** The identity receiving the trust. */ + private final Identity trustee; + + /** The score of the relation. */ + private final Integer score; + + /** The comment of the relation. */ + private final String comment; + + /** + * Creates a new set trust job. + * + * @param truster + * The identity giving the trust + * @param trustee + * The identity receiving the trust + * @param score + * The score of the trust (from -100 to 100, may be {@code null} to remote + * the trust relation completely) + * @param comment + * The comment of the trust relation + */ + public SetTrustJob(OwnIdentity truster, Identity trustee, Integer score, String comment) { + this.truster = checkNotNull(truster, "truster must not be null"); + this.trustee = checkNotNull(trustee, "trustee must not be null"); + this.score = score; + this.comment = comment; + } + + /** {@inheritDoc} */ + @Override + @SuppressWarnings("synthetic-access") + public void run() { + try { + if (score != null) { + webOfTrustConnector.setTrust(truster, trustee, score, comment); + trustee.setTrust(truster, new Trust(score, null, 0)); + } else { + webOfTrustConnector.removeTrust(truster, trustee); + trustee.removeTrust(truster); + } + finish(true); + } catch (WebOfTrustException wote1) { + logger.log(Level.WARNING, "Could not set Trust value for " + truster + " -> " + trustee + " to " + score + " (" + comment + ")!", wote1); + finish(false); + } + } + + // + // OBJECT METHODS + // + + /** {@inheritDoc} */ + @Override + public boolean equals(Object object) { + if ((object == null) || !object.getClass().equals(getClass())) { + return false; + } + SetTrustJob updateJob = (SetTrustJob) object; + return updateJob.truster.equals(truster) && updateJob.trustee.equals(trustee); + } + + /** {@inheritDoc} */ + @Override + public int hashCode() { + return getClass().hashCode() ^ truster.hashCode() ^ trustee.hashCode(); + } + + /** {@inheritDoc} */ + @Override + public String toString() { + return String.format("%s[truster=%s,trustee=%s]", getClass().getSimpleName(), truster.getId(), trustee.getId()); + } + + } + + /** + * Base class for context updates of an {@link OwnIdentity}. + * + * @author David ‘Bombe’ Roden + */ + @VisibleForTesting + class WebOfTrustContextUpdateJob extends WebOfTrustUpdateJob { + + /** The own identity whose contexts to manage. */ + protected final OwnIdentity ownIdentity; + + /** The context to update. */ + protected final String context; + + /** + * Creates a new context update job. + * + * @param ownIdentity + * The own identity to update + * @param context + * The context to update + */ + @SuppressWarnings("synthetic-access") + public WebOfTrustContextUpdateJob(OwnIdentity ownIdentity, String context) { + this.ownIdentity = checkNotNull(ownIdentity, "ownIdentity must not be null"); + this.context = checkNotNull(context, "context must not be null"); + } + + // + // OBJECT METHODS + // + + /** {@inheritDoc} */ + @Override + public boolean equals(Object object) { + if ((object == null) || !object.getClass().equals(getClass())) { + return false; + } + WebOfTrustContextUpdateJob updateJob = (WebOfTrustContextUpdateJob) object; + return updateJob.ownIdentity.equals(ownIdentity) && updateJob.context.equals(context); + } + + /** {@inheritDoc} */ + @Override + public int hashCode() { + return getClass().hashCode() ^ ownIdentity.hashCode() ^ context.hashCode(); + } + + /** {@inheritDoc} */ + @Override + public String toString() { + return String.format("%s[ownIdentity=%s,context=%s]", getClass().getSimpleName(), ownIdentity, context); + } + + } + + /** + * Job that adds a context to an {@link OwnIdentity}. + * + * @author David ‘Bombe’ Roden + */ + @VisibleForTesting + class AddContextJob extends WebOfTrustContextUpdateJob { + + /** + * Creates a new add-context job. + * + * @param ownIdentity + * The own identity whose contexts to manage + * @param context + * The context to add + */ + public AddContextJob(OwnIdentity ownIdentity, String context) { + super(ownIdentity, context); + } + + /** {@inheritDoc} */ + @Override + @SuppressWarnings("synthetic-access") + public void run() { + try { + webOfTrustConnector.addContext(ownIdentity, context); + ownIdentity.addContext(context); + finish(true); + } catch (PluginException pe1) { + logger.log(Level.WARNING, String.format("Could not add Context “%2$s” to Own Identity %1$s!", ownIdentity, context), pe1); + finish(false); + } + } + + } + + /** + * Job that removes a context from an {@link OwnIdentity}. + * + * @author David ‘Bombe’ Roden + */ + @VisibleForTesting + class RemoveContextJob extends WebOfTrustContextUpdateJob { + + /** + * Creates a new remove-context job. + * + * @param ownIdentity + * The own identity whose contexts to manage + * @param context + * The context to remove + */ + public RemoveContextJob(OwnIdentity ownIdentity, String context) { + super(ownIdentity, context); + } + + /** {@inheritDoc} */ + @Override + @SuppressWarnings("synthetic-access") + public void run() { + try { + webOfTrustConnector.removeContext(ownIdentity, context); + ownIdentity.removeContext(context); + finish(true); + } catch (PluginException pe1) { + logger.log(Level.WARNING, String.format("Could not remove Context “%2$s” to Own Identity %1$s!", ownIdentity, context), pe1); + finish(false); + } + } + + } + + /** + * WebOfTrust update job that sets a property on an {@link OwnIdentity}. + * + * @author David ‘Bombe’ Roden + */ + @VisibleForTesting + class SetPropertyJob extends WebOfTrustUpdateJob { + + /** The own identity to update properties on. */ + private final OwnIdentity ownIdentity; + + /** The name of the property to update. */ + private final String propertyName; + + /** The value of the property to set. */ + private final String propertyValue; + + /** + * Creates a new set-property job. + * + * @param ownIdentity + * The own identity to set the property on + * @param propertyName + * The name of the property to set + * @param propertyValue + * The value of the property to set + */ + public SetPropertyJob(OwnIdentity ownIdentity, String propertyName, String propertyValue) { + this.ownIdentity = ownIdentity; + this.propertyName = propertyName; + this.propertyValue = propertyValue; + } + + /** {@inheritDoc} */ + @Override + @SuppressWarnings("synthetic-access") + public void run() { + try { + if (propertyValue == null) { + webOfTrustConnector.removeProperty(ownIdentity, propertyName); + ownIdentity.removeProperty(propertyName); + } else { + webOfTrustConnector.setProperty(ownIdentity, propertyName, propertyValue); + ownIdentity.setProperty(propertyName, propertyValue); + } + finish(true); + } catch (PluginException pe1) { + logger.log(Level.WARNING, String.format("Could not set Property “%2$s” to “%3$s” on Own Identity %1$s!", ownIdentity, propertyName, propertyValue), pe1); + finish(false); + } + } + + // + // OBJECT METHODS + // + + /** {@inheritDoc} */ + @Override + public boolean equals(Object object) { + if ((object == null) || !object.getClass().equals(getClass())) { + return false; + } + SetPropertyJob updateJob = (SetPropertyJob) object; + return updateJob.ownIdentity.equals(ownIdentity) && updateJob.propertyName.equals(propertyName); + } + + /** {@inheritDoc} */ + @Override + public int hashCode() { + return getClass().hashCode() ^ ownIdentity.hashCode() ^ propertyName.hashCode(); + } + + /** {@inheritDoc} */ + @Override + public String toString() { + return String.format("%s[ownIdentity=%s,propertyName=%s]", getClass().getSimpleName(), ownIdentity, propertyName); + } + + } + +} diff --git a/src/main/java/net/pterodactylus/sone/core/event/InsertionDelayChangedEvent.java b/src/main/java/net/pterodactylus/sone/core/event/InsertionDelayChangedEvent.java new file mode 100644 index 0000000..a3dc2ce --- /dev/null +++ b/src/main/java/net/pterodactylus/sone/core/event/InsertionDelayChangedEvent.java @@ -0,0 +1,23 @@ +package net.pterodactylus.sone.core.event; + +import com.google.common.eventbus.EventBus; + +/** + * Notifies interested {@link EventBus} clients that the Sone insertion delay + * has changed. + * + * @author David ‘Bombe’ Roden + */ +public class InsertionDelayChangedEvent { + + private final int insertionDelay; + + public InsertionDelayChangedEvent(int insertionDelay) { + this.insertionDelay = insertionDelay; + } + + public int getInsertionDelay() { + return insertionDelay; + } + +} diff --git a/src/main/java/net/pterodactylus/sone/core/event/SoneInsertedEvent.java b/src/main/java/net/pterodactylus/sone/core/event/SoneInsertedEvent.java index 8a4280d..936a134 100644 --- a/src/main/java/net/pterodactylus/sone/core/event/SoneInsertedEvent.java +++ b/src/main/java/net/pterodactylus/sone/core/event/SoneInsertedEvent.java @@ -26,33 +26,21 @@ import net.pterodactylus.sone.data.Sone; */ public class SoneInsertedEvent extends SoneEvent { - /** The duration of the insert. */ private final long insertDuration; + private final String insertFingerprint; - /** - * Creates a new “Sone was inserted” event. - * - * @param sone - * The Sone that was inserted - * @param insertDuration - * The duration of the insert (in milliseconds) - */ - public SoneInsertedEvent(Sone sone, long insertDuration) { + public SoneInsertedEvent(Sone sone, long insertDuration, String insertFingerprint) { super(sone); this.insertDuration = insertDuration; + this.insertFingerprint = insertFingerprint; } - // - // ACCESSORS - // - - /** - * Returns the duration of the insert. - * - * @return The duration of the insert (in milliseconds) - */ public long insertDuration() { return insertDuration; } + public String insertFingerprint() { + return insertFingerprint; + } + } diff --git a/src/main/java/net/pterodactylus/sone/data/Album.java b/src/main/java/net/pterodactylus/sone/data/Album.java index 09a7da4..c75088f 100644 --- a/src/main/java/net/pterodactylus/sone/data/Album.java +++ b/src/main/java/net/pterodactylus/sone/data/Album.java @@ -116,16 +116,6 @@ public interface Album extends Identified, Fingerprintable { Sone getSone(); /** - * Sets the owner of the album. The owner can only be set as long as the - * current owner is {@code null}. - * - * @param sone - * The album owner - * @return This album - */ - Album setSone(Sone sone); - - /** * Returns the nested albums. * * @return The nested albums @@ -302,6 +292,8 @@ public interface Album extends Identified, Fingerprintable { Album update() throws IllegalStateException; + class AlbumTitleMustNotBeEmpty extends IllegalStateException { } + } } diff --git a/src/main/java/net/pterodactylus/sone/data/AlbumImpl.java b/src/main/java/net/pterodactylus/sone/data/AlbumImpl.java deleted file mode 100644 index f489b0b..0000000 --- a/src/main/java/net/pterodactylus/sone/data/AlbumImpl.java +++ /dev/null @@ -1,380 +0,0 @@ -/* - * Sone - Album.java - Copyright © 2011–2013 David Roden - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package net.pterodactylus.sone.data; - -import static com.google.common.base.Optional.absent; -import static com.google.common.base.Optional.fromNullable; -import static com.google.common.base.Preconditions.checkArgument; -import static com.google.common.base.Preconditions.checkNotNull; -import static com.google.common.base.Preconditions.checkState; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.UUID; - -import com.google.common.base.Function; -import com.google.common.base.Optional; -import com.google.common.base.Predicates; -import com.google.common.collect.Collections2; -import com.google.common.hash.Hasher; -import com.google.common.hash.Hashing; - -/** - * Container for images that can also contain nested {@link AlbumImpl}s. - * - * @author David ‘Bombe’ Roden - */ -public class AlbumImpl implements Album { - - /** The ID of this album. */ - private final String id; - - /** The Sone this album belongs to. */ - private Sone sone; - - /** Nested albums. */ - private final List albums = new ArrayList(); - - /** The image IDs in order. */ - private final List imageIds = new ArrayList(); - - /** The images in this album. */ - private final Map images = new HashMap(); - - /** The parent album. */ - private Album parent; - - /** The title of this album. */ - private String title; - - /** The description of this album. */ - private String description; - - /** The ID of the album picture. */ - private String albumImage; - - /** Creates a new album with a random ID. */ - public AlbumImpl() { - this(UUID.randomUUID().toString()); - } - - /** - * Creates a new album with the given ID. - * - * @param id - * The ID of the album - */ - public AlbumImpl(String id) { - this.id = checkNotNull(id, "id must not be null"); - } - - // - // ACCESSORS - // - - @Override - public String getId() { - return id; - } - - @Override - public Sone getSone() { - return sone; - } - - @Override - public Album setSone(Sone sone) { - checkNotNull(sone, "sone must not be null"); - checkState((this.sone == null) || (this.sone.equals(sone)), "album owner must not already be set to some other Sone"); - this.sone = sone; - return this; - } - - @Override - public List getAlbums() { - return new ArrayList(albums); - } - - @Override - public void addAlbum(Album album) { - checkNotNull(album, "album must not be null"); - checkArgument(album.getSone().equals(sone), "album must belong to the same Sone as this album"); - album.setParent(this); - if (!albums.contains(album)) { - albums.add(album); - } - } - - @Override - public void removeAlbum(Album album) { - checkNotNull(album, "album must not be null"); - checkArgument(album.getSone().equals(sone), "album must belong this album’s Sone"); - checkArgument(equals(album.getParent()), "album must belong to this album"); - albums.remove(album); - album.removeParent(); - } - - @Override - public Album moveAlbumUp(Album album) { - checkNotNull(album, "album must not be null"); - checkArgument(album.getSone().equals(sone), "album must belong to the same Sone as this album"); - checkArgument(equals(album.getParent()), "album must belong to this album"); - int oldIndex = albums.indexOf(album); - if (oldIndex <= 0) { - return null; - } - albums.remove(oldIndex); - albums.add(oldIndex - 1, album); - return albums.get(oldIndex); - } - - @Override - public Album moveAlbumDown(Album album) { - checkNotNull(album, "album must not be null"); - checkArgument(album.getSone().equals(sone), "album must belong to the same Sone as this album"); - checkArgument(equals(album.getParent()), "album must belong to this album"); - int oldIndex = albums.indexOf(album); - if ((oldIndex < 0) || (oldIndex >= (albums.size() - 1))) { - return null; - } - albums.remove(oldIndex); - albums.add(oldIndex + 1, album); - return albums.get(oldIndex); - } - - @Override - public List getImages() { - return new ArrayList(Collections2.filter(Collections2.transform(imageIds, new Function() { - - @Override - @SuppressWarnings("synthetic-access") - public Image apply(String imageId) { - return images.get(imageId); - } - }), Predicates.notNull())); - } - - @Override - public void addImage(Image image) { - checkNotNull(image, "image must not be null"); - checkNotNull(image.getSone(), "image must have an owner"); - checkArgument(image.getSone().equals(sone), "image must belong to the same Sone as this album"); - if (image.getAlbum() != null) { - image.getAlbum().removeImage(image); - } - image.setAlbum(this); - if (imageIds.isEmpty() && (albumImage == null)) { - albumImage = image.getId(); - } - if (!imageIds.contains(image.getId())) { - imageIds.add(image.getId()); - images.put(image.getId(), image); - } - } - - @Override - public void removeImage(Image image) { - checkNotNull(image, "image must not be null"); - checkNotNull(image.getSone(), "image must have an owner"); - checkArgument(image.getSone().equals(sone), "image must belong to the same Sone as this album"); - imageIds.remove(image.getId()); - images.remove(image.getId()); - if (image.getId().equals(albumImage)) { - if (images.isEmpty()) { - albumImage = null; - } else { - albumImage = images.values().iterator().next().getId(); - } - } - } - - @Override - public Image moveImageUp(Image image) { - checkNotNull(image, "image must not be null"); - checkNotNull(image.getSone(), "image must have an owner"); - checkArgument(image.getSone().equals(sone), "image must belong to the same Sone as this album"); - checkArgument(image.getAlbum().equals(this), "image must belong to this album"); - int oldIndex = imageIds.indexOf(image.getId()); - if (oldIndex <= 0) { - return null; - } - imageIds.remove(image.getId()); - imageIds.add(oldIndex - 1, image.getId()); - return images.get(imageIds.get(oldIndex)); - } - - @Override - public Image moveImageDown(Image image) { - checkNotNull(image, "image must not be null"); - checkNotNull(image.getSone(), "image must have an owner"); - checkArgument(image.getSone().equals(sone), "image must belong to the same Sone as this album"); - checkArgument(image.getAlbum().equals(this), "image must belong to this album"); - int oldIndex = imageIds.indexOf(image.getId()); - if ((oldIndex == -1) || (oldIndex >= (imageIds.size() - 1))) { - return null; - } - imageIds.remove(image.getId()); - imageIds.add(oldIndex + 1, image.getId()); - return images.get(imageIds.get(oldIndex)); - } - - @Override - public Image getAlbumImage() { - if (albumImage == null) { - return null; - } - return Optional.fromNullable(images.get(albumImage)).or(images.values().iterator().next()); - } - - @Override - public boolean isEmpty() { - return albums.isEmpty() && images.isEmpty(); - } - - @Override - public boolean isRoot() { - return parent == null; - } - - @Override - public Album getParent() { - return parent; - } - - @Override - public Album setParent(Album parent) { - this.parent = checkNotNull(parent, "parent must not be null"); - return this; - } - - @Override - public Album removeParent() { - this.parent = null; - return this; - } - - @Override - public String getTitle() { - return title; - } - - @Override - public String getDescription() { - return description; - } - - @Override - public Modifier modify() throws IllegalStateException { - // TODO: reenable check for local Sones - return new Modifier() { - private Optional title = absent(); - - private Optional description = absent(); - - private Optional albumImage = absent(); - - @Override - public Modifier setTitle(String title) { - this.title = fromNullable(title); - return this; - } - - @Override - public Modifier setDescription(String description) { - this.description = fromNullable(description); - return this; - } - - @Override - public Modifier setAlbumImage(String imageId) { - this.albumImage = fromNullable(imageId); - return this; - } - - @Override - public Album update() throws IllegalStateException { - if (title.isPresent()) { - AlbumImpl.this.title = title.get(); - } - if (description.isPresent()) { - AlbumImpl.this.description = description.get(); - } - if (albumImage.isPresent()) { - AlbumImpl.this.albumImage = albumImage.get(); - } - return AlbumImpl.this; - } - }; - } - - // - // FINGERPRINTABLE METHODS - // - - @Override - public String getFingerprint() { - Hasher hash = Hashing.sha256().newHasher(); - hash.putString("Album("); - hash.putString("ID(").putString(id).putString(")"); - hash.putString("Title(").putString(title).putString(")"); - hash.putString("Description(").putString(description).putString(")"); - if (albumImage != null) { - hash.putString("AlbumImage(").putString(albumImage).putString(")"); - } - - /* add nested albums. */ - hash.putString("Albums("); - for (Album album : albums) { - hash.putString(album.getFingerprint()); - } - hash.putString(")"); - - /* add images. */ - hash.putString("Images("); - for (Image image : getImages()) { - if (image.isInserted()) { - hash.putString(image.getFingerprint()); - } - } - hash.putString(")"); - - hash.putString(")"); - return hash.hash().toString(); - } - - // - // OBJECT METHODS - // - - @Override - public int hashCode() { - return id.hashCode(); - } - - @Override - public boolean equals(Object object) { - if (!(object instanceof AlbumImpl)) { - return false; - } - AlbumImpl album = (AlbumImpl) object; - return id.equals(album.id); - } - -} diff --git a/src/main/java/net/pterodactylus/sone/data/Client.java b/src/main/java/net/pterodactylus/sone/data/Client.java index b77b9f2..ad094f2 100644 --- a/src/main/java/net/pterodactylus/sone/data/Client.java +++ b/src/main/java/net/pterodactylus/sone/data/Client.java @@ -17,6 +17,8 @@ package net.pterodactylus.sone.data; +import static com.google.common.base.Objects.equal; + /** * Container for the client information of a {@link Sone}. * @@ -65,4 +67,13 @@ public class Client { return version; } + @Override + public boolean equals(Object object) { + if (!(object instanceof Client)) { + return false; + } + Client client = (Client) object; + return equal(getName(), client.getName()) && equal(getVersion(), client.getVersion()); + } + } diff --git a/src/main/java/net/pterodactylus/sone/data/Image.java b/src/main/java/net/pterodactylus/sone/data/Image.java index e3e1b4d..22ddc29 100644 --- a/src/main/java/net/pterodactylus/sone/data/Image.java +++ b/src/main/java/net/pterodactylus/sone/data/Image.java @@ -134,6 +134,8 @@ public interface Image extends Identified, Fingerprintable { Image update() throws IllegalStateException; + class ImageTitleMustNotBeEmpty extends IllegalStateException { } + } } diff --git a/src/main/java/net/pterodactylus/sone/data/ImageImpl.java b/src/main/java/net/pterodactylus/sone/data/ImageImpl.java deleted file mode 100644 index 447bb82..0000000 --- a/src/main/java/net/pterodactylus/sone/data/ImageImpl.java +++ /dev/null @@ -1,271 +0,0 @@ -/* - * Sone - ImageImpl.java - Copyright © 2011–2013 David Roden - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package net.pterodactylus.sone.data; - -import static com.google.common.base.Optional.absent; -import static com.google.common.base.Optional.fromNullable; -import static com.google.common.base.Optional.of; -import static com.google.common.base.Preconditions.checkNotNull; -import static com.google.common.base.Preconditions.checkState; - -import java.util.UUID; - -import com.google.common.base.Optional; -import com.google.common.hash.Hasher; -import com.google.common.hash.Hashing; - -/** - * Container for image metadata. - * - * @author David ‘Bombe’ Roden - */ -public class ImageImpl implements Image { - - /** The ID of the image. */ - private final String id; - - /** The Sone the image belongs to. */ - private Sone sone; - - /** The album this image belongs to. */ - private Album album; - - /** The request key of the image. */ - private String key; - - /** The creation time of the image. */ - private long creationTime; - - /** The width of the image. */ - private int width; - - /** The height of the image. */ - private int height; - - /** The title of the image. */ - private String title; - - /** The description of the image. */ - private String description; - - /** Creates a new image with a random ID. */ - public ImageImpl() { - this(UUID.randomUUID().toString()); - this.creationTime = System.currentTimeMillis(); - } - - /** - * Creates a new image. - * - * @param id - * The ID of the image - */ - public ImageImpl(String id) { - this.id = checkNotNull(id, "id must not be null"); - } - - // - // ACCESSORS - // - - @Override - public String getId() { - return id; - } - - @Override - public Sone getSone() { - return sone; - } - - @Override - public Album getAlbum() { - return album; - } - - @Override - public Image setAlbum(Album album) { - checkNotNull(album, "album must not be null"); - checkNotNull(album.getSone().equals(getSone()), "album must belong to the same Sone as this image"); - this.album = album; - return this; - } - - @Override - public String getKey() { - return key; - } - - @Override - public boolean isInserted() { - return key != null; - } - - @Override - public long getCreationTime() { - return creationTime; - } - - @Override - public int getWidth() { - return width; - } - - @Override - public int getHeight() { - return height; - } - - @Override - public String getTitle() { - return title; - } - - @Override - public String getDescription() { - return description; - } - - public Modifier modify() throws IllegalStateException { - // TODO: reenable check for local images - return new Modifier() { - private Optional sone = absent(); - - private Optional creationTime = absent(); - - private Optional key = absent(); - - private Optional title = absent(); - - private Optional description = absent(); - - private Optional width = absent(); - - private Optional height = absent(); - - @Override - public Modifier setSone(Sone sone) { - this.sone = fromNullable(sone); - return this; - } - - @Override - public Modifier setCreationTime(long creationTime) { - this.creationTime = of(creationTime); - return this; - } - - @Override - public Modifier setKey(String key) { - this.key = fromNullable(key); - return this; - } - - @Override - public Modifier setTitle(String title) { - this.title = fromNullable(title); - return this; - } - - @Override - public Modifier setDescription(String description) { - this.description = fromNullable(description); - return this; - } - - @Override - public Modifier setWidth(int width) { - this.width = of(width); - return this; - } - - @Override - public Modifier setHeight(int height) { - this.height = of(height); - return this; - } - - @Override - public Image update() throws IllegalStateException { - checkState(!sone.isPresent() || (ImageImpl.this.sone == null) || sone.get().equals(ImageImpl.this.sone), "can not change Sone once set"); - checkState(!creationTime.isPresent() || ((ImageImpl.this.creationTime == 0) || (ImageImpl.this.creationTime == creationTime.get())), "can not change creation time once set"); - checkState(!key.isPresent() || (ImageImpl.this.key == null) || key.get().equals(ImageImpl.this.key), "can not change key once set"); - checkState(!width.isPresent() || (ImageImpl.this.width == 0) || width.get().equals(ImageImpl.this.width), "can not change width once set"); - checkState(!height.isPresent() || (ImageImpl.this.height == 0) || height.get().equals(ImageImpl.this.height), "can not change height once set"); - - if (sone.isPresent()) { - ImageImpl.this.sone = sone.get(); - } - if (creationTime.isPresent()) { - ImageImpl.this.creationTime = creationTime.get(); - } - if (key.isPresent()) { - ImageImpl.this.key = key.get(); - } - if (title.isPresent()) { - ImageImpl.this.title = title.get(); - } - if (description.isPresent()) { - ImageImpl.this.description = description.get(); - } - if (width.isPresent()) { - ImageImpl.this.width = width.get(); - } - if (height.isPresent()) { - ImageImpl.this.height = height.get(); - } - - return ImageImpl.this; - } - }; - } - - // - // FINGERPRINTABLE METHODS - // - - @Override - public String getFingerprint() { - Hasher hash = Hashing.sha256().newHasher(); - hash.putString("Image("); - hash.putString("ID(").putString(id).putString(")"); - hash.putString("Title(").putString(title).putString(")"); - hash.putString("Description(").putString(description).putString(")"); - hash.putString(")"); - return hash.hash().toString(); - } - - // - // OBJECT METHODS - // - - /** {@inheritDoc} */ - @Override - public int hashCode() { - return id.hashCode(); - } - - /** {@inheritDoc} */ - @Override - public boolean equals(Object object) { - if (!(object instanceof ImageImpl)) { - return false; - } - return ((ImageImpl) object).id.equals(id); - } - -} diff --git a/src/main/java/net/pterodactylus/sone/data/Post.java b/src/main/java/net/pterodactylus/sone/data/Post.java index 759a8ba..95abae6 100644 --- a/src/main/java/net/pterodactylus/sone/data/Post.java +++ b/src/main/java/net/pterodactylus/sone/data/Post.java @@ -17,6 +17,8 @@ package net.pterodactylus.sone.data; +import static com.google.common.base.Optional.absent; + import java.util.Comparator; import com.google.common.base.Optional; @@ -45,7 +47,7 @@ public interface Post extends Identified { @Override public boolean apply(Post post) { - return (post == null) ? false : post.getTime() <= System.currentTimeMillis(); + return (post != null) && (post.getTime() <= System.currentTimeMillis()); } }; @@ -62,6 +64,14 @@ public interface Post extends Identified { public String getId(); /** + * Returns whether this post has already been loaded. + * + * @return {@code true} if this post has already been loaded, {@code + * false} otherwise + */ + boolean isLoaded(); + + /** * Returns the Sone this post belongs to. * * @return The Sone of this post @@ -114,4 +124,65 @@ public interface Post extends Identified { */ public Post setKnown(boolean known); + /** + * Shell for a post that has not yet been loaded. + * + * @author David ‘Bombe’ + * Roden + */ + public static class EmptyPost implements Post { + + private final String id; + + public EmptyPost(String id) { + this.id = id; + } + + @Override + public String getId() { + return id; + } + + @Override + public boolean isLoaded() { + return false; + } + + @Override + public Sone getSone() { + return null; + } + + @Override + public Optional getRecipientId() { + return absent(); + } + + @Override + public Optional getRecipient() { + return absent(); + } + + @Override + public long getTime() { + return 0; + } + + @Override + public String getText() { + return null; + } + + @Override + public boolean isKnown() { + return false; + } + + @Override + public Post setKnown(boolean known) { + return this; + } + + } + } diff --git a/src/main/java/net/pterodactylus/sone/data/PostReply.java b/src/main/java/net/pterodactylus/sone/data/PostReply.java index 4a1fbad..d010261 100644 --- a/src/main/java/net/pterodactylus/sone/data/PostReply.java +++ b/src/main/java/net/pterodactylus/sone/data/PostReply.java @@ -36,7 +36,7 @@ public interface PostReply extends Reply { @Override public boolean apply(PostReply postReply) { - return (postReply == null) ? false : postReply.getPost().isPresent(); + return (postReply != null) && postReply.getPost().isPresent(); } }; diff --git a/src/main/java/net/pterodactylus/sone/data/Profile.java b/src/main/java/net/pterodactylus/sone/data/Profile.java index 5a4fde0..4970cf9 100644 --- a/src/main/java/net/pterodactylus/sone/data/Profile.java +++ b/src/main/java/net/pterodactylus/sone/data/Profile.java @@ -325,10 +325,14 @@ public class Profile implements Fingerprintable { */ public Field addField(String fieldName) throws IllegalArgumentException { checkNotNull(fieldName, "fieldName must not be null"); - checkArgument(fieldName.length() > 0, "fieldName must not be empty"); - checkState(getFieldByName(fieldName) == null, "fieldName must be unique"); + if (fieldName.length() == 0) { + throw new EmptyFieldName(); + } + if (getFieldByName(fieldName) != null) { + throw new DuplicateField(); + } @SuppressWarnings("synthetic-access") - Field field = new Field().setName(fieldName); + Field field = new Field().setName(fieldName).setValue(""); fields.add(field); return field; } @@ -553,4 +557,18 @@ public class Profile implements Fingerprintable { } + /** + * Exception that signals the addition of a field with an empty name. + * + * @author David ‘Bombe’ Roden + */ + public static class EmptyFieldName extends IllegalArgumentException { } + + /** + * Exception that signals the addition of a field that already exists. + * + * @author David ‘Bombe’ Roden + */ + public static class DuplicateField extends IllegalArgumentException { } + } diff --git a/src/main/java/net/pterodactylus/sone/data/Reply.java b/src/main/java/net/pterodactylus/sone/data/Reply.java index c229d04..e9b7a1d 100644 --- a/src/main/java/net/pterodactylus/sone/data/Reply.java +++ b/src/main/java/net/pterodactylus/sone/data/Reply.java @@ -51,7 +51,7 @@ public interface Reply> extends Identified { */ @Override public boolean apply(Reply reply) { - return (reply == null) ? false : reply.getTime() <= System.currentTimeMillis(); + return (reply != null) && (reply.getTime() <= System.currentTimeMillis()); } }; diff --git a/src/main/java/net/pterodactylus/sone/data/Sone.java b/src/main/java/net/pterodactylus/sone/data/Sone.java index d7a7dee..04c5f39 100644 --- a/src/main/java/net/pterodactylus/sone/data/Sone.java +++ b/src/main/java/net/pterodactylus/sone/data/Sone.java @@ -23,17 +23,21 @@ import static net.pterodactylus.sone.data.Album.FLATTENER; import static net.pterodactylus.sone.data.Album.IMAGES; import java.util.Collection; +import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Set; -import net.pterodactylus.sone.core.Options; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + import net.pterodactylus.sone.freenet.wot.Identity; import net.pterodactylus.sone.freenet.wot.OwnIdentity; import net.pterodactylus.sone.template.SoneAccessor; import freenet.keys.FreenetURI; +import com.google.common.base.Function; import com.google.common.base.Predicate; import com.google.common.primitives.Ints; @@ -144,7 +148,7 @@ public interface Sone extends Identified, Fingerprintable, Comparable { @Override public boolean apply(Sone sone) { - return (sone == null) ? false : sone.getTime() != 0; + return (sone != null) && (sone.getTime() != 0); } }; @@ -153,7 +157,7 @@ public interface Sone extends Identified, Fingerprintable, Comparable { @Override public boolean apply(Sone sone) { - return (sone == null) ? false : sone.getIdentity() instanceof OwnIdentity; + return (sone != null) && (sone.getIdentity() instanceof OwnIdentity); } }; @@ -163,7 +167,35 @@ public interface Sone extends Identified, Fingerprintable, Comparable { @Override public boolean apply(Sone sone) { - return (sone == null) ? false : !sone.getRootAlbum().getAlbums().isEmpty(); + return (sone != null) && !sone.getRootAlbum().getAlbums().isEmpty(); + } + }; + + public static final Function toSoneXmlUri = + new Function() { + @Nonnull + @Override + public String apply(@Nullable Sone input) { + return input.getRequestUri() + .setMetaString(new String[] { "sone.xml" }) + .toString(); + } + }; + + public static final Function> toAllAlbums = new Function>() { + @Override + public List apply(@Nullable Sone sone) { + return (sone == null) ? Collections.emptyList() : FLATTENER.apply( + sone.getRootAlbum()); + } + }; + + public static final Function> toAllImages = new Function>() { + @Override + public List apply(@Nullable Sone sone) { + return (sone == null) ? Collections.emptyList() : + from(FLATTENER.apply(sone.getRootAlbum())) + .transformAndConcat(IMAGES).toList(); } }; @@ -175,18 +207,6 @@ public interface Sone extends Identified, Fingerprintable, Comparable { Identity getIdentity(); /** - * Sets the identity of this Sone. The {@link Identity#getId() ID} of the - * identity has to match this Sone’s {@link #getId()}. - * - * @param identity - * The identity of this Sone - * @return This Sone (for method chaining) - * @throws IllegalArgumentException - * if the ID of the identity does not match this Sone’s ID - */ - Sone setIdentity(Identity identity) throws IllegalArgumentException; - - /** * Returns the name of this Sone. * * @return The name of this Sone @@ -208,15 +228,6 @@ public interface Sone extends Identified, Fingerprintable, Comparable { FreenetURI getRequestUri(); /** - * Sets the request URI of this Sone. - * - * @param requestUri - * The request URI of this Sone - * @return This Sone (for method chaining) - */ - Sone setRequestUri(FreenetURI requestUri); - - /** * Returns the insert URI of this Sone. * * @return The insert URI of this Sone @@ -224,15 +235,6 @@ public interface Sone extends Identified, Fingerprintable, Comparable { FreenetURI getInsertUri(); /** - * Sets the insert URI of this Sone. - * - * @param insertUri - * The insert URI of this Sone - * @return This Sone (for method chaining) - */ - Sone setInsertUri(FreenetURI insertUri); - - /** * Returns the latest edition of this Sone. * * @return The latest edition of this Sone @@ -339,7 +341,7 @@ public interface Sone extends Identified, Fingerprintable, Comparable { * * @return The friend Sones of this Sone */ - List getFriends(); + Collection getFriends(); /** * Returns whether this Sone has the given Sone as a friend Sone. @@ -352,24 +354,6 @@ public interface Sone extends Identified, Fingerprintable, Comparable { boolean hasFriend(String friendSoneId); /** - * Adds the given Sone as a friend Sone. - * - * @param friendSone - * The friend Sone to add - * @return This Sone (for method chaining) - */ - Sone addFriend(String friendSone); - - /** - * Removes the given Sone as a friend Sone. - * - * @param friendSoneId - * The ID of the friend Sone to remove - * @return This Sone (for method chaining) - */ - Sone removeFriend(String friendSoneId); - - /** * Returns the list of posts of this Sone, sorted by time, newest first. * * @return All posts of this Sone @@ -535,7 +519,7 @@ public interface Sone extends Identified, Fingerprintable, Comparable { * * @return The options of this Sone */ - Options getOptions(); + SoneOptions getOptions(); /** * Sets the options of this Sone. @@ -544,6 +528,6 @@ public interface Sone extends Identified, Fingerprintable, Comparable { * The options of this Sone */ /* TODO - remove this method again, maybe add an option provider */ - void setOptions(Options options); + void setOptions(SoneOptions options); } diff --git a/src/main/java/net/pterodactylus/sone/data/SoneImpl.java b/src/main/java/net/pterodactylus/sone/data/SoneImpl.java deleted file mode 100644 index 880eb95..0000000 --- a/src/main/java/net/pterodactylus/sone/data/SoneImpl.java +++ /dev/null @@ -1,749 +0,0 @@ -/* - * Sone - SoneImpl.java - Copyright © 2010–2013 David Roden - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package net.pterodactylus.sone.data; - -import static com.google.common.base.Preconditions.checkNotNull; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.Set; -import java.util.concurrent.CopyOnWriteArraySet; -import java.util.logging.Level; -import java.util.logging.Logger; - -import net.pterodactylus.sone.core.Options; -import net.pterodactylus.sone.freenet.wot.Identity; -import net.pterodactylus.util.logging.Logging; - -import freenet.keys.FreenetURI; - -import com.google.common.hash.Hasher; -import com.google.common.hash.Hashing; - -/** - * {@link Sone} implementation. - *

- * Operations that modify the Sone need to synchronize on the Sone in question. - * - * @author David ‘Bombe’ Roden - */ -public class SoneImpl implements Sone { - - /** The logger. */ - private static final Logger logger = Logging.getLogger(SoneImpl.class); - - /** The ID of this Sone. */ - private final String id; - - /** Whether the Sone is local. */ - private final boolean local; - - /** The identity of this Sone. */ - private Identity identity; - - /** The URI under which the Sone is stored in Freenet. */ - private volatile FreenetURI requestUri; - - /** The URI used to insert a new version of this Sone. */ - /* This will be null for remote Sones! */ - private volatile FreenetURI insertUri; - - /** The latest edition of the Sone. */ - private volatile long latestEdition; - - /** The time of the last inserted update. */ - private volatile long time; - - /** The status of this Sone. */ - private volatile SoneStatus status = SoneStatus.unknown; - - /** The profile of this Sone. */ - private volatile Profile profile = new Profile(this); - - /** The client used by the Sone. */ - private volatile Client client; - - /** Whether this Sone is known. */ - private volatile boolean known; - - /** All friend Sones. */ - private final Set friendSones = new CopyOnWriteArraySet(); - - /** All posts. */ - private final Set posts = new CopyOnWriteArraySet(); - - /** All replies. */ - private final Set replies = new CopyOnWriteArraySet(); - - /** The IDs of all liked posts. */ - private final Set likedPostIds = new CopyOnWriteArraySet(); - - /** The IDs of all liked replies. */ - private final Set likedReplyIds = new CopyOnWriteArraySet(); - - /** The root album containing all albums. */ - private final Album rootAlbum = new AlbumImpl().setSone(this); - - /** Sone-specific options. */ - private Options options = new Options(); - - /** - * Creates a new Sone. - * - * @param id - * The ID of the Sone - * @param local - * {@code true} if the Sone is a local Sone, {@code false} otherwise - */ - public SoneImpl(String id, boolean local) { - this.id = id; - this.local = local; - } - - // - // ACCESSORS - // - - /** - * Returns the identity of this Sone. - * - * @return The identity of this Sone - */ - public String getId() { - return id; - } - - /** - * Returns the identity of this Sone. - * - * @return The identity of this Sone - */ - public Identity getIdentity() { - return identity; - } - - /** - * Sets the identity of this Sone. The {@link Identity#getId() ID} of the - * identity has to match this Sone’s {@link #getId()}. - * - * @param identity - * The identity of this Sone - * @return This Sone (for method chaining) - * @throws IllegalArgumentException - * if the ID of the identity does not match this Sone’s ID - */ - public SoneImpl setIdentity(Identity identity) throws IllegalArgumentException { - if (!identity.getId().equals(id)) { - throw new IllegalArgumentException("Identity’s ID does not match Sone’s ID!"); - } - this.identity = identity; - return this; - } - - /** - * Returns the name of this Sone. - * - * @return The name of this Sone - */ - public String getName() { - return (identity != null) ? identity.getNickname() : null; - } - - /** - * Returns whether this Sone is a local Sone. - * - * @return {@code true} if this Sone is a local Sone, {@code false} otherwise - */ - public boolean isLocal() { - return local; - } - - /** - * Returns the request URI of this Sone. - * - * @return The request URI of this Sone - */ - public FreenetURI getRequestUri() { - return (requestUri != null) ? requestUri.setSuggestedEdition(latestEdition) : null; - } - - /** - * Sets the request URI of this Sone. - * - * @param requestUri - * The request URI of this Sone - * @return This Sone (for method chaining) - */ - public Sone setRequestUri(FreenetURI requestUri) { - if (this.requestUri == null) { - this.requestUri = requestUri.setKeyType("USK").setDocName("Sone").setMetaString(new String[0]); - return this; - } - if (!this.requestUri.equalsKeypair(requestUri)) { - logger.log(Level.WARNING, String.format("Request URI %s tried to overwrite %s!", requestUri, this.requestUri)); - return this; - } - return this; - } - - /** - * Returns the insert URI of this Sone. - * - * @return The insert URI of this Sone - */ - public FreenetURI getInsertUri() { - return (insertUri != null) ? insertUri.setSuggestedEdition(latestEdition) : null; - } - - /** - * Sets the insert URI of this Sone. - * - * @param insertUri - * The insert URI of this Sone - * @return This Sone (for method chaining) - */ - public Sone setInsertUri(FreenetURI insertUri) { - if (this.insertUri == null) { - this.insertUri = insertUri.setKeyType("USK").setDocName("Sone").setMetaString(new String[0]); - return this; - } - if (!this.insertUri.equalsKeypair(insertUri)) { - logger.log(Level.WARNING, String.format("Request URI %s tried to overwrite %s!", insertUri, this.insertUri)); - return this; - } - return this; - } - - /** - * Returns the latest edition of this Sone. - * - * @return The latest edition of this Sone - */ - public long getLatestEdition() { - return latestEdition; - } - - /** - * Sets the latest edition of this Sone. If the given latest edition is not - * greater than the current latest edition, the latest edition of this Sone is - * not changed. - * - * @param latestEdition - * The latest edition of this Sone - */ - public void setLatestEdition(long latestEdition) { - if (!(latestEdition > this.latestEdition)) { - logger.log(Level.FINE, String.format("New latest edition %d is not greater than current latest edition %d!", latestEdition, this.latestEdition)); - return; - } - this.latestEdition = latestEdition; - } - - /** - * Return the time of the last inserted update of this Sone. - * - * @return The time of the update (in milliseconds since Jan 1, 1970 UTC) - */ - public long getTime() { - return time; - } - - /** - * Sets the time of the last inserted update of this Sone. - * - * @param time - * The time of the update (in milliseconds since Jan 1, 1970 UTC) - * @return This Sone (for method chaining) - */ - public Sone setTime(long time) { - this.time = time; - return this; - } - - /** - * Returns the status of this Sone. - * - * @return The status of this Sone - */ - public SoneStatus getStatus() { - return status; - } - - /** - * Sets the new status of this Sone. - * - * @param status - * The new status of this Sone - * @return This Sone - * @throws IllegalArgumentException - * if {@code status} is {@code null} - */ - public Sone setStatus(SoneStatus status) { - this.status = checkNotNull(status, "status must not be null"); - return this; - } - - /** - * Returns a copy of the profile. If you want to update values in the profile - * of this Sone, update the values in the returned {@link Profile} and use - * {@link #setProfile(Profile)} to change the profile in this Sone. - * - * @return A copy of the profile - */ - public Profile getProfile() { - return new Profile(profile); - } - - /** - * Sets the profile of this Sone. A copy of the given profile is stored so that - * subsequent modifications of the given profile are not reflected in this - * Sone! - * - * @param profile - * The profile to set - */ - public void setProfile(Profile profile) { - this.profile = new Profile(profile); - } - - /** - * Returns the client used by this Sone. - * - * @return The client used by this Sone, or {@code null} - */ - public Client getClient() { - return client; - } - - /** - * Sets the client used by this Sone. - * - * @param client - * The client used by this Sone, or {@code null} - * @return This Sone (for method chaining) - */ - public Sone setClient(Client client) { - this.client = client; - return this; - } - - /** - * Returns whether this Sone is known. - * - * @return {@code true} if this Sone is known, {@code false} otherwise - */ - public boolean isKnown() { - return known; - } - - /** - * Sets whether this Sone is known. - * - * @param known - * {@code true} if this Sone is known, {@code false} otherwise - * @return This Sone - */ - public Sone setKnown(boolean known) { - this.known = known; - return this; - } - - /** - * Returns all friend Sones of this Sone. - * - * @return The friend Sones of this Sone - */ - public List getFriends() { - List friends = new ArrayList(friendSones); - return friends; - } - - /** - * Returns whether this Sone has the given Sone as a friend Sone. - * - * @param friendSoneId - * The ID of the Sone to check for - * @return {@code true} if this Sone has the given Sone as a friend, {@code - * false} otherwise - */ - public boolean hasFriend(String friendSoneId) { - return friendSones.contains(friendSoneId); - } - - /** - * Adds the given Sone as a friend Sone. - * - * @param friendSone - * The friend Sone to add - * @return This Sone (for method chaining) - */ - public Sone addFriend(String friendSone) { - if (!friendSone.equals(id)) { - friendSones.add(friendSone); - } - return this; - } - - /** - * Removes the given Sone as a friend Sone. - * - * @param friendSoneId - * The ID of the friend Sone to remove - * @return This Sone (for method chaining) - */ - public Sone removeFriend(String friendSoneId) { - friendSones.remove(friendSoneId); - return this; - } - - /** - * Returns the list of posts of this Sone, sorted by time, newest first. - * - * @return All posts of this Sone - */ - public List getPosts() { - List sortedPosts; - synchronized (this) { - sortedPosts = new ArrayList(posts); - } - Collections.sort(sortedPosts, Post.TIME_COMPARATOR); - return sortedPosts; - } - - /** - * Sets all posts of this Sone at once. - * - * @param posts - * The new (and only) posts of this Sone - * @return This Sone (for method chaining) - */ - public Sone setPosts(Collection posts) { - synchronized (this) { - this.posts.clear(); - this.posts.addAll(posts); - } - return this; - } - - /** - * Adds the given post to this Sone. The post will not be added if its {@link - * Post#getSone() Sone} is not this Sone. - * - * @param post - * The post to add - */ - public void addPost(Post post) { - if (post.getSone().equals(this) && posts.add(post)) { - logger.log(Level.FINEST, String.format("Adding %s to “%s”.", post, getName())); - } - } - - /** - * Removes the given post from this Sone. - * - * @param post - * The post to remove - */ - public void removePost(Post post) { - if (post.getSone().equals(this)) { - posts.remove(post); - } - } - - /** - * Returns all replies this Sone made. - * - * @return All replies this Sone made - */ - public Set getReplies() { - return Collections.unmodifiableSet(replies); - } - - /** - * Sets all replies of this Sone at once. - * - * @param replies - * The new (and only) replies of this Sone - * @return This Sone (for method chaining) - */ - public Sone setReplies(Collection replies) { - this.replies.clear(); - this.replies.addAll(replies); - return this; - } - - /** - * Adds a reply to this Sone. If the given reply was not made by this Sone, - * nothing is added to this Sone. - * - * @param reply - * The reply to add - */ - public void addReply(PostReply reply) { - if (reply.getSone().equals(this)) { - replies.add(reply); - } - } - - /** - * Removes a reply from this Sone. - * - * @param reply - * The reply to remove - */ - public void removeReply(PostReply reply) { - if (reply.getSone().equals(this)) { - replies.remove(reply); - } - } - - /** - * Returns the IDs of all liked posts. - * - * @return All liked posts’ IDs - */ - public Set getLikedPostIds() { - return Collections.unmodifiableSet(likedPostIds); - } - - /** - * Sets the IDs of all liked posts. - * - * @param likedPostIds - * All liked posts’ IDs - * @return This Sone (for method chaining) - */ - public Sone setLikePostIds(Set likedPostIds) { - this.likedPostIds.clear(); - this.likedPostIds.addAll(likedPostIds); - return this; - } - - /** - * Checks whether the given post ID is liked by this Sone. - * - * @param postId - * The ID of the post - * @return {@code true} if this Sone likes the given post, {@code false} - * otherwise - */ - public boolean isLikedPostId(String postId) { - return likedPostIds.contains(postId); - } - - /** - * Adds the given post ID to the list of posts this Sone likes. - * - * @param postId - * The ID of the post - * @return This Sone (for method chaining) - */ - public Sone addLikedPostId(String postId) { - likedPostIds.add(postId); - return this; - } - - /** - * Removes the given post ID from the list of posts this Sone likes. - * - * @param postId - * The ID of the post - * @return This Sone (for method chaining) - */ - public Sone removeLikedPostId(String postId) { - likedPostIds.remove(postId); - return this; - } - - /** - * Returns the IDs of all liked replies. - * - * @return All liked replies’ IDs - */ - public Set getLikedReplyIds() { - return Collections.unmodifiableSet(likedReplyIds); - } - - /** - * Sets the IDs of all liked replies. - * - * @param likedReplyIds - * All liked replies’ IDs - * @return This Sone (for method chaining) - */ - public Sone setLikeReplyIds(Set likedReplyIds) { - this.likedReplyIds.clear(); - this.likedReplyIds.addAll(likedReplyIds); - return this; - } - - /** - * Checks whether the given reply ID is liked by this Sone. - * - * @param replyId - * The ID of the reply - * @return {@code true} if this Sone likes the given reply, {@code false} - * otherwise - */ - public boolean isLikedReplyId(String replyId) { - return likedReplyIds.contains(replyId); - } - - /** - * Adds the given reply ID to the list of replies this Sone likes. - * - * @param replyId - * The ID of the reply - * @return This Sone (for method chaining) - */ - public Sone addLikedReplyId(String replyId) { - likedReplyIds.add(replyId); - return this; - } - - /** - * Removes the given post ID from the list of replies this Sone likes. - * - * @param replyId - * The ID of the reply - * @return This Sone (for method chaining) - */ - public Sone removeLikedReplyId(String replyId) { - likedReplyIds.remove(replyId); - return this; - } - - /** - * Returns the root album that contains all visible albums of this Sone. - * - * @return The root album of this Sone - */ - public Album getRootAlbum() { - return rootAlbum; - } - - /** - * Returns Sone-specific options. - * - * @return The options of this Sone - */ - public Options getOptions() { - return options; - } - - /** - * Sets the options of this Sone. - * - * @param options - * The options of this Sone - */ - /* TODO - remove this method again, maybe add an option provider */ - public void setOptions(Options options) { - this.options = options; - } - - // - // FINGERPRINTABLE METHODS - // - - /** {@inheritDoc} */ - @Override - public synchronized String getFingerprint() { - Hasher hash = Hashing.sha256().newHasher(); - hash.putString(profile.getFingerprint()); - - hash.putString("Posts("); - for (Post post : getPosts()) { - hash.putString("Post(").putString(post.getId()).putString(")"); - } - hash.putString(")"); - - List replies = new ArrayList(getReplies()); - Collections.sort(replies, Reply.TIME_COMPARATOR); - hash.putString("Replies("); - for (PostReply reply : replies) { - hash.putString("Reply(").putString(reply.getId()).putString(")"); - } - hash.putString(")"); - - List likedPostIds = new ArrayList(getLikedPostIds()); - Collections.sort(likedPostIds); - hash.putString("LikedPosts("); - for (String likedPostId : likedPostIds) { - hash.putString("Post(").putString(likedPostId).putString(")"); - } - hash.putString(")"); - - List likedReplyIds = new ArrayList(getLikedReplyIds()); - Collections.sort(likedReplyIds); - hash.putString("LikedReplies("); - for (String likedReplyId : likedReplyIds) { - hash.putString("Reply(").putString(likedReplyId).putString(")"); - } - hash.putString(")"); - - hash.putString("Albums("); - for (Album album : rootAlbum.getAlbums()) { - if (!Album.NOT_EMPTY.apply(album)) { - continue; - } - hash.putString(album.getFingerprint()); - } - hash.putString(")"); - - return hash.hash().toString(); - } - - // - // INTERFACE Comparable - // - - /** {@inheritDoc} */ - @Override - public int compareTo(Sone sone) { - return NICE_NAME_COMPARATOR.compare(this, sone); - } - - // - // OBJECT METHODS - // - - /** {@inheritDoc} */ - @Override - public int hashCode() { - return id.hashCode(); - } - - /** {@inheritDoc} */ - @Override - public boolean equals(Object object) { - if (!(object instanceof Sone)) { - return false; - } - return ((Sone) object).getId().equals(id); - } - - /** {@inheritDoc} */ - @Override - public String toString() { - return getClass().getName() + "[identity=" + identity + ",requestUri=" + requestUri + ",insertUri(" + String.valueOf(insertUri).length() + "),friends(" + friendSones.size() + "),posts(" + posts.size() + "),replies(" + replies.size() + "),albums(" + getRootAlbum().getAlbums().size() + ")]"; - } - -} diff --git a/src/main/java/net/pterodactylus/sone/data/SoneOptions.java b/src/main/java/net/pterodactylus/sone/data/SoneOptions.java new file mode 100644 index 0000000..819e94a --- /dev/null +++ b/src/main/java/net/pterodactylus/sone/data/SoneOptions.java @@ -0,0 +1,108 @@ +package net.pterodactylus.sone.data; + +import static net.pterodactylus.sone.data.Sone.ShowCustomAvatars.NEVER; + +import net.pterodactylus.sone.data.Sone.ShowCustomAvatars; + +/** + * All Sone-specific options. + * + * @author David ‘Bombe’ Roden + */ +public interface SoneOptions { + + boolean isAutoFollow(); + void setAutoFollow(boolean autoFollow); + + boolean isSoneInsertNotificationEnabled(); + void setSoneInsertNotificationEnabled(boolean soneInsertNotificationEnabled); + + boolean isShowNewSoneNotifications(); + void setShowNewSoneNotifications(boolean showNewSoneNotifications); + + boolean isShowNewPostNotifications(); + void setShowNewPostNotifications(boolean showNewPostNotifications); + + boolean isShowNewReplyNotifications(); + void setShowNewReplyNotifications(boolean showNewReplyNotifications); + + ShowCustomAvatars getShowCustomAvatars(); + void setShowCustomAvatars(ShowCustomAvatars showCustomAvatars); + + /** + * {@link SoneOptions} implementation. + * + * @author David ‘Bombe’ Roden + */ + public class DefaultSoneOptions implements SoneOptions { + + private boolean autoFollow = false; + private boolean soneInsertNotificationsEnabled = false; + private boolean showNewSoneNotifications = true; + private boolean showNewPostNotifications = true; + private boolean showNewReplyNotifications = true; + private ShowCustomAvatars showCustomAvatars = NEVER; + + @Override + public boolean isAutoFollow() { + return autoFollow; + } + + @Override + public void setAutoFollow(boolean autoFollow) { + this.autoFollow = autoFollow; + } + + @Override + public boolean isSoneInsertNotificationEnabled() { + return soneInsertNotificationsEnabled; + } + + @Override + public void setSoneInsertNotificationEnabled(boolean soneInsertNotificationEnabled) { + this.soneInsertNotificationsEnabled = soneInsertNotificationEnabled; + } + + @Override + public boolean isShowNewSoneNotifications() { + return showNewSoneNotifications; + } + + @Override + public void setShowNewSoneNotifications(boolean showNewSoneNotifications) { + this.showNewSoneNotifications = showNewSoneNotifications; + } + + @Override + public boolean isShowNewPostNotifications() { + return showNewPostNotifications; + } + + @Override + public void setShowNewPostNotifications(boolean showNewPostNotifications) { + this.showNewPostNotifications = showNewPostNotifications; + } + + @Override + public boolean isShowNewReplyNotifications() { + return showNewReplyNotifications; + } + + @Override + public void setShowNewReplyNotifications(boolean showNewReplyNotifications) { + this.showNewReplyNotifications = showNewReplyNotifications; + } + + @Override + public ShowCustomAvatars getShowCustomAvatars() { + return showCustomAvatars; + } + + @Override + public void setShowCustomAvatars(ShowCustomAvatars showCustomAvatars) { + this.showCustomAvatars = showCustomAvatars; + } + + } + +} diff --git a/src/main/java/net/pterodactylus/sone/data/impl/AbstractAlbumBuilder.java b/src/main/java/net/pterodactylus/sone/data/impl/AbstractAlbumBuilder.java index 312c795..8e15b6f 100644 --- a/src/main/java/net/pterodactylus/sone/data/impl/AbstractAlbumBuilder.java +++ b/src/main/java/net/pterodactylus/sone/data/impl/AbstractAlbumBuilder.java @@ -19,6 +19,7 @@ package net.pterodactylus.sone.data.impl; import static com.google.common.base.Preconditions.checkState; +import net.pterodactylus.sone.data.Sone; import net.pterodactylus.sone.database.AlbumBuilder; /** @@ -34,6 +35,7 @@ public abstract class AbstractAlbumBuilder implements AlbumBuilder { /** The ID of the album to create. */ protected String id; + protected Sone sone; @Override public AlbumBuilder randomId() { @@ -47,6 +49,11 @@ public abstract class AbstractAlbumBuilder implements AlbumBuilder { return this; } + public AlbumBuilder by(Sone sone) { + this.sone = sone; + return this; + } + // // PROTECTED METHODS // @@ -59,6 +66,7 @@ public abstract class AbstractAlbumBuilder implements AlbumBuilder { */ protected void validate() throws IllegalStateException { checkState((randomId && (id == null)) || (!randomId && (id != null)), "exactly one of random ID or custom ID must be set"); + checkState(sone != null, "Sone must not be null"); } } diff --git a/src/main/java/net/pterodactylus/sone/data/impl/AbstractSoneBuilder.java b/src/main/java/net/pterodactylus/sone/data/impl/AbstractSoneBuilder.java new file mode 100644 index 0000000..a214677 --- /dev/null +++ b/src/main/java/net/pterodactylus/sone/data/impl/AbstractSoneBuilder.java @@ -0,0 +1,37 @@ +package net.pterodactylus.sone.data.impl; + +import static com.google.common.base.Preconditions.checkState; + +import net.pterodactylus.sone.database.SoneBuilder; +import net.pterodactylus.sone.freenet.wot.Identity; +import net.pterodactylus.sone.freenet.wot.OwnIdentity; + +/** + * Abstract {@link SoneBuilder} implementation. + * + * @author David ‘Bombe’ Roden + */ +public abstract class AbstractSoneBuilder implements SoneBuilder { + + protected Identity identity; + protected boolean local; + + @Override + public SoneBuilder from(Identity identity) { + this.identity = identity; + return this; + } + + @Override + public SoneBuilder local() { + this.local = true; + return this; + } + + protected void validate() throws IllegalStateException { + checkState(identity != null, "identity must not be null"); + checkState(!local || (identity instanceof OwnIdentity), + "can not create local Sone from remote identity"); + } + +} diff --git a/src/main/java/net/pterodactylus/sone/data/impl/AlbumBuilderImpl.java b/src/main/java/net/pterodactylus/sone/data/impl/AlbumBuilderImpl.java index 3403a62..df61e36 100644 --- a/src/main/java/net/pterodactylus/sone/data/impl/AlbumBuilderImpl.java +++ b/src/main/java/net/pterodactylus/sone/data/impl/AlbumBuilderImpl.java @@ -18,7 +18,6 @@ package net.pterodactylus.sone.data.impl; import net.pterodactylus.sone.data.Album; -import net.pterodactylus.sone.data.AlbumImpl; import net.pterodactylus.sone.database.AlbumBuilder; /** @@ -31,7 +30,7 @@ public class AlbumBuilderImpl extends AbstractAlbumBuilder { @Override public Album build() throws IllegalStateException { validate(); - return randomId ? new AlbumImpl() : new AlbumImpl(id); + return randomId ? new AlbumImpl(sone) : new AlbumImpl(sone, id); } } diff --git a/src/main/java/net/pterodactylus/sone/data/impl/AlbumImpl.java b/src/main/java/net/pterodactylus/sone/data/impl/AlbumImpl.java new file mode 100644 index 0000000..c569e0a --- /dev/null +++ b/src/main/java/net/pterodactylus/sone/data/impl/AlbumImpl.java @@ -0,0 +1,379 @@ +/* + * Sone - Album.java - Copyright © 2011–2013 David Roden + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.pterodactylus.sone.data.impl; + +import static com.google.common.base.Optional.absent; +import static com.google.common.base.Optional.fromNullable; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import net.pterodactylus.sone.data.Album; +import net.pterodactylus.sone.data.Image; +import net.pterodactylus.sone.data.Sone; + +import com.google.common.base.Function; +import com.google.common.base.Optional; +import com.google.common.base.Predicates; +import com.google.common.collect.Collections2; +import com.google.common.hash.Hasher; +import com.google.common.hash.Hashing; + +/** + * Container for images that can also contain nested {@link AlbumImpl}s. + * + * @author David ‘Bombe’ Roden + */ +public class AlbumImpl implements Album { + + /** The ID of this album. */ + private final String id; + + /** The Sone this album belongs to. */ + private final Sone sone; + + /** Nested albums. */ + private final List albums = new ArrayList(); + + /** The image IDs in order. */ + private final List imageIds = new ArrayList(); + + /** The images in this album. */ + private final Map images = new HashMap(); + + /** The parent album. */ + private Album parent; + + /** The title of this album. */ + private String title; + + /** The description of this album. */ + private String description; + + /** The ID of the album picture. */ + private String albumImage; + + /** Creates a new album with a random ID. */ + public AlbumImpl(Sone sone) { + this(sone, UUID.randomUUID().toString()); + } + + /** + * Creates a new album with the given ID. + * + * @param id + * The ID of the album + */ + public AlbumImpl(Sone sone, String id) { + this.sone = checkNotNull(sone, "Sone must not be null"); + this.id = checkNotNull(id, "id must not be null"); + } + + // + // ACCESSORS + // + + @Override + public String getId() { + return id; + } + + @Override + public Sone getSone() { + return sone; + } + + @Override + public List getAlbums() { + return new ArrayList(albums); + } + + @Override + public void addAlbum(Album album) { + checkNotNull(album, "album must not be null"); + checkArgument(album.getSone().equals(sone), "album must belong to the same Sone as this album"); + album.setParent(this); + if (!albums.contains(album)) { + albums.add(album); + } + } + + @Override + public void removeAlbum(Album album) { + checkNotNull(album, "album must not be null"); + checkArgument(album.getSone().equals(sone), "album must belong this album’s Sone"); + checkArgument(equals(album.getParent()), "album must belong to this album"); + albums.remove(album); + album.removeParent(); + } + + @Override + public Album moveAlbumUp(Album album) { + checkNotNull(album, "album must not be null"); + checkArgument(album.getSone().equals(sone), "album must belong to the same Sone as this album"); + checkArgument(equals(album.getParent()), "album must belong to this album"); + int oldIndex = albums.indexOf(album); + if (oldIndex <= 0) { + return null; + } + albums.remove(oldIndex); + albums.add(oldIndex - 1, album); + return albums.get(oldIndex); + } + + @Override + public Album moveAlbumDown(Album album) { + checkNotNull(album, "album must not be null"); + checkArgument(album.getSone().equals(sone), "album must belong to the same Sone as this album"); + checkArgument(equals(album.getParent()), "album must belong to this album"); + int oldIndex = albums.indexOf(album); + if ((oldIndex < 0) || (oldIndex >= (albums.size() - 1))) { + return null; + } + albums.remove(oldIndex); + albums.add(oldIndex + 1, album); + return albums.get(oldIndex); + } + + @Override + public List getImages() { + return new ArrayList(Collections2.filter(Collections2.transform(imageIds, new Function() { + + @Override + @SuppressWarnings("synthetic-access") + public Image apply(String imageId) { + return images.get(imageId); + } + }), Predicates.notNull())); + } + + @Override + public void addImage(Image image) { + checkNotNull(image, "image must not be null"); + checkNotNull(image.getSone(), "image must have an owner"); + checkArgument(image.getSone().equals(sone), "image must belong to the same Sone as this album"); + if (image.getAlbum() != null) { + image.getAlbum().removeImage(image); + } + image.setAlbum(this); + if (imageIds.isEmpty() && (albumImage == null)) { + albumImage = image.getId(); + } + if (!imageIds.contains(image.getId())) { + imageIds.add(image.getId()); + images.put(image.getId(), image); + } + } + + @Override + public void removeImage(Image image) { + checkNotNull(image, "image must not be null"); + checkNotNull(image.getSone(), "image must have an owner"); + checkArgument(image.getSone().equals(sone), "image must belong to the same Sone as this album"); + imageIds.remove(image.getId()); + images.remove(image.getId()); + if (image.getId().equals(albumImage)) { + if (images.isEmpty()) { + albumImage = null; + } else { + albumImage = images.values().iterator().next().getId(); + } + } + } + + @Override + public Image moveImageUp(Image image) { + checkNotNull(image, "image must not be null"); + checkNotNull(image.getSone(), "image must have an owner"); + checkArgument(image.getSone().equals(sone), "image must belong to the same Sone as this album"); + checkArgument(image.getAlbum().equals(this), "image must belong to this album"); + int oldIndex = imageIds.indexOf(image.getId()); + if (oldIndex <= 0) { + return null; + } + imageIds.remove(image.getId()); + imageIds.add(oldIndex - 1, image.getId()); + return images.get(imageIds.get(oldIndex)); + } + + @Override + public Image moveImageDown(Image image) { + checkNotNull(image, "image must not be null"); + checkNotNull(image.getSone(), "image must have an owner"); + checkArgument(image.getSone().equals(sone), "image must belong to the same Sone as this album"); + checkArgument(image.getAlbum().equals(this), "image must belong to this album"); + int oldIndex = imageIds.indexOf(image.getId()); + if ((oldIndex == -1) || (oldIndex >= (imageIds.size() - 1))) { + return null; + } + imageIds.remove(image.getId()); + imageIds.add(oldIndex + 1, image.getId()); + return images.get(imageIds.get(oldIndex)); + } + + @Override + public Image getAlbumImage() { + if (albumImage == null) { + return null; + } + return Optional.fromNullable(images.get(albumImage)).or(images.values().iterator().next()); + } + + @Override + public boolean isEmpty() { + return albums.isEmpty() && images.isEmpty(); + } + + @Override + public boolean isRoot() { + return parent == null; + } + + @Override + public Album getParent() { + return parent; + } + + @Override + public Album setParent(Album parent) { + this.parent = checkNotNull(parent, "parent must not be null"); + return this; + } + + @Override + public Album removeParent() { + this.parent = null; + return this; + } + + @Override + public String getTitle() { + return title; + } + + @Override + public String getDescription() { + return description; + } + + @Override + public Modifier modify() throws IllegalStateException { + // TODO: reenable check for local Sones + return new Modifier() { + private Optional title = absent(); + + private Optional description = absent(); + + private Optional albumImage = absent(); + + @Override + public Modifier setTitle(String title) { + this.title = fromNullable(title); + return this; + } + + @Override + public Modifier setDescription(String description) { + this.description = fromNullable(description); + return this; + } + + @Override + public Modifier setAlbumImage(String imageId) { + this.albumImage = fromNullable(imageId); + return this; + } + + @Override + public Album update() throws IllegalStateException { + if (title.isPresent() && title.get().trim().isEmpty()) { + throw new AlbumTitleMustNotBeEmpty(); + } + if (title.isPresent()) { + AlbumImpl.this.title = title.get(); + } + if (description.isPresent()) { + AlbumImpl.this.description = description.get(); + } + if (albumImage.isPresent()) { + AlbumImpl.this.albumImage = albumImage.get(); + } + return AlbumImpl.this; + } + }; + } + + // + // FINGERPRINTABLE METHODS + // + + @Override + public String getFingerprint() { + Hasher hash = Hashing.sha256().newHasher(); + hash.putString("Album("); + hash.putString("ID(").putString(id).putString(")"); + hash.putString("Title(").putString(title).putString(")"); + hash.putString("Description(").putString(description).putString(")"); + if (albumImage != null) { + hash.putString("AlbumImage(").putString(albumImage).putString(")"); + } + + /* add nested albums. */ + hash.putString("Albums("); + for (Album album : albums) { + hash.putString(album.getFingerprint()); + } + hash.putString(")"); + + /* add images. */ + hash.putString("Images("); + for (Image image : getImages()) { + if (image.isInserted()) { + hash.putString(image.getFingerprint()); + } + } + hash.putString(")"); + + hash.putString(")"); + return hash.hash().toString(); + } + + // + // OBJECT METHODS + // + + @Override + public int hashCode() { + return id.hashCode(); + } + + @Override + public boolean equals(Object object) { + if (!(object instanceof AlbumImpl)) { + return false; + } + AlbumImpl album = (AlbumImpl) object; + return id.equals(album.id); + } + +} diff --git a/src/main/java/net/pterodactylus/sone/data/impl/IdOnlySone.java b/src/main/java/net/pterodactylus/sone/data/impl/IdOnlySone.java new file mode 100644 index 0000000..0ef220b --- /dev/null +++ b/src/main/java/net/pterodactylus/sone/data/impl/IdOnlySone.java @@ -0,0 +1,243 @@ +package net.pterodactylus.sone.data.impl; + +import static java.util.Collections.emptyList; +import static java.util.Collections.emptySet; + +import java.util.Collection; +import java.util.List; +import java.util.Set; + +import net.pterodactylus.sone.data.Album; +import net.pterodactylus.sone.data.Client; +import net.pterodactylus.sone.data.Post; +import net.pterodactylus.sone.data.PostReply; +import net.pterodactylus.sone.data.Profile; +import net.pterodactylus.sone.data.Sone; +import net.pterodactylus.sone.data.SoneOptions; +import net.pterodactylus.sone.freenet.wot.Identity; + +import freenet.keys.FreenetURI; + +/** + * {@link Sone} implementation that only stores the ID of a Sone and returns + * {@code null}, {@code 0}, or empty collections where appropriate. + * + * @author David ‘Bombe’ Roden + */ +public class IdOnlySone implements Sone { + + private final String id; + + public IdOnlySone(String id) { + this.id = id; + } + + @Override + public Identity getIdentity() { + return null; + } + + @Override + public String getName() { + return id; + } + + @Override + public boolean isLocal() { + return false; + } + + @Override + public FreenetURI getRequestUri() { + return null; + } + + @Override + public FreenetURI getInsertUri() { + return null; + } + + @Override + public long getLatestEdition() { + return 0; + } + + @Override + public void setLatestEdition(long latestEdition) { + } + + @Override + public long getTime() { + return 0; + } + + @Override + public Sone setTime(long time) { + return null; + } + + @Override + public SoneStatus getStatus() { + return null; + } + + @Override + public Sone setStatus(SoneStatus status) { + return null; + } + + @Override + public Profile getProfile() { + return new Profile(this); + } + + @Override + public void setProfile(Profile profile) { + } + + @Override + public Client getClient() { + return null; + } + + @Override + public Sone setClient(Client client) { + return null; + } + + @Override + public boolean isKnown() { + return false; + } + + @Override + public Sone setKnown(boolean known) { + return null; + } + + @Override + public List getFriends() { + return emptyList(); + } + + @Override + public boolean hasFriend(String friendSoneId) { + return false; + } + + @Override + public List getPosts() { + return emptyList(); + } + + @Override + public Sone setPosts(Collection posts) { + return this; + } + + @Override + public void addPost(Post post) { + } + + @Override + public void removePost(Post post) { + } + + @Override + public Set getReplies() { + return emptySet(); + } + + @Override + public Sone setReplies(Collection replies) { + return this; + } + + @Override + public void addReply(PostReply reply) { + } + + @Override + public void removeReply(PostReply reply) { + } + + @Override + public Set getLikedPostIds() { + return emptySet(); + } + + @Override + public Sone setLikePostIds(Set likedPostIds) { + return this; + } + + @Override + public boolean isLikedPostId(String postId) { + return false; + } + + @Override + public Sone addLikedPostId(String postId) { + return this; + } + + @Override + public Sone removeLikedPostId(String postId) { + return this; + } + + @Override + public Set getLikedReplyIds() { + return emptySet(); + } + + @Override + public Sone setLikeReplyIds(Set likedReplyIds) { + return this; + } + + @Override + public boolean isLikedReplyId(String replyId) { + return false; + } + + @Override + public Sone addLikedReplyId(String replyId) { + return this; + } + + @Override + public Sone removeLikedReplyId(String replyId) { + return this; + } + + @Override + public Album getRootAlbum() { + return null; + } + + @Override + public SoneOptions getOptions() { + return null; + } + + @Override + public void setOptions(SoneOptions options) { + } + + @Override + public int compareTo(Sone o) { + return 0; + } + + @Override + public String getFingerprint() { + return null; + } + + @Override + public String getId() { + return id; + } + +} diff --git a/src/main/java/net/pterodactylus/sone/data/impl/ImageBuilderImpl.java b/src/main/java/net/pterodactylus/sone/data/impl/ImageBuilderImpl.java index 870b5d7..ba7d75f 100644 --- a/src/main/java/net/pterodactylus/sone/data/impl/ImageBuilderImpl.java +++ b/src/main/java/net/pterodactylus/sone/data/impl/ImageBuilderImpl.java @@ -18,7 +18,6 @@ package net.pterodactylus.sone.data.impl; import net.pterodactylus.sone.data.Image; -import net.pterodactylus.sone.data.ImageImpl; import net.pterodactylus.sone.database.ImageBuilder; /** diff --git a/src/main/java/net/pterodactylus/sone/data/impl/ImageImpl.java b/src/main/java/net/pterodactylus/sone/data/impl/ImageImpl.java new file mode 100644 index 0000000..2df98b1 --- /dev/null +++ b/src/main/java/net/pterodactylus/sone/data/impl/ImageImpl.java @@ -0,0 +1,278 @@ +/* + * Sone - ImageImpl.java - Copyright © 2011–2013 David Roden + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package net.pterodactylus.sone.data.impl; + +import static com.google.common.base.Optional.absent; +import static com.google.common.base.Optional.fromNullable; +import static com.google.common.base.Optional.of; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + +import java.util.UUID; + +import net.pterodactylus.sone.data.Album; +import net.pterodactylus.sone.data.Image; +import net.pterodactylus.sone.data.Sone; + +import com.google.common.base.Optional; +import com.google.common.hash.Hasher; +import com.google.common.hash.Hashing; + +/** + * Container for image metadata. + * + * @author David ‘Bombe’ Roden + */ +public class ImageImpl implements Image { + + /** The ID of the image. */ + private final String id; + + /** The Sone the image belongs to. */ + private Sone sone; + + /** The album this image belongs to. */ + private Album album; + + /** The request key of the image. */ + private String key; + + /** The creation time of the image. */ + private long creationTime; + + /** The width of the image. */ + private int width; + + /** The height of the image. */ + private int height; + + /** The title of the image. */ + private String title; + + /** The description of the image. */ + private String description; + + /** Creates a new image with a random ID. */ + public ImageImpl() { + this(UUID.randomUUID().toString()); + this.creationTime = System.currentTimeMillis(); + } + + /** + * Creates a new image. + * + * @param id + * The ID of the image + */ + public ImageImpl(String id) { + this.id = checkNotNull(id, "id must not be null"); + } + + // + // ACCESSORS + // + + @Override + public String getId() { + return id; + } + + @Override + public Sone getSone() { + return sone; + } + + @Override + public Album getAlbum() { + return album; + } + + @Override + public Image setAlbum(Album album) { + checkNotNull(album, "album must not be null"); + checkNotNull(album.getSone().equals(getSone()), "album must belong to the same Sone as this image"); + this.album = album; + return this; + } + + @Override + public String getKey() { + return key; + } + + @Override + public boolean isInserted() { + return key != null; + } + + @Override + public long getCreationTime() { + return creationTime; + } + + @Override + public int getWidth() { + return width; + } + + @Override + public int getHeight() { + return height; + } + + @Override + public String getTitle() { + return title; + } + + @Override + public String getDescription() { + return description; + } + + public Modifier modify() throws IllegalStateException { + // TODO: reenable check for local images + return new Modifier() { + private Optional sone = absent(); + + private Optional creationTime = absent(); + + private Optional key = absent(); + + private Optional title = absent(); + + private Optional description = absent(); + + private Optional width = absent(); + + private Optional height = absent(); + + @Override + public Modifier setSone(Sone sone) { + this.sone = fromNullable(sone); + return this; + } + + @Override + public Modifier setCreationTime(long creationTime) { + this.creationTime = of(creationTime); + return this; + } + + @Override + public Modifier setKey(String key) { + this.key = fromNullable(key); + return this; + } + + @Override + public Modifier setTitle(String title) { + this.title = fromNullable(title); + return this; + } + + @Override + public Modifier setDescription(String description) { + this.description = fromNullable(description); + return this; + } + + @Override + public Modifier setWidth(int width) { + this.width = of(width); + return this; + } + + @Override + public Modifier setHeight(int height) { + this.height = of(height); + return this; + } + + @Override + public Image update() throws IllegalStateException { + checkState(!sone.isPresent() || (ImageImpl.this.sone == null) || sone.get().equals(ImageImpl.this.sone), "can not change Sone once set"); + checkState(!creationTime.isPresent() || ((ImageImpl.this.creationTime == 0) || (ImageImpl.this.creationTime == creationTime.get())), "can not change creation time once set"); + checkState(!key.isPresent() || (ImageImpl.this.key == null) || key.get().equals(ImageImpl.this.key), "can not change key once set"); + if (title.isPresent() && title.get().trim().isEmpty()) { + throw new ImageTitleMustNotBeEmpty(); + } + checkState(!width.isPresent() || (ImageImpl.this.width == 0) || width.get().equals(ImageImpl.this.width), "can not change width once set"); + checkState(!height.isPresent() || (ImageImpl.this.height == 0) || height.get().equals(ImageImpl.this.height), "can not change height once set"); + + if (sone.isPresent()) { + ImageImpl.this.sone = sone.get(); + } + if (creationTime.isPresent()) { + ImageImpl.this.creationTime = creationTime.get(); + } + if (key.isPresent()) { + ImageImpl.this.key = key.get(); + } + if (title.isPresent()) { + ImageImpl.this.title = title.get(); + } + if (description.isPresent()) { + ImageImpl.this.description = description.get(); + } + if (width.isPresent()) { + ImageImpl.this.width = width.get(); + } + if (height.isPresent()) { + ImageImpl.this.height = height.get(); + } + + return ImageImpl.this; + } + }; + } + + // + // FINGERPRINTABLE METHODS + // + + @Override + public String getFingerprint() { + Hasher hash = Hashing.sha256().newHasher(); + hash.putString("Image("); + hash.putString("ID(").putString(id).putString(")"); + hash.putString("Title(").putString(title).putString(")"); + hash.putString("Description(").putString(description).putString(")"); + hash.putString(")"); + return hash.hash().toString(); + } + + // + // OBJECT METHODS + // + + /** {@inheritDoc} */ + @Override + public int hashCode() { + return id.hashCode(); + } + + /** {@inheritDoc} */ + @Override + public boolean equals(Object object) { + if (!(object instanceof ImageImpl)) { + return false; + } + return ((ImageImpl) object).id.equals(id); + } + +} diff --git a/src/main/java/net/pterodactylus/sone/data/impl/PostImpl.java b/src/main/java/net/pterodactylus/sone/data/impl/PostImpl.java index 2d25715..9dcd7d0 100644 --- a/src/main/java/net/pterodactylus/sone/data/impl/PostImpl.java +++ b/src/main/java/net/pterodactylus/sone/data/impl/PostImpl.java @@ -17,8 +17,6 @@ package net.pterodactylus.sone.data.impl; -import java.util.UUID; - import net.pterodactylus.sone.data.Post; import net.pterodactylus.sone.data.Sone; import net.pterodactylus.sone.database.SoneProvider; @@ -37,7 +35,7 @@ public class PostImpl implements Post { private final SoneProvider soneProvider; /** The GUID of the post. */ - private final UUID id; + private final String id; /** The ID of the owning Sone. */ private final String soneId; @@ -72,7 +70,7 @@ public class PostImpl implements Post { */ public PostImpl(SoneProvider soneProvider, String id, String soneId, String recipientId, long time, String text) { this.soneProvider = soneProvider; - this.id = UUID.fromString(id); + this.id = id; this.soneId = soneId; this.recipientId = recipientId; this.time = time; @@ -88,7 +86,12 @@ public class PostImpl implements Post { */ @Override public String getId() { - return id.toString(); + return id; + } + + @Override + public boolean isLoaded() { + return true; } /** diff --git a/src/main/java/net/pterodactylus/sone/data/impl/SoneImpl.java b/src/main/java/net/pterodactylus/sone/data/impl/SoneImpl.java new file mode 100644 index 0000000..2fdd40c --- /dev/null +++ b/src/main/java/net/pterodactylus/sone/data/impl/SoneImpl.java @@ -0,0 +1,694 @@ +/* + * Sone - SoneImpl.java - Copyright © 2010–2013 David Roden + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.pterodactylus.sone.data.impl; + +import static com.google.common.base.Preconditions.checkNotNull; +import static java.lang.String.format; +import static java.util.logging.Logger.getLogger; + +import java.net.MalformedURLException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArraySet; +import java.util.logging.Level; +import java.util.logging.Logger; + +import net.pterodactylus.sone.data.Album; +import net.pterodactylus.sone.data.Client; +import net.pterodactylus.sone.data.Post; +import net.pterodactylus.sone.data.PostReply; +import net.pterodactylus.sone.data.Profile; +import net.pterodactylus.sone.data.Reply; +import net.pterodactylus.sone.data.Sone; +import net.pterodactylus.sone.data.SoneOptions; +import net.pterodactylus.sone.data.SoneOptions.DefaultSoneOptions; +import net.pterodactylus.sone.database.Database; +import net.pterodactylus.sone.freenet.wot.Identity; +import net.pterodactylus.sone.freenet.wot.OwnIdentity; + +import freenet.keys.FreenetURI; + +import com.google.common.hash.Hasher; +import com.google.common.hash.Hashing; + +/** + * {@link Sone} implementation. + *

+ * Operations that modify the Sone need to synchronize on the Sone in question. + * + * @author David ‘Bombe’ Roden + */ +public class SoneImpl implements Sone { + + /** The logger. */ + private static final Logger logger = getLogger("Sone.Data"); + + /** The database. */ + private final Database database; + + /** The ID of this Sone. */ + private final String id; + + /** Whether the Sone is local. */ + private final boolean local; + + /** The identity of this Sone. */ + private final Identity identity; + + /** The latest edition of the Sone. */ + private volatile long latestEdition; + + /** The time of the last inserted update. */ + private volatile long time; + + /** The status of this Sone. */ + private volatile SoneStatus status = SoneStatus.unknown; + + /** The profile of this Sone. */ + private volatile Profile profile = new Profile(this); + + /** The client used by the Sone. */ + private volatile Client client; + + /** Whether this Sone is known. */ + private volatile boolean known; + + /** All posts. */ + private final Set posts = new CopyOnWriteArraySet(); + + /** All replies. */ + private final Set replies = new CopyOnWriteArraySet(); + + /** The IDs of all liked posts. */ + private final Set likedPostIds = new CopyOnWriteArraySet(); + + /** The IDs of all liked replies. */ + private final Set likedReplyIds = new CopyOnWriteArraySet(); + + /** The root album containing all albums. */ + private final Album rootAlbum = new AlbumImpl(this); + + /** Sone-specific options. */ + private SoneOptions options = new DefaultSoneOptions(); + + /** + * Creates a new Sone. + * + * @param database The database + * @param identity + * The identity of the Sone + * @param local + * {@code true} if the Sone is a local Sone, {@code false} otherwise + */ + public SoneImpl(Database database, Identity identity, boolean local) { + this.database = database; + this.id = identity.getId(); + this.identity = identity; + this.local = local; + } + + // + // ACCESSORS + // + + /** + * Returns the identity of this Sone. + * + * @return The identity of this Sone + */ + public String getId() { + return id; + } + + /** + * Returns the identity of this Sone. + * + * @return The identity of this Sone + */ + public Identity getIdentity() { + return identity; + } + + /** + * Returns the name of this Sone. + * + * @return The name of this Sone + */ + public String getName() { + return (identity != null) ? identity.getNickname() : null; + } + + /** + * Returns whether this Sone is a local Sone. + * + * @return {@code true} if this Sone is a local Sone, {@code false} otherwise + */ + public boolean isLocal() { + return local; + } + + /** + * Returns the request URI of this Sone. + * + * @return The request URI of this Sone + */ + public FreenetURI getRequestUri() { + try { + return new FreenetURI(getIdentity().getRequestUri()) + .setKeyType("USK") + .setDocName("Sone") + .setMetaString(new String[0]) + .setSuggestedEdition(latestEdition); + } catch (MalformedURLException e) { + throw new IllegalStateException( + format("Identity %s's request URI is incorrect.", + getIdentity()), e); + } + } + + /** + * Returns the insert URI of this Sone. + * + * @return The insert URI of this Sone + */ + public FreenetURI getInsertUri() { + if (!isLocal()) { + return null; + } + try { + return new FreenetURI(((OwnIdentity) getIdentity()).getInsertUri()) + .setDocName("Sone") + .setMetaString(new String[0]) + .setSuggestedEdition(latestEdition); + } catch (MalformedURLException e) { + throw new IllegalStateException(format("Own identity %s's insert URI is incorrect.", getIdentity()), e); + } + } + + /** + * Returns the latest edition of this Sone. + * + * @return The latest edition of this Sone + */ + public long getLatestEdition() { + return latestEdition; + } + + /** + * Sets the latest edition of this Sone. If the given latest edition is not + * greater than the current latest edition, the latest edition of this Sone is + * not changed. + * + * @param latestEdition + * The latest edition of this Sone + */ + public void setLatestEdition(long latestEdition) { + if (!(latestEdition > this.latestEdition)) { + logger.log(Level.FINE, String.format("New latest edition %d is not greater than current latest edition %d!", latestEdition, this.latestEdition)); + return; + } + this.latestEdition = latestEdition; + } + + /** + * Return the time of the last inserted update of this Sone. + * + * @return The time of the update (in milliseconds since Jan 1, 1970 UTC) + */ + public long getTime() { + return time; + } + + /** + * Sets the time of the last inserted update of this Sone. + * + * @param time + * The time of the update (in milliseconds since Jan 1, 1970 UTC) + * @return This Sone (for method chaining) + */ + public Sone setTime(long time) { + this.time = time; + return this; + } + + /** + * Returns the status of this Sone. + * + * @return The status of this Sone + */ + public SoneStatus getStatus() { + return status; + } + + /** + * Sets the new status of this Sone. + * + * @param status + * The new status of this Sone + * @return This Sone + * @throws IllegalArgumentException + * if {@code status} is {@code null} + */ + public Sone setStatus(SoneStatus status) { + this.status = checkNotNull(status, "status must not be null"); + return this; + } + + /** + * Returns a copy of the profile. If you want to update values in the profile + * of this Sone, update the values in the returned {@link Profile} and use + * {@link #setProfile(Profile)} to change the profile in this Sone. + * + * @return A copy of the profile + */ + public Profile getProfile() { + return new Profile(profile); + } + + /** + * Sets the profile of this Sone. A copy of the given profile is stored so that + * subsequent modifications of the given profile are not reflected in this + * Sone! + * + * @param profile + * The profile to set + */ + public void setProfile(Profile profile) { + this.profile = new Profile(profile); + } + + /** + * Returns the client used by this Sone. + * + * @return The client used by this Sone, or {@code null} + */ + public Client getClient() { + return client; + } + + /** + * Sets the client used by this Sone. + * + * @param client + * The client used by this Sone, or {@code null} + * @return This Sone (for method chaining) + */ + public Sone setClient(Client client) { + this.client = client; + return this; + } + + /** + * Returns whether this Sone is known. + * + * @return {@code true} if this Sone is known, {@code false} otherwise + */ + public boolean isKnown() { + return known; + } + + /** + * Sets whether this Sone is known. + * + * @param known + * {@code true} if this Sone is known, {@code false} otherwise + * @return This Sone + */ + public Sone setKnown(boolean known) { + this.known = known; + return this; + } + + /** + * Returns all friend Sones of this Sone. + * + * @return The friend Sones of this Sone + */ + public Collection getFriends() { + return database.getFriends(this); + } + + /** + * Returns whether this Sone has the given Sone as a friend Sone. + * + * @param friendSoneId + * The ID of the Sone to check for + * @return {@code true} if this Sone has the given Sone as a friend, {@code + * false} otherwise + */ + public boolean hasFriend(String friendSoneId) { + return database.isFriend(this, friendSoneId); + } + + /** + * Returns the list of posts of this Sone, sorted by time, newest first. + * + * @return All posts of this Sone + */ + public List getPosts() { + List sortedPosts; + synchronized (this) { + sortedPosts = new ArrayList(posts); + } + Collections.sort(sortedPosts, Post.TIME_COMPARATOR); + return sortedPosts; + } + + /** + * Sets all posts of this Sone at once. + * + * @param posts + * The new (and only) posts of this Sone + * @return This Sone (for method chaining) + */ + public Sone setPosts(Collection posts) { + synchronized (this) { + this.posts.clear(); + this.posts.addAll(posts); + } + return this; + } + + /** + * Adds the given post to this Sone. The post will not be added if its {@link + * Post#getSone() Sone} is not this Sone. + * + * @param post + * The post to add + */ + public void addPost(Post post) { + if (post.getSone().equals(this) && posts.add(post)) { + logger.log(Level.FINEST, String.format("Adding %s to “%s”.", post, getName())); + } + } + + /** + * Removes the given post from this Sone. + * + * @param post + * The post to remove + */ + public void removePost(Post post) { + if (post.getSone().equals(this)) { + posts.remove(post); + } + } + + /** + * Returns all replies this Sone made. + * + * @return All replies this Sone made + */ + public Set getReplies() { + return Collections.unmodifiableSet(replies); + } + + /** + * Sets all replies of this Sone at once. + * + * @param replies + * The new (and only) replies of this Sone + * @return This Sone (for method chaining) + */ + public Sone setReplies(Collection replies) { + this.replies.clear(); + this.replies.addAll(replies); + return this; + } + + /** + * Adds a reply to this Sone. If the given reply was not made by this Sone, + * nothing is added to this Sone. + * + * @param reply + * The reply to add + */ + public void addReply(PostReply reply) { + if (reply.getSone().equals(this)) { + replies.add(reply); + } + } + + /** + * Removes a reply from this Sone. + * + * @param reply + * The reply to remove + */ + public void removeReply(PostReply reply) { + if (reply.getSone().equals(this)) { + replies.remove(reply); + } + } + + /** + * Returns the IDs of all liked posts. + * + * @return All liked posts’ IDs + */ + public Set getLikedPostIds() { + return Collections.unmodifiableSet(likedPostIds); + } + + /** + * Sets the IDs of all liked posts. + * + * @param likedPostIds + * All liked posts’ IDs + * @return This Sone (for method chaining) + */ + public Sone setLikePostIds(Set likedPostIds) { + this.likedPostIds.clear(); + this.likedPostIds.addAll(likedPostIds); + return this; + } + + /** + * Checks whether the given post ID is liked by this Sone. + * + * @param postId + * The ID of the post + * @return {@code true} if this Sone likes the given post, {@code false} + * otherwise + */ + public boolean isLikedPostId(String postId) { + return likedPostIds.contains(postId); + } + + /** + * Adds the given post ID to the list of posts this Sone likes. + * + * @param postId + * The ID of the post + * @return This Sone (for method chaining) + */ + public Sone addLikedPostId(String postId) { + likedPostIds.add(postId); + return this; + } + + /** + * Removes the given post ID from the list of posts this Sone likes. + * + * @param postId + * The ID of the post + * @return This Sone (for method chaining) + */ + public Sone removeLikedPostId(String postId) { + likedPostIds.remove(postId); + return this; + } + + /** + * Returns the IDs of all liked replies. + * + * @return All liked replies’ IDs + */ + public Set getLikedReplyIds() { + return Collections.unmodifiableSet(likedReplyIds); + } + + /** + * Sets the IDs of all liked replies. + * + * @param likedReplyIds + * All liked replies’ IDs + * @return This Sone (for method chaining) + */ + public Sone setLikeReplyIds(Set likedReplyIds) { + this.likedReplyIds.clear(); + this.likedReplyIds.addAll(likedReplyIds); + return this; + } + + /** + * Checks whether the given reply ID is liked by this Sone. + * + * @param replyId + * The ID of the reply + * @return {@code true} if this Sone likes the given reply, {@code false} + * otherwise + */ + public boolean isLikedReplyId(String replyId) { + return likedReplyIds.contains(replyId); + } + + /** + * Adds the given reply ID to the list of replies this Sone likes. + * + * @param replyId + * The ID of the reply + * @return This Sone (for method chaining) + */ + public Sone addLikedReplyId(String replyId) { + likedReplyIds.add(replyId); + return this; + } + + /** + * Removes the given post ID from the list of replies this Sone likes. + * + * @param replyId + * The ID of the reply + * @return This Sone (for method chaining) + */ + public Sone removeLikedReplyId(String replyId) { + likedReplyIds.remove(replyId); + return this; + } + + /** + * Returns the root album that contains all visible albums of this Sone. + * + * @return The root album of this Sone + */ + public Album getRootAlbum() { + return rootAlbum; + } + + /** + * Returns Sone-specific options. + * + * @return The options of this Sone + */ + public SoneOptions getOptions() { + return options; + } + + /** + * Sets the options of this Sone. + * + * @param options + * The options of this Sone + */ + /* TODO - remove this method again, maybe add an option provider */ + public void setOptions(SoneOptions options) { + this.options = options; + } + + // + // FINGERPRINTABLE METHODS + // + + /** {@inheritDoc} */ + @Override + public synchronized String getFingerprint() { + Hasher hash = Hashing.sha256().newHasher(); + hash.putString(profile.getFingerprint()); + + hash.putString("Posts("); + for (Post post : getPosts()) { + hash.putString("Post(").putString(post.getId()).putString(")"); + } + hash.putString(")"); + + List replies = new ArrayList(getReplies()); + Collections.sort(replies, Reply.TIME_COMPARATOR); + hash.putString("Replies("); + for (PostReply reply : replies) { + hash.putString("Reply(").putString(reply.getId()).putString(")"); + } + hash.putString(")"); + + List likedPostIds = new ArrayList(getLikedPostIds()); + Collections.sort(likedPostIds); + hash.putString("LikedPosts("); + for (String likedPostId : likedPostIds) { + hash.putString("Post(").putString(likedPostId).putString(")"); + } + hash.putString(")"); + + List likedReplyIds = new ArrayList(getLikedReplyIds()); + Collections.sort(likedReplyIds); + hash.putString("LikedReplies("); + for (String likedReplyId : likedReplyIds) { + hash.putString("Reply(").putString(likedReplyId).putString(")"); + } + hash.putString(")"); + + hash.putString("Albums("); + for (Album album : rootAlbum.getAlbums()) { + if (!Album.NOT_EMPTY.apply(album)) { + continue; + } + hash.putString(album.getFingerprint()); + } + hash.putString(")"); + + return hash.hash().toString(); + } + + // + // INTERFACE Comparable + // + + /** {@inheritDoc} */ + @Override + public int compareTo(Sone sone) { + return NICE_NAME_COMPARATOR.compare(this, sone); + } + + // + // OBJECT METHODS + // + + /** {@inheritDoc} */ + @Override + public int hashCode() { + return id.hashCode(); + } + + /** {@inheritDoc} */ + @Override + public boolean equals(Object object) { + if (!(object instanceof Sone)) { + return false; + } + return ((Sone) object).getId().equals(id); + } + + /** {@inheritDoc} */ + @Override + public String toString() { + return getClass().getName() + "[identity=" + identity + ",posts(" + posts.size() + "),replies(" + replies.size() + "),albums(" + getRootAlbum().getAlbums().size() + ")]"; + } + +} diff --git a/src/main/java/net/pterodactylus/sone/database/AlbumBuilder.java b/src/main/java/net/pterodactylus/sone/database/AlbumBuilder.java index b888828..a084ae7 100644 --- a/src/main/java/net/pterodactylus/sone/database/AlbumBuilder.java +++ b/src/main/java/net/pterodactylus/sone/database/AlbumBuilder.java @@ -18,6 +18,7 @@ package net.pterodactylus.sone.database; import net.pterodactylus.sone.data.Album; +import net.pterodactylus.sone.data.Sone; /** * Builder for {@link Album} objects. @@ -42,6 +43,8 @@ public interface AlbumBuilder { */ AlbumBuilder withId(String id); + AlbumBuilder by(Sone sone); + /** * Creates the album. * diff --git a/src/main/java/net/pterodactylus/sone/database/BookmarkDatabase.java b/src/main/java/net/pterodactylus/sone/database/BookmarkDatabase.java new file mode 100644 index 0000000..6eb9c38 --- /dev/null +++ b/src/main/java/net/pterodactylus/sone/database/BookmarkDatabase.java @@ -0,0 +1,19 @@ +package net.pterodactylus.sone.database; + +import java.util.Set; + +import net.pterodactylus.sone.data.Post; + +/** + * Database interface for bookmark-related functionality. + * + * @author David ‘Bombe’ Roden + */ +public interface BookmarkDatabase { + + void bookmarkPost(Post post); + void unbookmarkPost(Post post); + boolean isPostBookmarked(Post post); + Set getBookmarkedPosts(); + +} diff --git a/src/main/java/net/pterodactylus/sone/database/Database.java b/src/main/java/net/pterodactylus/sone/database/Database.java index ee7a9af..971a427 100644 --- a/src/main/java/net/pterodactylus/sone/database/Database.java +++ b/src/main/java/net/pterodactylus/sone/database/Database.java @@ -17,16 +17,19 @@ package net.pterodactylus.sone.database; +import net.pterodactylus.sone.database.memory.MemoryDatabase; + import com.google.common.util.concurrent.Service; +import com.google.inject.ImplementedBy; /** - * Database for Sone data. This interface combines the various provider, store, - * and builder factory interfaces into a single interface and adds some methods - * necessary for lifecycle management. + * Database for Sone data. This interface combines the various provider, + * store, and builder factory interfaces into a single interface. * * @author David ‘Bombe’ Roden */ -public interface Database extends Service, PostDatabase, PostReplyDatabase, AlbumDatabase, ImageDatabase { +@ImplementedBy(MemoryDatabase.class) +public interface Database extends Service, SoneDatabase, FriendDatabase, PostDatabase, PostReplyDatabase, AlbumDatabase, ImageDatabase, BookmarkDatabase { /** * Saves the database. diff --git a/src/main/java/net/pterodactylus/sone/database/FriendDatabase.java b/src/main/java/net/pterodactylus/sone/database/FriendDatabase.java new file mode 100644 index 0000000..761d356 --- /dev/null +++ b/src/main/java/net/pterodactylus/sone/database/FriendDatabase.java @@ -0,0 +1,10 @@ +package net.pterodactylus.sone.database; + +/** + * Combines a {@link FriendProvider} and a {@link FriendStore} into a friend database. + * + * @author David ‘Bombe’ Roden + */ +public interface FriendDatabase extends FriendProvider, FriendStore { + +} diff --git a/src/main/java/net/pterodactylus/sone/database/FriendProvider.java b/src/main/java/net/pterodactylus/sone/database/FriendProvider.java new file mode 100644 index 0000000..3665d1b --- /dev/null +++ b/src/main/java/net/pterodactylus/sone/database/FriendProvider.java @@ -0,0 +1,17 @@ +package net.pterodactylus.sone.database; + +import java.util.Collection; + +import net.pterodactylus.sone.data.Sone; + +/** + * Provides information about {@link Sone#getFriends() friends} of a {@link Sone}. + * + * @author David ‘Bombe’ Roden + */ +public interface FriendProvider { + + Collection getFriends(Sone localSone); + boolean isFriend(Sone localSone, String friendSoneId); + +} diff --git a/src/main/java/net/pterodactylus/sone/database/FriendStore.java b/src/main/java/net/pterodactylus/sone/database/FriendStore.java new file mode 100644 index 0000000..38c1c80 --- /dev/null +++ b/src/main/java/net/pterodactylus/sone/database/FriendStore.java @@ -0,0 +1,15 @@ +package net.pterodactylus.sone.database; + +import net.pterodactylus.sone.data.Sone; + +/** + * Stores information about the {@link Sone#getFriends() friends} of a {@link Sone}. + * + * @author David ‘Bombe’ Roden + */ +public interface FriendStore { + + void addFriend(Sone localSone, String friendSoneId); + void removeFriend(Sone localSone, String friendSoneId); + +} diff --git a/src/main/java/net/pterodactylus/sone/database/PostBuilderFactory.java b/src/main/java/net/pterodactylus/sone/database/PostBuilderFactory.java index b89ae28..e74a6bd 100644 --- a/src/main/java/net/pterodactylus/sone/database/PostBuilderFactory.java +++ b/src/main/java/net/pterodactylus/sone/database/PostBuilderFactory.java @@ -17,11 +17,16 @@ package net.pterodactylus.sone.database; +import net.pterodactylus.sone.database.memory.MemoryDatabase; + +import com.google.inject.ImplementedBy; + /** * Factory for {@link PostBuilder}s. * * @author David ‘Bombe’ Roden */ +@ImplementedBy(MemoryDatabase.class) public interface PostBuilderFactory { /** diff --git a/src/main/java/net/pterodactylus/sone/database/PostProvider.java b/src/main/java/net/pterodactylus/sone/database/PostProvider.java index 740373e..7d5437a 100644 --- a/src/main/java/net/pterodactylus/sone/database/PostProvider.java +++ b/src/main/java/net/pterodactylus/sone/database/PostProvider.java @@ -20,14 +20,17 @@ package net.pterodactylus.sone.database; import java.util.Collection; import net.pterodactylus.sone.data.Post; +import net.pterodactylus.sone.database.memory.MemoryDatabase; import com.google.common.base.Optional; +import com.google.inject.ImplementedBy; /** * Interface for objects that can provide {@link Post}s by their ID. * * @author David ‘Bombe’ Roden */ +@ImplementedBy(MemoryDatabase.class) public interface PostProvider { /** diff --git a/src/main/java/net/pterodactylus/sone/database/PostReplyBuilderFactory.java b/src/main/java/net/pterodactylus/sone/database/PostReplyBuilderFactory.java index 7fd4ae1..cac3e30 100644 --- a/src/main/java/net/pterodactylus/sone/database/PostReplyBuilderFactory.java +++ b/src/main/java/net/pterodactylus/sone/database/PostReplyBuilderFactory.java @@ -17,11 +17,16 @@ package net.pterodactylus.sone.database; +import net.pterodactylus.sone.database.memory.MemoryDatabase; + +import com.google.inject.ImplementedBy; + /** * Factory for {@link PostReplyBuilder}s. * * @author David ‘Bombe’ Roden */ +@ImplementedBy(MemoryDatabase.class) public interface PostReplyBuilderFactory { /** diff --git a/src/main/java/net/pterodactylus/sone/database/PostReplyStore.java b/src/main/java/net/pterodactylus/sone/database/PostReplyStore.java index a3cefb3..30268b8 100644 --- a/src/main/java/net/pterodactylus/sone/database/PostReplyStore.java +++ b/src/main/java/net/pterodactylus/sone/database/PostReplyStore.java @@ -38,19 +38,6 @@ public interface PostReplyStore { public void storePostReply(PostReply postReply); /** - * Stores the given post replies as exclusive collection of post replies for - * the given Sone. This will remove all other post replies from this Sone! - * - * @param sone - * The Sone to store all post replies for - * @param postReplies - * The post replies of the Sone - * @throws IllegalArgumentException - * if one of the replies does not belong to the given Sone - */ - public void storePostReplies(Sone sone, Collection postReplies) throws IllegalArgumentException; - - /** * Removes the given post reply from this store. * * @param postReply @@ -58,12 +45,4 @@ public interface PostReplyStore { */ public void removePostReply(PostReply postReply); - /** - * Removes all post replies of the given Sone. - * - * @param sone - * The Sone to remove all post replies for - */ - public void removePostReplies(Sone sone); - } diff --git a/src/main/java/net/pterodactylus/sone/database/PostStore.java b/src/main/java/net/pterodactylus/sone/database/PostStore.java index 9c2ca42..402a647 100644 --- a/src/main/java/net/pterodactylus/sone/database/PostStore.java +++ b/src/main/java/net/pterodactylus/sone/database/PostStore.java @@ -45,25 +45,4 @@ public interface PostStore { */ public void removePost(Post post); - /** - * Stores the given posts as all posts of a single {@link Sone}. This method - * will removed all other posts from the Sone! - * - * @param sone - * The Sone to store the posts for - * @param posts - * The posts to store - * @throws IllegalArgumentException - * if posts do not all belong to the same Sone - */ - public void storePosts(Sone sone, Collection posts) throws IllegalArgumentException; - - /** - * Removes all posts of the given {@link Sone} - * - * @param sone - * The Sone to remove all posts for - */ - public void removePosts(Sone sone); - } diff --git a/src/main/java/net/pterodactylus/sone/database/SoneBuilder.java b/src/main/java/net/pterodactylus/sone/database/SoneBuilder.java new file mode 100644 index 0000000..d2047af --- /dev/null +++ b/src/main/java/net/pterodactylus/sone/database/SoneBuilder.java @@ -0,0 +1,18 @@ +package net.pterodactylus.sone.database; + +import net.pterodactylus.sone.data.Sone; +import net.pterodactylus.sone.freenet.wot.Identity; + +/** + * Builder for {@link Sone} objects. + * + * @author David ‘Bombe’ Roden + */ +public interface SoneBuilder { + + SoneBuilder from(Identity identity); + SoneBuilder local(); + + Sone build() throws IllegalStateException; + +} diff --git a/src/main/java/net/pterodactylus/sone/database/SoneBuilderFactory.java b/src/main/java/net/pterodactylus/sone/database/SoneBuilderFactory.java new file mode 100644 index 0000000..c95251f --- /dev/null +++ b/src/main/java/net/pterodactylus/sone/database/SoneBuilderFactory.java @@ -0,0 +1,12 @@ +package net.pterodactylus.sone.database; + +/** + * Factory for {@link SoneBuilder}s. + * + * @author David ‘Bombe’ Roden + */ +public interface SoneBuilderFactory { + + SoneBuilder newSoneBuilder(); + +} diff --git a/src/main/java/net/pterodactylus/sone/database/SoneDatabase.java b/src/main/java/net/pterodactylus/sone/database/SoneDatabase.java new file mode 100644 index 0000000..f5c5cda --- /dev/null +++ b/src/main/java/net/pterodactylus/sone/database/SoneDatabase.java @@ -0,0 +1,11 @@ +package net.pterodactylus.sone.database; + +/** + * Combines a {@link SoneProvider} and a {@link SoneStore} into a Sone + * database. + * + * @author David ‘Bombe’ Roden + */ +public interface SoneDatabase extends SoneProvider, SoneBuilderFactory, SoneStore { + +} diff --git a/src/main/java/net/pterodactylus/sone/database/SoneProvider.java b/src/main/java/net/pterodactylus/sone/database/SoneProvider.java index 993804f..73467a2 100644 --- a/src/main/java/net/pterodactylus/sone/database/SoneProvider.java +++ b/src/main/java/net/pterodactylus/sone/database/SoneProvider.java @@ -19,17 +19,23 @@ package net.pterodactylus.sone.database; import java.util.Collection; +import net.pterodactylus.sone.core.Core; import net.pterodactylus.sone.data.Sone; +import com.google.common.base.Function; import com.google.common.base.Optional; +import com.google.inject.ImplementedBy; /** * Interface for objects that can provide {@link Sone}s by their ID. * * @author David ‘Bombe’ Roden */ +@ImplementedBy(Core.class) public interface SoneProvider { + Function> soneLoader(); + /** * Returns the Sone with the given ID, or {@link Optional#absent()} if it * does not exist. diff --git a/src/main/java/net/pterodactylus/sone/database/SoneStore.java b/src/main/java/net/pterodactylus/sone/database/SoneStore.java new file mode 100644 index 0000000..3684d48 --- /dev/null +++ b/src/main/java/net/pterodactylus/sone/database/SoneStore.java @@ -0,0 +1,15 @@ +package net.pterodactylus.sone.database; + +import net.pterodactylus.sone.data.Sone; + +/** + * Interface for a store for {@link Sone}s. + * + * @author David ‘Bombe’ Roden + */ +public interface SoneStore { + + void storeSone(Sone sone); + void removeSone(Sone sone); + +} diff --git a/src/main/java/net/pterodactylus/sone/database/memory/ConfigurationLoader.java b/src/main/java/net/pterodactylus/sone/database/memory/ConfigurationLoader.java new file mode 100644 index 0000000..1691ddb --- /dev/null +++ b/src/main/java/net/pterodactylus/sone/database/memory/ConfigurationLoader.java @@ -0,0 +1,84 @@ +package net.pterodactylus.sone.database.memory; + +import static java.util.logging.Level.WARNING; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; +import java.util.logging.Logger; + +import net.pterodactylus.util.config.Configuration; +import net.pterodactylus.util.config.ConfigurationException; + +/** + * Helper class for interacting with a {@link Configuration}. + * + * @author David ‘Bombe’ Roden + */ +public class ConfigurationLoader { + + private static final Logger logger = + Logger.getLogger("Sone.Database.Memory.Configuration"); + private final Configuration configuration; + + public ConfigurationLoader(Configuration configuration) { + this.configuration = configuration; + } + + public synchronized Set loadFriends(String localSoneId) { + return loadIds("Sone/" + localSoneId + "/Friends"); + } + + public void saveFriends(String soneId, Collection friends) { + saveIds("Sone/" + soneId + "/Friends", friends); + } + + public synchronized Set loadKnownPosts() { + return loadIds("KnownPosts"); + } + + public synchronized Set loadKnownPostReplies() { + return loadIds("KnownReplies"); + } + + public synchronized Set loadBookmarkedPosts() { + return loadIds("Bookmarks/Post"); + } + + private Set loadIds(String prefix) { + Set ids = new HashSet(); + int idCounter = 0; + while (true) { + String id = configuration + .getStringValue(prefix + "/" + idCounter++ + "/ID") + .getValue(null); + if (id == null) { + break; + } + ids.add(id); + } + return ids; + } + + public synchronized void saveBookmarkedPosts( + Set bookmarkedPosts) { + saveIds("Bookmarks/Post", bookmarkedPosts); + } + + private void saveIds(String prefix, Collection ids) { + try { + int idCounter = 0; + for (String id : ids) { + configuration + .getStringValue(prefix + "/" + idCounter++ + "/ID") + .setValue(id); + } + configuration + .getStringValue(prefix + "/" + idCounter + "/ID") + .setValue(null); + } catch (ConfigurationException ce1) { + logger.log(WARNING, "Could not save bookmarked posts!", ce1); + } + } + +} diff --git a/src/main/java/net/pterodactylus/sone/database/memory/MemoryBookmarkDatabase.java b/src/main/java/net/pterodactylus/sone/database/memory/MemoryBookmarkDatabase.java new file mode 100644 index 0000000..594cf2b --- /dev/null +++ b/src/main/java/net/pterodactylus/sone/database/memory/MemoryBookmarkDatabase.java @@ -0,0 +1,111 @@ +package net.pterodactylus.sone.database.memory; + +import static com.google.common.collect.FluentIterable.from; + +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +import net.pterodactylus.sone.data.Post; +import net.pterodactylus.sone.data.Post.EmptyPost; +import net.pterodactylus.sone.database.BookmarkDatabase; + +import com.google.common.base.Function; + +/** + * Memory-based {@link BookmarkDatabase} implementation. + * + * @author David ‘Bombe’ Roden + */ +public class MemoryBookmarkDatabase implements BookmarkDatabase { + + private final ReadWriteLock lock = new ReentrantReadWriteLock(); + private final MemoryDatabase memoryDatabase; + private final ConfigurationLoader configurationLoader; + private final Set bookmarkedPosts = new HashSet(); + + public MemoryBookmarkDatabase(MemoryDatabase memoryDatabase, + ConfigurationLoader configurationLoader) { + this.memoryDatabase = memoryDatabase; + this.configurationLoader = configurationLoader; + } + + public void start() { + loadBookmarkedPosts(); + } + + private void loadBookmarkedPosts() { + Set bookmarkedPosts = configurationLoader.loadBookmarkedPosts(); + lock.writeLock().lock(); + try { + this.bookmarkedPosts.clear(); + this.bookmarkedPosts.addAll(bookmarkedPosts); + } finally { + lock.writeLock().unlock(); + } + } + + public void stop() { + saveBookmarkedPosts(); + } + + private void saveBookmarkedPosts() { + lock.readLock().lock(); + try { + configurationLoader.saveBookmarkedPosts(this.bookmarkedPosts); + } finally { + lock.readLock().unlock(); + } + } + + @Override + public void bookmarkPost(Post post) { + lock.writeLock().lock(); + try { + bookmarkedPosts.add(post.getId()); + saveBookmarkedPosts(); + } finally { + lock.writeLock().unlock(); + } + } + + @Override + public void unbookmarkPost(Post post) { + lock.writeLock().lock(); + try { + bookmarkedPosts.remove(post.getId()); + saveBookmarkedPosts(); + } finally { + lock.writeLock().unlock(); + } + } + + @Override + public boolean isPostBookmarked(Post post) { + lock.readLock().lock(); + try { + return bookmarkedPosts.contains(post.getId()); + } finally { + lock.readLock().unlock(); + } + } + + @Override + public Set getBookmarkedPosts() { + lock.readLock().lock(); + try { + return from(bookmarkedPosts).transform( + new Function() { + @Override + public Post apply(String postId) { + return memoryDatabase.getPost(postId) + .or(new EmptyPost(postId)); + } + }).toSet(); + } finally { + lock.readLock().unlock(); + } + } + +} diff --git a/src/main/java/net/pterodactylus/sone/database/memory/MemoryDatabase.java b/src/main/java/net/pterodactylus/sone/database/memory/MemoryDatabase.java index 8b25954..b830ba5 100644 --- a/src/main/java/net/pterodactylus/sone/database/memory/MemoryDatabase.java +++ b/src/main/java/net/pterodactylus/sone/database/memory/MemoryDatabase.java @@ -19,8 +19,13 @@ package net.pterodactylus.sone.database.memory; import static com.google.common.base.Optional.fromNullable; import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Predicates.not; +import static com.google.common.collect.FluentIterable.from; +import static net.pterodactylus.sone.data.Reply.TIME_COMPARATOR; +import static net.pterodactylus.sone.data.Sone.LOCAL_SONE_FILTER; +import static net.pterodactylus.sone.data.Sone.toAllAlbums; +import static net.pterodactylus.sone.data.Sone.toAllImages; -import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; @@ -29,8 +34,6 @@ import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; -import java.util.SortedSet; -import java.util.TreeSet; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; @@ -38,7 +41,6 @@ import net.pterodactylus.sone.data.Album; import net.pterodactylus.sone.data.Image; import net.pterodactylus.sone.data.Post; import net.pterodactylus.sone.data.PostReply; -import net.pterodactylus.sone.data.Reply; import net.pterodactylus.sone.data.Sone; import net.pterodactylus.sone.data.impl.AlbumBuilderImpl; import net.pterodactylus.sone.data.impl.ImageBuilderImpl; @@ -49,21 +51,28 @@ import net.pterodactylus.sone.database.ImageBuilder; import net.pterodactylus.sone.database.PostBuilder; import net.pterodactylus.sone.database.PostDatabase; import net.pterodactylus.sone.database.PostReplyBuilder; +import net.pterodactylus.sone.database.SoneBuilder; import net.pterodactylus.sone.database.SoneProvider; import net.pterodactylus.util.config.Configuration; import net.pterodactylus.util.config.ConfigurationException; +import com.google.common.base.Function; import com.google.common.base.Optional; +import com.google.common.base.Predicate; +import com.google.common.collect.HashMultimap; +import com.google.common.collect.Multimap; import com.google.common.collect.SortedSetMultimap; import com.google.common.collect.TreeMultimap; import com.google.common.util.concurrent.AbstractService; import com.google.inject.Inject; +import com.google.inject.Singleton; /** * Memory-based {@link PostDatabase} implementation. * * @author David ‘Bombe’ Roden */ +@Singleton public class MemoryDatabase extends AbstractService implements Database { /** The lock. */ @@ -74,15 +83,15 @@ public class MemoryDatabase extends AbstractService implements Database { /** The configuration. */ private final Configuration configuration; + private final ConfigurationLoader configurationLoader; + + private final Map allSones = new HashMap(); /** All posts by their ID. */ private final Map allPosts = new HashMap(); /** All posts by their Sones. */ - private final Map> sonePosts = new HashMap>(); - - /** All posts by their recipient. */ - private final Map> recipientPosts = new HashMap>(); + private final Multimap sonePosts = HashMultimap.create(); /** Whether posts are known. */ private final Set knownPosts = new HashSet(); @@ -97,17 +106,19 @@ public class MemoryDatabase extends AbstractService implements Database { public int compare(String leftString, String rightString) { return leftString.compareTo(rightString); } - }, PostReply.TIME_COMPARATOR); - - /** Replies by post. */ - private final Map> postReplies = new HashMap>(); + }, TIME_COMPARATOR); /** Whether post replies are known. */ private final Set knownPostReplies = new HashSet(); private final Map allAlbums = new HashMap(); + private final Multimap soneAlbums = HashMultimap.create(); private final Map allImages = new HashMap(); + private final Multimap soneImages = HashMultimap.create(); + + private final MemoryBookmarkDatabase memoryBookmarkDatabase; + private final MemoryFriendDatabase memoryFriendDatabase; /** * Creates a new memory database. @@ -121,6 +132,10 @@ public class MemoryDatabase extends AbstractService implements Database { public MemoryDatabase(SoneProvider soneProvider, Configuration configuration) { this.soneProvider = soneProvider; this.configuration = configuration; + this.configurationLoader = new ConfigurationLoader(configuration); + memoryBookmarkDatabase = + new MemoryBookmarkDatabase(this, configurationLoader); + memoryFriendDatabase = new MemoryFriendDatabase(configurationLoader); } // @@ -146,6 +161,7 @@ public class MemoryDatabase extends AbstractService implements Database { /** {@inheritDocs} */ @Override protected void doStart() { + memoryBookmarkDatabase.start(); loadKnownPosts(); loadKnownPostReplies(); notifyStarted(); @@ -155,6 +171,7 @@ public class MemoryDatabase extends AbstractService implements Database { @Override protected void doStop() { try { + memoryBookmarkDatabase.stop(); save(); notifyStopped(); } catch (DatabaseException de1) { @@ -162,6 +179,151 @@ public class MemoryDatabase extends AbstractService implements Database { } } + @Override + public SoneBuilder newSoneBuilder() { + return new MemorySoneBuilder(this); + } + + @Override + public void storeSone(Sone sone) { + lock.writeLock().lock(); + try { + removeSone(sone); + + allSones.put(sone.getId(), sone); + sonePosts.putAll(sone.getId(), sone.getPosts()); + for (Post post : sone.getPosts()) { + allPosts.put(post.getId(), post); + } + sonePostReplies.putAll(sone.getId(), sone.getReplies()); + for (PostReply postReply : sone.getReplies()) { + allPostReplies.put(postReply.getId(), postReply); + } + soneAlbums.putAll(sone.getId(), toAllAlbums.apply(sone)); + for (Album album : toAllAlbums.apply(sone)) { + allAlbums.put(album.getId(), album); + } + soneImages.putAll(sone.getId(), toAllImages.apply(sone)); + for (Image image : toAllImages.apply(sone)) { + allImages.put(image.getId(), image); + } + } finally { + lock.writeLock().unlock(); + } + } + + @Override + public void removeSone(Sone sone) { + lock.writeLock().lock(); + try { + allSones.remove(sone.getId()); + Collection removedPosts = sonePosts.removeAll(sone.getId()); + for (Post removedPost : removedPosts) { + allPosts.remove(removedPost.getId()); + } + Collection removedPostReplies = + sonePostReplies.removeAll(sone.getId()); + for (PostReply removedPostReply : removedPostReplies) { + allPostReplies.remove(removedPostReply.getId()); + } + Collection removedAlbums = + soneAlbums.removeAll(sone.getId()); + for (Album removedAlbum : removedAlbums) { + allAlbums.remove(removedAlbum.getId()); + } + Collection removedImages = + soneImages.removeAll(sone.getId()); + for (Image removedImage : removedImages) { + allImages.remove(removedImage.getId()); + } + } finally { + lock.writeLock().unlock(); + } + } + + @Override + public Function> soneLoader() { + return new Function>() { + @Override + public Optional apply(String soneId) { + return getSone(soneId); + } + }; + } + + @Override + public Optional getSone(String soneId) { + lock.readLock().lock(); + try { + return fromNullable(allSones.get(soneId)); + } finally { + lock.readLock().unlock(); + } + } + + @Override + public Collection getSones() { + lock.readLock().lock(); + try { + return new HashSet(allSones.values()); + } finally { + lock.readLock().unlock(); + } + } + + @Override + public Collection getLocalSones() { + lock.readLock().lock(); + try { + return from(allSones.values()).filter(LOCAL_SONE_FILTER).toSet(); + } finally { + lock.readLock().unlock(); + } + } + + @Override + public Collection getRemoteSones() { + lock.readLock().lock(); + try { + return from(allSones.values()) + .filter(not(LOCAL_SONE_FILTER)) .toSet(); + } finally { + lock.readLock().unlock(); + } + } + + @Override + public Collection getFriends(Sone localSone) { + if (!localSone.isLocal()) { + return Collections.emptySet(); + } + return memoryFriendDatabase.getFriends(localSone.getId()); + } + + @Override + public boolean isFriend(Sone localSone, String friendSoneId) { + if (!localSone.isLocal()) { + return false; + } + return memoryFriendDatabase.isFriend(localSone.getId(), friendSoneId); + } + + @Override + public void addFriend(Sone localSone, String friendSoneId) { + if (!localSone.isLocal()) { + return; + } + memoryFriendDatabase.addFriend(localSone.getId(), friendSoneId); + } + + @Override + public void removeFriend(Sone localSone, String friendSoneId) { + if (!localSone.isLocal()) { + return; + } + memoryFriendDatabase.removeFriend(localSone.getId(), friendSoneId); + } + // // POSTPROVIDER METHODS // @@ -185,11 +347,15 @@ public class MemoryDatabase extends AbstractService implements Database { /** {@inheritDocs} */ @Override - public Collection getDirectedPosts(String recipientId) { + public Collection getDirectedPosts(final String recipientId) { lock.readLock().lock(); try { - Collection posts = recipientPosts.get(recipientId); - return (posts == null) ? Collections.emptySet() : new HashSet(posts); + return from(sonePosts.values()).filter(new Predicate() { + @Override + public boolean apply(Post post) { + return post.getRecipientId().asSet().contains(recipientId); + } + }).toSet(); } finally { lock.readLock().unlock(); } @@ -217,9 +383,6 @@ public class MemoryDatabase extends AbstractService implements Database { try { allPosts.put(post.getId(), post); getPostsFrom(post.getSone().getId()).add(post); - if (post.getRecipientId().isPresent()) { - getPostsTo(post.getRecipientId().get()).add(post); - } } finally { lock.writeLock().unlock(); } @@ -233,69 +396,12 @@ public class MemoryDatabase extends AbstractService implements Database { try { allPosts.remove(post.getId()); getPostsFrom(post.getSone().getId()).remove(post); - if (post.getRecipientId().isPresent()) { - getPostsTo(post.getRecipientId().get()).remove(post); - } post.getSone().removePost(post); } finally { lock.writeLock().unlock(); } } - /** {@inheritDocs} */ - @Override - public void storePosts(Sone sone, Collection posts) throws IllegalArgumentException { - checkNotNull(sone, "sone must not be null"); - /* verify that all posts are from the same Sone. */ - for (Post post : posts) { - if (!sone.equals(post.getSone())) { - throw new IllegalArgumentException(String.format("Post from different Sone found: %s", post)); - } - } - - lock.writeLock().lock(); - try { - /* remove all posts by the Sone. */ - getPostsFrom(sone.getId()).clear(); - for (Post post : posts) { - allPosts.remove(post.getId()); - if (post.getRecipientId().isPresent()) { - getPostsTo(post.getRecipientId().get()).remove(post); - } - } - - /* add new posts. */ - getPostsFrom(sone.getId()).addAll(posts); - for (Post post : posts) { - allPosts.put(post.getId(), post); - if (post.getRecipientId().isPresent()) { - getPostsTo(post.getRecipientId().get()).add(post); - } - } - } finally { - lock.writeLock().unlock(); - } - } - - /** {@inheritDocs} */ - @Override - public void removePosts(Sone sone) { - checkNotNull(sone, "sone must not be null"); - lock.writeLock().lock(); - try { - /* remove all posts by the Sone. */ - getPostsFrom(sone.getId()).clear(); - for (Post post : sone.getPosts()) { - allPosts.remove(post.getId()); - if (post.getRecipientId().isPresent()) { - getPostsTo(post.getRecipientId().get()).remove(post); - } - } - } finally { - lock.writeLock().unlock(); - } - } - // // POSTREPLYPROVIDER METHODS // @@ -313,13 +419,16 @@ public class MemoryDatabase extends AbstractService implements Database { /** {@inheritDocs} */ @Override - public List getReplies(String postId) { + public List getReplies(final String postId) { lock.readLock().lock(); try { - if (!postReplies.containsKey(postId)) { - return Collections.emptyList(); - } - return new ArrayList(postReplies.get(postId)); + return from(allPostReplies.values()) + .filter(new Predicate() { + @Override + public boolean apply(PostReply postReply) { + return postReply.getPostId().equals(postId); + } + }).toSortedList(TIME_COMPARATOR); } finally { lock.readLock().unlock(); } @@ -345,46 +454,6 @@ public class MemoryDatabase extends AbstractService implements Database { lock.writeLock().lock(); try { allPostReplies.put(postReply.getId(), postReply); - if (postReplies.containsKey(postReply.getPostId())) { - postReplies.get(postReply.getPostId()).add(postReply); - } else { - TreeSet replies = new TreeSet(Reply.TIME_COMPARATOR); - replies.add(postReply); - postReplies.put(postReply.getPostId(), replies); - } - } finally { - lock.writeLock().unlock(); - } - } - - /** {@inheritDocs} */ - @Override - public void storePostReplies(Sone sone, Collection postReplies) { - checkNotNull(sone, "sone must not be null"); - /* verify that all posts are from the same Sone. */ - for (PostReply postReply : postReplies) { - if (!sone.equals(postReply.getSone())) { - throw new IllegalArgumentException(String.format("PostReply from different Sone found: %s", postReply)); - } - } - - lock.writeLock().lock(); - try { - /* remove all post replies of the Sone. */ - for (PostReply postReply : getRepliesFrom(sone.getId())) { - removePostReply(postReply); - } - for (PostReply postReply : postReplies) { - allPostReplies.put(postReply.getId(), postReply); - sonePostReplies.put(postReply.getSone().getId(), postReply); - if (this.postReplies.containsKey(postReply.getPostId())) { - this.postReplies.get(postReply.getPostId()).add(postReply); - } else { - TreeSet replies = new TreeSet(Reply.TIME_COMPARATOR); - replies.add(postReply); - this.postReplies.put(postReply.getPostId(), replies); - } - } } finally { lock.writeLock().unlock(); } @@ -396,27 +465,6 @@ public class MemoryDatabase extends AbstractService implements Database { lock.writeLock().lock(); try { allPostReplies.remove(postReply.getId()); - if (postReplies.containsKey(postReply.getPostId())) { - postReplies.get(postReply.getPostId()).remove(postReply); - if (postReplies.get(postReply.getPostId()).isEmpty()) { - postReplies.remove(postReply.getPostId()); - } - } - } finally { - lock.writeLock().unlock(); - } - } - - /** {@inheritDocs} */ - @Override - public void removePostReplies(Sone sone) { - checkNotNull(sone, "sone must not be null"); - - lock.writeLock().lock(); - try { - for (PostReply postReply : sone.getReplies()) { - removePostReply(postReply); - } } finally { lock.writeLock().unlock(); } @@ -454,6 +502,7 @@ public class MemoryDatabase extends AbstractService implements Database { lock.writeLock().lock(); try { allAlbums.put(album.getId(), album); + soneAlbums.put(album.getSone().getId(), album); } finally { lock.writeLock().unlock(); } @@ -464,6 +513,7 @@ public class MemoryDatabase extends AbstractService implements Database { lock.writeLock().lock(); try { allAlbums.remove(album.getId()); + soneAlbums.remove(album.getSone().getId(), album); } finally { lock.writeLock().unlock(); } @@ -501,6 +551,7 @@ public class MemoryDatabase extends AbstractService implements Database { lock.writeLock().lock(); try { allImages.put(image.getId(), image); + soneImages.put(image.getSone().getId(), image); } finally { lock.writeLock().unlock(); } @@ -511,11 +562,32 @@ public class MemoryDatabase extends AbstractService implements Database { lock.writeLock().lock(); try { allImages.remove(image.getId()); + soneImages.remove(image.getSone().getId(), image); } finally { lock.writeLock().unlock(); } } + @Override + public void bookmarkPost(Post post) { + memoryBookmarkDatabase.bookmarkPost(post); + } + + @Override + public void unbookmarkPost(Post post) { + memoryBookmarkDatabase.unbookmarkPost(post); + } + + @Override + public boolean isPostBookmarked(Post post) { + return memoryBookmarkDatabase.isPostBookmarked(post); + } + + @Override + public Set getBookmarkedPosts() { + return memoryBookmarkDatabase.getBookmarkedPosts(); + } + // // PACKAGE-PRIVATE METHODS // @@ -608,71 +680,21 @@ public class MemoryDatabase extends AbstractService implements Database { * @return All posts */ private Collection getPostsFrom(String soneId) { - Collection posts = null; lock.readLock().lock(); try { - posts = sonePosts.get(soneId); + return sonePosts.get(soneId); } finally { lock.readLock().unlock(); } - if (posts != null) { - return posts; - } - - posts = new HashSet(); - lock.writeLock().lock(); - try { - sonePosts.put(soneId, posts); - } finally { - lock.writeLock().unlock(); - } - - return posts; - } - - /** - * Gets all posts that are directed the given Sone, creating a new collection - * if there is none yet. - * - * @param recipientId - * The ID of the Sone to get the posts for - * @return All posts - */ - private Collection getPostsTo(String recipientId) { - Collection posts = null; - lock.readLock().lock(); - try { - posts = recipientPosts.get(recipientId); - } finally { - lock.readLock().unlock(); - } - if (posts != null) { - return posts; - } - - posts = new HashSet(); - lock.writeLock().lock(); - try { - recipientPosts.put(recipientId, posts); - } finally { - lock.writeLock().unlock(); - } - - return posts; } /** Loads the known posts. */ private void loadKnownPosts() { + Set knownPosts = configurationLoader.loadKnownPosts(); lock.writeLock().lock(); try { - int postCounter = 0; - while (true) { - String knownPostId = configuration.getStringValue("KnownPosts/" + postCounter++ + "/ID").getValue(null); - if (knownPostId == null) { - break; - } - knownPosts.add(knownPostId); - } + this.knownPosts.clear(); + this.knownPosts.addAll(knownPosts); } finally { lock.writeLock().unlock(); } @@ -699,37 +721,13 @@ public class MemoryDatabase extends AbstractService implements Database { } } - /** - * Returns all replies by the given Sone. - * - * @param id - * The ID of the Sone - * @return The post replies of the Sone, sorted by time (newest first) - */ - private Collection getRepliesFrom(String id) { - lock.readLock().lock(); - try { - if (sonePostReplies.containsKey(id)) { - return Collections.unmodifiableCollection(sonePostReplies.get(id)); - } - return Collections.emptySet(); - } finally { - lock.readLock().unlock(); - } - } - /** Loads the known post replies. */ private void loadKnownPostReplies() { + Set knownPostReplies = configurationLoader.loadKnownPostReplies(); lock.writeLock().lock(); try { - int replyCounter = 0; - while (true) { - String knownReplyId = configuration.getStringValue("KnownReplies/" + replyCounter++ + "/ID").getValue(null); - if (knownReplyId == null) { - break; - } - knownPostReplies.add(knownReplyId); - } + this.knownPostReplies.clear(); + this.knownPostReplies.addAll(knownPostReplies); } finally { lock.writeLock().unlock(); } diff --git a/src/main/java/net/pterodactylus/sone/database/memory/MemoryFriendDatabase.java b/src/main/java/net/pterodactylus/sone/database/memory/MemoryFriendDatabase.java new file mode 100644 index 0000000..0be8738 --- /dev/null +++ b/src/main/java/net/pterodactylus/sone/database/memory/MemoryFriendDatabase.java @@ -0,0 +1,81 @@ +package net.pterodactylus.sone.database.memory; + +import java.util.Collection; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +import com.google.common.collect.HashMultimap; +import com.google.common.collect.Multimap; + +/** + * In-memory implementation of friend-related functionality. + * + * @author David ‘Bombe’ Roden + */ +class MemoryFriendDatabase { + + private final ReadWriteLock lock = new ReentrantReadWriteLock(); + private final ConfigurationLoader configurationLoader; + private final Multimap soneFriends = HashMultimap.create(); + + MemoryFriendDatabase(ConfigurationLoader configurationLoader) { + this.configurationLoader = configurationLoader; + } + + Collection getFriends(String localSoneId) { + loadFriends(localSoneId); + lock.readLock().lock(); + try { + return soneFriends.get(localSoneId); + } finally { + lock.readLock().unlock(); + } + } + + boolean isFriend(String localSoneId, String friendSoneId) { + loadFriends(localSoneId); + lock.readLock().lock(); + try { + return soneFriends.containsEntry(localSoneId, friendSoneId); + } finally { + lock.readLock().unlock(); + } + } + + void addFriend(String localSoneId, String friendSoneId) { + loadFriends(localSoneId); + lock.writeLock().lock(); + try { + if (soneFriends.put(localSoneId, friendSoneId)) { + configurationLoader.saveFriends(localSoneId, soneFriends.get(localSoneId)); + } + } finally { + lock.writeLock().unlock(); + } + } + + void removeFriend(String localSoneId, String friendSoneId) { + loadFriends(localSoneId); + lock.writeLock().lock(); + try { + if (soneFriends.remove(localSoneId, friendSoneId)) { + configurationLoader.saveFriends(localSoneId, soneFriends.get(localSoneId)); + } + } finally { + lock.writeLock().unlock(); + } + } + + private void loadFriends(String localSoneId) { + lock.writeLock().lock(); + try { + if (soneFriends.containsKey(localSoneId)) { + return; + } + soneFriends.putAll(localSoneId, configurationLoader.loadFriends(localSoneId)); + } finally { + lock.writeLock().unlock(); + } + } + +} diff --git a/src/main/java/net/pterodactylus/sone/database/memory/MemoryPost.java b/src/main/java/net/pterodactylus/sone/database/memory/MemoryPost.java index 22fa7e6..180cf6c 100644 --- a/src/main/java/net/pterodactylus/sone/database/memory/MemoryPost.java +++ b/src/main/java/net/pterodactylus/sone/database/memory/MemoryPost.java @@ -94,6 +94,11 @@ class MemoryPost implements Post { return id.toString(); } + @Override + public boolean isLoaded() { + return true; + } + /** * {@inheritDoc} */ diff --git a/src/main/java/net/pterodactylus/sone/database/memory/MemorySoneBuilder.java b/src/main/java/net/pterodactylus/sone/database/memory/MemorySoneBuilder.java new file mode 100644 index 0000000..49531a1 --- /dev/null +++ b/src/main/java/net/pterodactylus/sone/database/memory/MemorySoneBuilder.java @@ -0,0 +1,27 @@ +package net.pterodactylus.sone.database.memory; + +import net.pterodactylus.sone.data.Sone; +import net.pterodactylus.sone.data.impl.SoneImpl; +import net.pterodactylus.sone.data.impl.AbstractSoneBuilder; +import net.pterodactylus.sone.database.Database; + +/** + * Memory-based {@link AbstractSoneBuilder} implementation. + * + * @author David ‘Bombe’ Roden + */ +public class MemorySoneBuilder extends AbstractSoneBuilder { + + private final Database database; + + public MemorySoneBuilder(Database database) { + this.database = database; + } + + @Override + public Sone build() throws IllegalStateException { + validate(); + return new SoneImpl(database, identity, local); + } + +} diff --git a/src/main/java/net/pterodactylus/sone/fcp/FcpInterface.java b/src/main/java/net/pterodactylus/sone/fcp/FcpInterface.java index 02d84cc..839930c 100644 --- a/src/main/java/net/pterodactylus/sone/fcp/FcpInterface.java +++ b/src/main/java/net/pterodactylus/sone/fcp/FcpInterface.java @@ -18,20 +18,23 @@ package net.pterodactylus.sone.fcp; import static com.google.common.base.Preconditions.checkNotNull; +import static java.util.logging.Logger.getLogger; import java.util.Collections; import java.util.HashMap; import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; import java.util.logging.Level; import java.util.logging.Logger; import net.pterodactylus.sone.core.Core; +import net.pterodactylus.sone.fcp.event.FcpInterfaceActivatedEvent; +import net.pterodactylus.sone.fcp.event.FcpInterfaceDeactivatedEvent; +import net.pterodactylus.sone.fcp.event.FullAccessRequiredChanged; import net.pterodactylus.sone.freenet.fcp.Command.AccessType; import net.pterodactylus.sone.freenet.fcp.Command.ErrorResponse; import net.pterodactylus.sone.freenet.fcp.Command.Response; -import net.pterodactylus.util.logging.Logging; - -import com.google.inject.Inject; import freenet.pluginmanager.FredPluginFCP; import freenet.pluginmanager.PluginNotFoundException; @@ -39,12 +42,18 @@ import freenet.pluginmanager.PluginReplySender; import freenet.support.SimpleFieldSet; import freenet.support.api.Bucket; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.eventbus.Subscribe; +import com.google.inject.Inject; +import com.google.inject.Singleton; + /** * Implementation of an FCP interface for other clients or plugins to * communicate with Sone. * * @author David ‘Bombe’ Roden */ +@Singleton public class FcpInterface { /** @@ -66,13 +75,13 @@ public class FcpInterface { } /** The logger. */ - private static final Logger logger = Logging.getLogger(FcpInterface.class); + private static final Logger logger = getLogger("Sone.External.Fcp"); /** Whether the FCP interface is currently active. */ - private volatile boolean active; + private final AtomicBoolean active = new AtomicBoolean(); /** What function full access is required for. */ - private volatile FullAccessRequired fullAccessRequired = FullAccessRequired.ALWAYS; + private final AtomicReference fullAccessRequired = new AtomicReference(FullAccessRequired.ALWAYS); /** All available FCP commands. */ private final Map commands = Collections.synchronizedMap(new HashMap()); @@ -106,26 +115,22 @@ public class FcpInterface { // ACCESSORS // - /** - * Sets whether the FCP interface should handle requests. If {@code active} - * is {@code false}, all requests are answered with an error. - * - * @param active - * {@code true} to activate the FCP interface, {@code false} to - * deactivate the FCP interface - */ - public void setActive(boolean active) { - this.active = active; + @VisibleForTesting + boolean isActive() { + return active.get(); } - /** - * Sets the action level for which full FCP access is required. - * - * @param fullAccessRequired - * The action level for which full FCP access is required - */ - public void setFullAccessRequired(FullAccessRequired fullAccessRequired) { - this.fullAccessRequired = checkNotNull(fullAccessRequired, "fullAccessRequired must not be null"); + private void setActive(boolean active) { + this.active.set(active); + } + + @VisibleForTesting + FullAccessRequired getFullAccessRequired() { + return fullAccessRequired.get(); + } + + private void setFullAccessRequired(FullAccessRequired fullAccessRequired) { + this.fullAccessRequired.set(checkNotNull(fullAccessRequired, "fullAccessRequired must not be null")); } // @@ -147,7 +152,7 @@ public class FcpInterface { * {@link FredPluginFCP#ACCESS_FCP_RESTRICTED} */ public void handle(PluginReplySender pluginReplySender, SimpleFieldSet parameters, Bucket data, int accessType) { - if (!active) { + if (!active.get()) { try { sendReply(pluginReplySender, null, new ErrorResponse(400, "FCP Interface deactivated")); } catch (PluginNotFoundException pnfe1) { @@ -156,7 +161,7 @@ public class FcpInterface { return; } AbstractSoneCommand command = commands.get(parameters.get("Message")); - if ((accessType == FredPluginFCP.ACCESS_FCP_RESTRICTED) && (((fullAccessRequired == FullAccessRequired.WRITING) && command.requiresWriteAccess()) || (fullAccessRequired == FullAccessRequired.ALWAYS))) { + if ((accessType == FredPluginFCP.ACCESS_FCP_RESTRICTED) && (((fullAccessRequired.get() == FullAccessRequired.WRITING) && command.requiresWriteAccess()) || (fullAccessRequired.get() == FullAccessRequired.ALWAYS))) { try { sendReply(pluginReplySender, null, new ErrorResponse(401, "Not authorized")); } catch (PluginNotFoundException pnfe1) { @@ -216,4 +221,19 @@ public class FcpInterface { } } + @Subscribe + public void fcpInterfaceActivated(FcpInterfaceActivatedEvent fcpInterfaceActivatedEvent) { + setActive(true); + } + + @Subscribe + public void fcpInterfaceDeactivated(FcpInterfaceDeactivatedEvent fcpInterfaceDeactivatedEvent) { + setActive(false); + } + + @Subscribe + public void fullAccessRequiredChanged(FullAccessRequiredChanged fullAccessRequiredChanged) { + setFullAccessRequired(fullAccessRequiredChanged.getFullAccessRequired()); + } + } diff --git a/src/main/java/net/pterodactylus/sone/fcp/event/FcpInterfaceActivatedEvent.java b/src/main/java/net/pterodactylus/sone/fcp/event/FcpInterfaceActivatedEvent.java new file mode 100644 index 0000000..56b658a --- /dev/null +++ b/src/main/java/net/pterodactylus/sone/fcp/event/FcpInterfaceActivatedEvent.java @@ -0,0 +1,13 @@ +package net.pterodactylus.sone.fcp.event; + +import net.pterodactylus.sone.fcp.FcpInterface; + +/** + * Event that signals that the {@link FcpInterface} was activated in the + * configuration. + * + * @author David ‘Bombe’ Roden + */ +public class FcpInterfaceActivatedEvent { + +} diff --git a/src/main/java/net/pterodactylus/sone/fcp/event/FcpInterfaceDeactivatedEvent.java b/src/main/java/net/pterodactylus/sone/fcp/event/FcpInterfaceDeactivatedEvent.java new file mode 100644 index 0000000..b97ef76 --- /dev/null +++ b/src/main/java/net/pterodactylus/sone/fcp/event/FcpInterfaceDeactivatedEvent.java @@ -0,0 +1,13 @@ +package net.pterodactylus.sone.fcp.event; + +import net.pterodactylus.sone.fcp.FcpInterface; + +/** + * Event that signals that the {@link FcpInterface} was deactivated in the + * configuration. + * + * @author David ‘Bombe’ Roden + */ +public class FcpInterfaceDeactivatedEvent { + +} diff --git a/src/main/java/net/pterodactylus/sone/fcp/event/FullAccessRequiredChanged.java b/src/main/java/net/pterodactylus/sone/fcp/event/FullAccessRequiredChanged.java new file mode 100644 index 0000000..9638880 --- /dev/null +++ b/src/main/java/net/pterodactylus/sone/fcp/event/FullAccessRequiredChanged.java @@ -0,0 +1,24 @@ +package net.pterodactylus.sone.fcp.event; + +import net.pterodactylus.sone.fcp.FcpInterface; +import net.pterodactylus.sone.fcp.FcpInterface.FullAccessRequired; + +/** + * Event that signals that the {@link FcpInterface}’s {@link + * FullAccessRequired} parameter was changed in the configuration. + * + * @author David ‘Bombe’ Roden + */ +public class FullAccessRequiredChanged { + + private final FullAccessRequired fullAccessRequired; + + public FullAccessRequiredChanged(FullAccessRequired fullAccessRequired) { + this.fullAccessRequired = fullAccessRequired; + } + + public FullAccessRequired getFullAccessRequired() { + return fullAccessRequired; + } + +} diff --git a/src/main/java/net/pterodactylus/sone/freenet/Key.java b/src/main/java/net/pterodactylus/sone/freenet/Key.java new file mode 100644 index 0000000..f21e2f6 --- /dev/null +++ b/src/main/java/net/pterodactylus/sone/freenet/Key.java @@ -0,0 +1,67 @@ +package net.pterodactylus.sone.freenet; + +import static freenet.support.Base64.encode; +import static java.lang.String.format; + +import freenet.keys.FreenetURI; + +import com.google.common.annotations.VisibleForTesting; + +/** + * Encapsulates the parts of a {@link FreenetURI} that do not change while + * being converted from SSK to USK and/or back. + * + * @author David ‘Bombe’ Roden + */ +public class Key { + + private final byte[] routingKey; + private final byte[] cryptoKey; + private final byte[] extra; + + private Key(byte[] routingKey, byte[] cryptoKey, byte[] extra) { + this.routingKey = routingKey; + this.cryptoKey = cryptoKey; + this.extra = extra; + } + + @VisibleForTesting + public String getRoutingKey() { + return encode(routingKey); + } + + @VisibleForTesting + public String getCryptoKey() { + return encode(cryptoKey); + } + + @VisibleForTesting + public String getExtra() { + return encode(extra); + } + + public FreenetURI toUsk(String docName, long edition, String... paths) { + return new FreenetURI("USK", docName, paths, routingKey, cryptoKey, + extra, edition); + } + + public FreenetURI toSsk(String docName, String... paths) { + return new FreenetURI("SSK", docName, paths, routingKey, cryptoKey, + extra); + } + + public FreenetURI toSsk(String docName, long edition, String... paths) { + return new FreenetURI("SSK", format("%s-%d", docName, edition), paths, + routingKey, cryptoKey, extra, edition); + } + + public static Key from(FreenetURI freenetURI) { + return new Key(freenetURI.getRoutingKey(), freenetURI.getCryptoKey(), + freenetURI.getExtra()); + } + + public static String routingKey(FreenetURI freenetURI) { + return from(freenetURI).getRoutingKey(); + } + +} diff --git a/src/main/java/net/pterodactylus/sone/freenet/PluginStoreConfigurationBackend.java b/src/main/java/net/pterodactylus/sone/freenet/PluginStoreConfigurationBackend.java index 1e95389..eb16a09 100644 --- a/src/main/java/net/pterodactylus/sone/freenet/PluginStoreConfigurationBackend.java +++ b/src/main/java/net/pterodactylus/sone/freenet/PluginStoreConfigurationBackend.java @@ -17,14 +17,15 @@ package net.pterodactylus.sone.freenet; +import static java.util.logging.Logger.getLogger; + import java.util.logging.Logger; import net.pterodactylus.util.config.AttributeNotFoundException; import net.pterodactylus.util.config.Configuration; import net.pterodactylus.util.config.ConfigurationException; import net.pterodactylus.util.config.ExtendedConfigurationBackend; -import net.pterodactylus.util.logging.Logging; -import freenet.client.async.DatabaseDisabledException; +import freenet.client.async.PersistenceDisabledException; import freenet.pluginmanager.PluginRespirator; import freenet.pluginmanager.PluginStore; @@ -37,7 +38,7 @@ public class PluginStoreConfigurationBackend implements ExtendedConfigurationBac /** The logger. */ @SuppressWarnings("unused") - private static final Logger logger = Logging.getLogger(PluginStoreConfigurationBackend.class); + private static final Logger logger = getLogger("Sone.Fred"); /** The plugin respirator. */ private final PluginRespirator pluginRespirator; @@ -50,15 +51,12 @@ public class PluginStoreConfigurationBackend implements ExtendedConfigurationBac * * @param pluginRespirator * The plugin respirator - * @throws DatabaseDisabledException + * @throws PersistenceDisabledException * if the plugin store is not available */ - public PluginStoreConfigurationBackend(PluginRespirator pluginRespirator) throws DatabaseDisabledException { + public PluginStoreConfigurationBackend(PluginRespirator pluginRespirator) throws PersistenceDisabledException { this.pluginRespirator = pluginRespirator; this.pluginStore = pluginRespirator.getStore(); - if (this.pluginStore == null) { - throw new DatabaseDisabledException(); - } } /** @@ -176,8 +174,8 @@ public class PluginStoreConfigurationBackend implements ExtendedConfigurationBac public void save() throws ConfigurationException { try { pluginRespirator.putStore(pluginStore); - } catch (DatabaseDisabledException dde1) { - throw new ConfigurationException("Could not store plugin store, database is disabled.", dde1); + } catch (PersistenceDisabledException pde1) { + throw new ConfigurationException("Could not store plugin store, persistence is disabled.", pde1); } } diff --git a/src/main/java/net/pterodactylus/sone/freenet/StringBucket.java b/src/main/java/net/pterodactylus/sone/freenet/StringBucket.java deleted file mode 100644 index c96a3cd..0000000 --- a/src/main/java/net/pterodactylus/sone/freenet/StringBucket.java +++ /dev/null @@ -1,146 +0,0 @@ -/* - * Sone - StringBucket.java - Copyright © 2010–2013 David Roden - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package net.pterodactylus.sone.freenet; - -import java.io.ByteArrayInputStream; -import java.io.InputStream; -import java.io.OutputStream; -import java.nio.charset.Charset; - -import com.db4o.ObjectContainer; - -import freenet.support.api.Bucket; - -/** - * {@link Bucket} implementation wrapped around a {@link String}. - * - * @author David ‘Bombe’ Roden - */ -public class StringBucket implements Bucket { - - /** The string to deliver. */ - private final String string; - - /** The encoding for the data. */ - private final Charset encoding; - - /** - * Creates a new string bucket using the default encoding. - * - * @param string - * The string to wrap - */ - public StringBucket(String string) { - this(string, Charset.defaultCharset()); - } - - /** - * Creates a new string bucket, using the given encoding to create a byte - * array from the string. - * - * @param string - * The string to wrap - * @param encoding - * The encoding of the data - */ - public StringBucket(String string, Charset encoding) { - this.string = string; - this.encoding = encoding; - } - - /** - * {@inheritDoc} - */ - @Override - public Bucket createShadow() { - return new StringBucket(string); - } - - /** - * {@inheritDoc} - */ - @Override - public void free() { - /* ignore. */ - } - - /** - * {@inheritDoc} - */ - @Override - public InputStream getInputStream() { - return new ByteArrayInputStream(string.getBytes(encoding)); - } - - /** - * {@inheritDoc} - */ - @Override - public String getName() { - return getClass().getName() + "@" + hashCode(); - } - - /** - * {@inheritDoc} - */ - @Override - public OutputStream getOutputStream() { - return null; - } - - /** - * {@inheritDoc} - */ - @Override - public boolean isReadOnly() { - return true; - } - - /** - * {@inheritDoc} - */ - @Override - public void removeFrom(ObjectContainer objectContainer) { - /* ignore. */ - } - - /** - * {@inheritDoc} - */ - @Override - public void setReadOnly() { - /* ignore, it is already read-only. */ - } - - /** - * {@inheritDoc} - */ - @Override - public long size() { - return string.getBytes(encoding).length; - } - - /** - * {@inheritDoc} - */ - @Override - public void storeTo(ObjectContainer objectContainer) { - /* ignore. */ - } - -} diff --git a/src/main/java/net/pterodactylus/sone/freenet/plugin/PluginConnector.java b/src/main/java/net/pterodactylus/sone/freenet/plugin/PluginConnector.java index 47330e8..56ac807 100644 --- a/src/main/java/net/pterodactylus/sone/freenet/plugin/PluginConnector.java +++ b/src/main/java/net/pterodactylus/sone/freenet/plugin/PluginConnector.java @@ -21,6 +21,7 @@ import net.pterodactylus.sone.freenet.plugin.event.ReceivedReplyEvent; import com.google.common.eventbus.EventBus; import com.google.inject.Inject; +import com.google.inject.Singleton; import freenet.pluginmanager.FredPluginTalker; import freenet.pluginmanager.PluginNotFoundException; @@ -35,6 +36,7 @@ import freenet.support.api.Bucket; * * @author David ‘Bombe’ Roden */ +@Singleton public class PluginConnector implements FredPluginTalker { /** The event bus. */ diff --git a/src/main/java/net/pterodactylus/sone/freenet/wot/Context.java b/src/main/java/net/pterodactylus/sone/freenet/wot/Context.java new file mode 100644 index 0000000..f72e56d --- /dev/null +++ b/src/main/java/net/pterodactylus/sone/freenet/wot/Context.java @@ -0,0 +1,50 @@ +/* + * Sone - Context.java - Copyright © 2014 David Roden + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.pterodactylus.sone.freenet.wot; + +import javax.annotation.Nullable; + +import com.google.common.base.Function; + +/** + * Custom container for the Web of Trust context. This allows easier + * configuration of dependency injection. + * + * @author David ‘Bombe’ Roden + */ +public class Context { + + public static final Function extractContext = new Function() { + @Nullable + @Override + public String apply(@Nullable Context context) { + return (context == null) ? null : context.getContext(); + } + }; + + private final String context; + + public Context(String context) { + this.context = context; + } + + public String getContext() { + return context; + } + +} diff --git a/src/main/java/net/pterodactylus/sone/freenet/wot/DefaultIdentity.java b/src/main/java/net/pterodactylus/sone/freenet/wot/DefaultIdentity.java index 0cfb055..dc44aab 100644 --- a/src/main/java/net/pterodactylus/sone/freenet/wot/DefaultIdentity.java +++ b/src/main/java/net/pterodactylus/sone/freenet/wot/DefaultIdentity.java @@ -69,151 +69,103 @@ public class DefaultIdentity implements Identity { // ACCESSORS // - /** - * {@inheritDoc} - */ @Override public String getId() { return id; } - /** - * {@inheritDoc} - */ @Override public String getNickname() { return nickname; } - /** - * {@inheritDoc} - */ @Override public String getRequestUri() { return requestUri; } - /** - * {@inheritDoc} - */ @Override public Set getContexts() { return Collections.unmodifiableSet(contexts); } - /** - * {@inheritDoc} - */ @Override public boolean hasContext(String context) { return contexts.contains(context); } - /** - * {@inheritDoc} - */ @Override public void setContexts(Collection contexts) { this.contexts.clear(); this.contexts.addAll(contexts); } - /** - * {@inheritDoc} - */ @Override - public void addContext(String context) { + public Identity addContext(String context) { contexts.add(context); + return this; } - /** - * {@inheritDoc} - */ @Override - public void removeContext(String context) { + public Identity removeContext(String context) { contexts.remove(context); + return this; } - /** - * {@inheritDoc} - */ @Override public Map getProperties() { return Collections.unmodifiableMap(properties); } - /** - * {@inheritDoc} - */ @Override public void setProperties(Map properties) { this.properties.clear(); this.properties.putAll(properties); } - /** - * {@inheritDoc} - */ @Override public String getProperty(String name) { return properties.get(name); } - /** - * {@inheritDoc} - */ @Override - public void setProperty(String name, String value) { + public Identity setProperty(String name, String value) { properties.put(name, value); + return this; } - /** - * {@inheritDoc} - */ @Override - public void removeProperty(String name) { + public Identity removeProperty(String name) { properties.remove(name); + return this; } - /** - * {@inheritDoc} - */ @Override public Trust getTrust(OwnIdentity ownIdentity) { return trustCache.get(ownIdentity); } - /** - * {@inheritDoc} - */ @Override - public void setTrust(OwnIdentity ownIdentity, Trust trust) { + public Identity setTrust(OwnIdentity ownIdentity, Trust trust) { trustCache.put(ownIdentity, trust); + return this; } - /** - * {@inheritDoc} - */ @Override - public void removeTrust(OwnIdentity ownIdentity) { + public Identity removeTrust(OwnIdentity ownIdentity) { trustCache.remove(ownIdentity); + return this; } // // OBJECT METHODS // - /** - * {@inheritDoc} - */ @Override public int hashCode() { return getId().hashCode(); } - /** - * {@inheritDoc} - */ @Override public boolean equals(Object object) { if (!(object instanceof Identity)) { @@ -223,9 +175,6 @@ public class DefaultIdentity implements Identity { return identity.getId().equals(getId()); } - /** - * {@inheritDoc} - */ @Override public String toString() { return getClass().getSimpleName() + "[id=" + id + ",nickname=" + nickname + ",contexts=" + contexts + ",properties=" + properties + "]"; diff --git a/src/main/java/net/pterodactylus/sone/freenet/wot/DefaultOwnIdentity.java b/src/main/java/net/pterodactylus/sone/freenet/wot/DefaultOwnIdentity.java index 348cd8c..4a842e9 100644 --- a/src/main/java/net/pterodactylus/sone/freenet/wot/DefaultOwnIdentity.java +++ b/src/main/java/net/pterodactylus/sone/freenet/wot/DefaultOwnIdentity.java @@ -17,6 +17,8 @@ package net.pterodactylus.sone.freenet.wot; +import static com.google.common.base.Preconditions.checkNotNull; + /** * An own identity is an identity that the owner of the node has full control * over. @@ -42,34 +44,38 @@ public class DefaultOwnIdentity extends DefaultIdentity implements OwnIdentity { */ public DefaultOwnIdentity(String id, String nickname, String requestUri, String insertUri) { super(id, nickname, requestUri); - this.insertUri = insertUri; - } - - /** - * Copy constructor for an own identity. - * - * @param ownIdentity - * The own identity to copy - */ - public DefaultOwnIdentity(OwnIdentity ownIdentity) { - super(ownIdentity.getId(), ownIdentity.getNickname(), ownIdentity.getRequestUri()); - this.insertUri = ownIdentity.getInsertUri(); - setContexts(ownIdentity.getContexts()); - setProperties(ownIdentity.getProperties()); + this.insertUri = checkNotNull(insertUri); } // // ACCESSORS // - /** - * {@inheritDoc} - */ @Override public String getInsertUri() { return insertUri; } + @Override + public OwnIdentity addContext(String context) { + return (OwnIdentity) super.addContext(context); + } + + @Override + public OwnIdentity removeContext(String context) { + return (OwnIdentity) super.removeContext(context); + } + + @Override + public OwnIdentity setProperty(String name, String value) { + return (OwnIdentity) super.setProperty(name, value); + } + + @Override + public OwnIdentity removeProperty(String name) { + return (OwnIdentity) super.removeProperty(name); + } + // // OBJECT METHODS // diff --git a/src/main/java/net/pterodactylus/sone/freenet/wot/Identity.java b/src/main/java/net/pterodactylus/sone/freenet/wot/Identity.java index bc594f8..d0cafc5 100644 --- a/src/main/java/net/pterodactylus/sone/freenet/wot/Identity.java +++ b/src/main/java/net/pterodactylus/sone/freenet/wot/Identity.java @@ -18,9 +18,12 @@ package net.pterodactylus.sone.freenet.wot; import java.util.Collection; +import java.util.Collections; import java.util.Map; import java.util.Set; +import com.google.common.base.Function; + /** * Interface for web of trust identities, defining all functions that can be * performed on an identity. An identity is only a container for identity data @@ -30,6 +33,20 @@ import java.util.Set; */ public interface Identity { + public static final Function> TO_CONTEXTS = new Function>() { + @Override + public Set apply(Identity identity) { + return (identity == null) ? Collections.emptySet() : identity.getContexts(); + } + }; + + public static final Function> TO_PROPERTIES = new Function>() { + @Override + public Map apply(Identity input) { + return (input == null) ? Collections.emptyMap() : input.getProperties(); + } + }; + /** * Returns the ID of the identity. * @@ -74,7 +91,7 @@ public interface Identity { * @param context * The context to add */ - public void addContext(String context); + public Identity addContext(String context); /** * Sets all contexts of this identity. @@ -90,7 +107,7 @@ public interface Identity { * @param context * The context to remove */ - public void removeContext(String context); + public Identity removeContext(String context); /** * Returns all properties of this identity. @@ -116,7 +133,7 @@ public interface Identity { * @param value * The value of the property */ - public void setProperty(String name, String value); + public Identity setProperty(String name, String value); /** * Sets all properties of this identity. @@ -132,7 +149,7 @@ public interface Identity { * @param name * The name of the property to remove */ - public void removeProperty(String name); + public Identity removeProperty(String name); /** * Retrieves the trust that this identity receives from the given own @@ -155,7 +172,7 @@ public interface Identity { * @param trust * The trust given by the given own identity */ - public void setTrust(OwnIdentity ownIdentity, Trust trust); + public Identity setTrust(OwnIdentity ownIdentity, Trust trust); /** * Removes trust assignment from the given own identity for this identity. @@ -164,6 +181,6 @@ public interface Identity { * The own identity that removed the trust assignment for this * identity */ - public void removeTrust(OwnIdentity ownIdentity); + public Identity removeTrust(OwnIdentity ownIdentity); } diff --git a/src/main/java/net/pterodactylus/sone/freenet/wot/IdentityChangeDetector.java b/src/main/java/net/pterodactylus/sone/freenet/wot/IdentityChangeDetector.java new file mode 100644 index 0000000..a8cb62b --- /dev/null +++ b/src/main/java/net/pterodactylus/sone/freenet/wot/IdentityChangeDetector.java @@ -0,0 +1,200 @@ +/* + * Sone - IdentityChangeDetector.java - Copyright © 2013 David Roden + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.pterodactylus.sone.freenet.wot; + +import static com.google.common.base.Optional.absent; +import static com.google.common.base.Optional.fromNullable; +import static com.google.common.base.Predicates.not; +import static com.google.common.collect.FluentIterable.from; +import static net.pterodactylus.sone.freenet.wot.Identity.TO_CONTEXTS; +import static net.pterodactylus.sone.freenet.wot.Identity.TO_PROPERTIES; + +import java.util.Collection; +import java.util.Map; +import java.util.Map.Entry; + +import com.google.common.base.Optional; +import com.google.common.base.Predicate; +import com.google.common.collect.FluentIterable; +import com.google.common.collect.ImmutableMap; + +/** + * Detects changes between two lists of {@link Identity}s. The detector can find + * added and removed identities, and for identities that exist in both list + * their contexts and properties are checked for added, removed, or (in case of + * properties) changed values. + * + * @author David ‘Bombe’ Roden + */ +public class IdentityChangeDetector { + + private final Map oldIdentities; + private Optional onNewIdentity = absent(); + private Optional onRemovedIdentity = absent(); + private Optional onChangedIdentity = absent(); + private Optional onUnchangedIdentity = absent(); + + public IdentityChangeDetector(Collection oldIdentities) { + this.oldIdentities = convertToMap(oldIdentities); + } + + public void onNewIdentity(IdentityProcessor onNewIdentity) { + this.onNewIdentity = fromNullable(onNewIdentity); + } + + public void onRemovedIdentity(IdentityProcessor onRemovedIdentity) { + this.onRemovedIdentity = fromNullable(onRemovedIdentity); + } + + public void onChangedIdentity(IdentityProcessor onChangedIdentity) { + this.onChangedIdentity = fromNullable(onChangedIdentity); + } + + public void onUnchangedIdentity(IdentityProcessor onUnchangedIdentity) { + this.onUnchangedIdentity = fromNullable(onUnchangedIdentity); + } + + public void detectChanges(final Collection newIdentities) { + notifyForRemovedIdentities(from(oldIdentities.values()).filter(notContainedIn(newIdentities))); + notifyForNewIdentities(from(newIdentities).filter(notContainedIn(oldIdentities.values()))); + notifyForChangedIdentities(from(newIdentities).filter(containedIn(oldIdentities)).filter(hasChanged(oldIdentities))); + notifyForUnchangedIdentities(from(newIdentities).filter(containedIn(oldIdentities)).filter(not(hasChanged(oldIdentities)))); + } + + private void notifyForRemovedIdentities(Iterable identities) { + notify(onRemovedIdentity, identities); + } + + private void notifyForNewIdentities(FluentIterable newIdentities) { + notify(onNewIdentity, newIdentities); + } + + private void notifyForChangedIdentities(FluentIterable identities) { + notify(onChangedIdentity, identities); + } + + private void notifyForUnchangedIdentities(FluentIterable identities) { + notify(onUnchangedIdentity, identities); + } + + private void notify(Optional identityProcessor, Iterable identities) { + if (!identityProcessor.isPresent()) { + return; + } + for (Identity identity : identities) { + identityProcessor.get().processIdentity(identity); + } + } + + private static Predicate hasChanged(final Map oldIdentities) { + return new Predicate() { + @Override + public boolean apply(Identity identity) { + return (identity == null) ? false : identityHasChanged(oldIdentities.get(identity.getId()), identity); + } + }; + } + + private static boolean identityHasChanged(Identity oldIdentity, Identity newIdentity) { + return identityHasNewContexts(oldIdentity, newIdentity) + || identityHasRemovedContexts(oldIdentity, newIdentity) + || identityHasNewProperties(oldIdentity, newIdentity) + || identityHasRemovedProperties(oldIdentity, newIdentity) + || identityHasChangedProperties(oldIdentity, newIdentity); + } + + private static boolean identityHasNewContexts(Identity oldIdentity, Identity newIdentity) { + return from(TO_CONTEXTS.apply(newIdentity)).anyMatch(notAContextOf(oldIdentity)); + } + + private static boolean identityHasRemovedContexts(Identity oldIdentity, Identity newIdentity) { + return from(TO_CONTEXTS.apply(oldIdentity)).anyMatch(notAContextOf(newIdentity)); + } + + private static boolean identityHasNewProperties(Identity oldIdentity, Identity newIdentity) { + return from(TO_PROPERTIES.apply(newIdentity).entrySet()).anyMatch(notAPropertyOf(oldIdentity)); + } + + private static boolean identityHasRemovedProperties(Identity oldIdentity, Identity newIdentity) { + return from(TO_PROPERTIES.apply(oldIdentity).entrySet()).anyMatch(notAPropertyOf(newIdentity)); + } + + private static boolean identityHasChangedProperties(Identity oldIdentity, Identity newIdentity) { + return from(TO_PROPERTIES.apply(oldIdentity).entrySet()).anyMatch(hasADifferentValueThanIn(newIdentity)); + } + + private static Predicate containedIn(final Map identities) { + return new Predicate() { + @Override + public boolean apply(Identity identity) { + return identities.containsKey(identity.getId()); + } + }; + } + + private static Predicate notAContextOf(final Identity identity) { + return new Predicate() { + @Override + public boolean apply(String context) { + return (identity == null) ? false : !identity.getContexts().contains(context); + } + }; + } + + private static Predicate notContainedIn(final Collection newIdentities) { + return new Predicate() { + @Override + public boolean apply(Identity identity) { + return (identity == null) ? false : !newIdentities.contains(identity); + } + }; + } + + private static Predicate> notAPropertyOf(final Identity identity) { + return new Predicate>() { + @Override + public boolean apply(Entry property) { + return (property == null) ? false : !identity.getProperties().containsKey(property.getKey()); + } + }; + } + + private static Predicate> hasADifferentValueThanIn(final Identity newIdentity) { + return new Predicate>() { + @Override + public boolean apply(Entry property) { + return (property == null) ? false : !newIdentity.getProperty(property.getKey()).equals(property.getValue()); + } + }; + } + + private static Map convertToMap(Collection identities) { + ImmutableMap.Builder mapBuilder = ImmutableMap.builder(); + for (Identity identity : identities) { + mapBuilder.put(identity.getId(), identity); + } + return mapBuilder.build(); + } + + public interface IdentityProcessor { + + void processIdentity(Identity identity); + + } + +} diff --git a/src/main/java/net/pterodactylus/sone/freenet/wot/IdentityChangeEventSender.java b/src/main/java/net/pterodactylus/sone/freenet/wot/IdentityChangeEventSender.java new file mode 100644 index 0000000..d087dec --- /dev/null +++ b/src/main/java/net/pterodactylus/sone/freenet/wot/IdentityChangeEventSender.java @@ -0,0 +1,133 @@ +/* + * Sone - IdentityChangeEventSender.java - Copyright © 2013 David Roden + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.pterodactylus.sone.freenet.wot; + +import java.util.Collection; +import java.util.Map; + +import net.pterodactylus.sone.freenet.wot.IdentityChangeDetector.IdentityProcessor; +import net.pterodactylus.sone.freenet.wot.event.IdentityAddedEvent; +import net.pterodactylus.sone.freenet.wot.event.IdentityRemovedEvent; +import net.pterodactylus.sone.freenet.wot.event.IdentityUpdatedEvent; +import net.pterodactylus.sone.freenet.wot.event.OwnIdentityAddedEvent; +import net.pterodactylus.sone.freenet.wot.event.OwnIdentityRemovedEvent; + +import com.google.common.eventbus.EventBus; + +/** + * Detects changes in {@link Identity}s trusted my multiple {@link + * OwnIdentity}s. + * + * @author David ‘Bombe’ Roden + * @see IdentityChangeDetector + */ +public class IdentityChangeEventSender { + + private final EventBus eventBus; + private final Map> oldIdentities; + + public IdentityChangeEventSender(EventBus eventBus, Map> oldIdentities) { + this.eventBus = eventBus; + this.oldIdentities = oldIdentities; + } + + public void detectChanges(Map> identities) { + IdentityChangeDetector identityChangeDetector = new IdentityChangeDetector(oldIdentities.keySet()); + identityChangeDetector.onNewIdentity(addNewOwnIdentityAndItsTrustedIdentities(identities)); + identityChangeDetector.onRemovedIdentity(removeOwnIdentityAndItsTrustedIdentities(oldIdentities)); + identityChangeDetector.onUnchangedIdentity(detectChangesInTrustedIdentities(identities, oldIdentities)); + identityChangeDetector.detectChanges(identities.keySet()); + } + + private IdentityProcessor addNewOwnIdentityAndItsTrustedIdentities(final Map> newIdentities) { + return new IdentityProcessor() { + @Override + public void processIdentity(Identity identity) { + eventBus.post(new OwnIdentityAddedEvent((OwnIdentity) identity)); + for (Identity newIdentity : newIdentities.get((OwnIdentity) identity)) { + eventBus.post(new IdentityAddedEvent((OwnIdentity) identity, newIdentity)); + } + } + }; + } + + private IdentityProcessor removeOwnIdentityAndItsTrustedIdentities(final Map> oldIdentities) { + return new IdentityProcessor() { + @Override + public void processIdentity(Identity identity) { + eventBus.post(new OwnIdentityRemovedEvent((OwnIdentity) identity)); + for (Identity removedIdentity : oldIdentities.get((OwnIdentity) identity)) { + eventBus.post(new IdentityRemovedEvent((OwnIdentity) identity, removedIdentity)); + } + } + }; + } + + private IdentityProcessor detectChangesInTrustedIdentities(Map> newIdentities, Map> oldIdentities) { + return new DefaultIdentityProcessor(oldIdentities, newIdentities); + } + + private class DefaultIdentityProcessor implements IdentityProcessor { + + private final Map> oldIdentities; + private final Map> newIdentities; + + public DefaultIdentityProcessor(Map> oldIdentities, Map> newIdentities) { + this.oldIdentities = oldIdentities; + this.newIdentities = newIdentities; + } + + @Override + public void processIdentity(Identity ownIdentity) { + IdentityChangeDetector identityChangeDetector = new IdentityChangeDetector(oldIdentities.get((OwnIdentity) ownIdentity)); + identityChangeDetector.onNewIdentity(notifyForAddedIdentities((OwnIdentity) ownIdentity)); + identityChangeDetector.onRemovedIdentity(notifyForRemovedIdentities((OwnIdentity) ownIdentity)); + identityChangeDetector.onChangedIdentity(notifyForChangedIdentities((OwnIdentity) ownIdentity)); + identityChangeDetector.detectChanges(newIdentities.get((OwnIdentity) ownIdentity)); + } + + private IdentityProcessor notifyForChangedIdentities(final OwnIdentity ownIdentity) { + return new IdentityProcessor() { + @Override + public void processIdentity(Identity identity) { + eventBus.post(new IdentityUpdatedEvent(ownIdentity, identity)); + } + }; + } + + private IdentityProcessor notifyForRemovedIdentities(final OwnIdentity ownIdentity) { + return new IdentityProcessor() { + @Override + public void processIdentity(Identity identity) { + eventBus.post(new IdentityRemovedEvent(ownIdentity, identity)); + } + }; + } + + private IdentityProcessor notifyForAddedIdentities(final OwnIdentity ownIdentity) { + return new IdentityProcessor() { + @Override + public void processIdentity(Identity identity) { + eventBus.post(new IdentityAddedEvent(ownIdentity, identity)); + } + }; + } + + } + +} diff --git a/src/main/java/net/pterodactylus/sone/freenet/wot/IdentityLoader.java b/src/main/java/net/pterodactylus/sone/freenet/wot/IdentityLoader.java new file mode 100644 index 0000000..75ec828 --- /dev/null +++ b/src/main/java/net/pterodactylus/sone/freenet/wot/IdentityLoader.java @@ -0,0 +1,79 @@ +/* + * Sone - IdentityLoader.java - Copyright © 2013 David Roden + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.pterodactylus.sone.freenet.wot; + +import static java.util.Collections.emptySet; +import static net.pterodactylus.sone.freenet.wot.Context.extractContext; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import net.pterodactylus.sone.freenet.plugin.PluginException; + +import com.google.common.base.Optional; +import com.google.inject.Inject; + +/** + * Loads {@link OwnIdentity}s and the {@link Identity}s they trust. + * + * @author David ‘Bombe’ Roden + */ +public class IdentityLoader { + + private final WebOfTrustConnector webOfTrustConnector; + private final Optional context; + + public IdentityLoader(WebOfTrustConnector webOfTrustConnector) { + this(webOfTrustConnector, Optional.absent()); + } + + @Inject + public IdentityLoader(WebOfTrustConnector webOfTrustConnector, Optional context) { + this.webOfTrustConnector = webOfTrustConnector; + this.context = context; + } + + public Map> loadIdentities() throws WebOfTrustException { + Collection currentOwnIdentities = webOfTrustConnector.loadAllOwnIdentities(); + return loadTrustedIdentitiesForOwnIdentities(currentOwnIdentities); + } + + private Map> loadTrustedIdentitiesForOwnIdentities(Collection ownIdentities) throws PluginException { + Map> currentIdentities = new HashMap>(); + + for (OwnIdentity ownIdentity : ownIdentities) { + if (identityDoesNotHaveTheCorrectContext(ownIdentity)) { + currentIdentities.put(ownIdentity, Collections.emptySet()); + continue; + } + + Set trustedIdentities = webOfTrustConnector.loadTrustedIdentities(ownIdentity, context.transform(extractContext)); + currentIdentities.put(ownIdentity, trustedIdentities); + } + + return currentIdentities; + } + + private boolean identityDoesNotHaveTheCorrectContext(OwnIdentity ownIdentity) { + return context.isPresent() && !ownIdentity.hasContext(context.transform(extractContext).get()); + } + +} diff --git a/src/main/java/net/pterodactylus/sone/freenet/wot/IdentityManager.java b/src/main/java/net/pterodactylus/sone/freenet/wot/IdentityManager.java index 77e0480..d3ba606 100644 --- a/src/main/java/net/pterodactylus/sone/freenet/wot/IdentityManager.java +++ b/src/main/java/net/pterodactylus/sone/freenet/wot/IdentityManager.java @@ -1,323 +1,22 @@ -/* - * Sone - IdentityManager.java - Copyright © 2010–2013 David Roden - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - package net.pterodactylus.sone.freenet.wot; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Map.Entry; import java.util.Set; -import java.util.logging.Level; -import java.util.logging.Logger; -import net.pterodactylus.sone.freenet.plugin.PluginException; -import net.pterodactylus.sone.freenet.wot.event.IdentityAddedEvent; -import net.pterodactylus.sone.freenet.wot.event.IdentityRemovedEvent; -import net.pterodactylus.sone.freenet.wot.event.IdentityUpdatedEvent; -import net.pterodactylus.sone.freenet.wot.event.OwnIdentityAddedEvent; -import net.pterodactylus.sone.freenet.wot.event.OwnIdentityRemovedEvent; -import net.pterodactylus.util.logging.Logging; -import net.pterodactylus.util.service.AbstractService; +import net.pterodactylus.util.service.Service; import com.google.common.eventbus.EventBus; -import com.google.inject.Inject; -import com.google.inject.name.Named; +import com.google.inject.ImplementedBy; /** - * The identity manager takes care of loading and storing identities, their - * contexts, and properties. It does so in a way that does not expose errors via - * exceptions but it only logs them and tries to return sensible defaults. - *

- * It is also responsible for polling identities from the Web of Trust plugin - * and sending events to the {@link EventBus} when {@link Identity}s and - * {@link OwnIdentity}s are discovered or disappearing. + * Connects to a {@link WebOfTrustConnector} and sends identity events to an + * {@link EventBus}. * * @author David ‘Bombe’ Roden */ -public class IdentityManager extends AbstractService { - - /** Object used for synchronization. */ - @SuppressWarnings("hiding") - private final Object syncObject = new Object() { - /* inner class for better lock names. */ - }; - - /** The logger. */ - private static final Logger logger = Logging.getLogger(IdentityManager.class); - - /** The event bus. */ - private final EventBus eventBus; - - /** The Web of Trust connector. */ - private final WebOfTrustConnector webOfTrustConnector; - - /** The context to filter for. */ - private final String context; - - /** The currently known own identities. */ - /* synchronize access on syncObject. */ - private final Map currentOwnIdentities = new HashMap(); - - /** The last time all identities were loaded. */ - private volatile long identitiesLastLoaded; - - /** - * Creates a new identity manager. - * - * @param eventBus - * The event bus - * @param webOfTrustConnector - * The Web of Trust connector - * @param context - * The context to focus on (may be {@code null} to ignore - * contexts) - */ - @Inject - public IdentityManager(EventBus eventBus, WebOfTrustConnector webOfTrustConnector, @Named("WebOfTrustContext") String context) { - super("Sone Identity Manager", false); - this.eventBus = eventBus; - this.webOfTrustConnector = webOfTrustConnector; - this.context = context; - } - - // - // ACCESSORS - // - - /** - * Returns the last time all identities were loaded. - * - * @return The last time all identities were loaded (in milliseconds since - * Jan 1, 1970 UTC) - */ - public long getIdentitiesLastLoaded() { - return identitiesLastLoaded; - } - - /** - * Returns whether the Web of Trust plugin could be reached during the last - * try. - * - * @return {@code true} if the Web of Trust plugin is connected, - * {@code false} otherwise - */ - public boolean isConnected() { - try { - webOfTrustConnector.ping(); - return true; - } catch (PluginException pe1) { - /* not connected, ignore. */ - return false; - } - } - - /** - * Returns the own identity with the given ID. - * - * @param id - * The ID of the own identity - * @return The own identity, or {@code null} if there is no such identity - */ - public OwnIdentity getOwnIdentity(String id) { - Set allOwnIdentities = getAllOwnIdentities(); - for (OwnIdentity ownIdentity : allOwnIdentities) { - if (ownIdentity.getId().equals(id)) { - return new DefaultOwnIdentity(ownIdentity); - } - } - return null; - } - - /** - * Returns all own identities. - * - * @return All own identities - */ - public Set getAllOwnIdentities() { - return new HashSet(currentOwnIdentities.values()); - } - - // - // SERVICE METHODS - // - - /** - * {@inheritDoc} - */ - @Override - protected void serviceRun() { - Map> oldIdentities = Collections.emptyMap(); - while (!shouldStop()) { - Map> currentIdentities = new HashMap>(); - Map currentOwnIdentities = new HashMap(); - - Set ownIdentities = null; - boolean identitiesLoaded = false; - try { - /* get all identities with the wanted context from WoT. */ - logger.finer("Getting all Own Identities from WoT..."); - ownIdentities = webOfTrustConnector.loadAllOwnIdentities(); - logger.finest(String.format("Loaded %d Own Identities.", ownIdentities.size())); - - /* load trusted identities. */ - for (OwnIdentity ownIdentity : ownIdentities) { - currentOwnIdentities.put(ownIdentity.getId(), ownIdentity); - Map identities = new HashMap(); - currentIdentities.put(ownIdentity, identities); - - /* - * if the context doesn’t match, skip getting trusted - * identities. - */ - if ((context != null) && !ownIdentity.hasContext(context)) { - continue; - } - - /* load trusted identities. */ - logger.finer(String.format("Getting trusted identities for %s...", ownIdentity.getId())); - Set trustedIdentities = webOfTrustConnector.loadTrustedIdentities(ownIdentity, context); - logger.finest(String.format("Got %d trusted identities.", trustedIdentities.size())); - for (Identity identity : trustedIdentities) { - identities.put(identity.getId(), identity); - } - } - identitiesLoaded = true; - identitiesLastLoaded = System.currentTimeMillis(); - } catch (WebOfTrustException wote1) { - logger.log(Level.WARNING, "WoT has disappeared!", wote1); - } - - if (identitiesLoaded) { - - /* check for changes. */ - checkOwnIdentities(currentOwnIdentities); - - /* now check for changes in remote identities. */ - for (OwnIdentity ownIdentity : currentOwnIdentities.values()) { - - /* find new identities. */ - for (Identity currentIdentity : currentIdentities.get(ownIdentity).values()) { - if (!oldIdentities.containsKey(ownIdentity) || !oldIdentities.get(ownIdentity).containsKey(currentIdentity.getId())) { - logger.finest(String.format("Identity added for %s: %s", ownIdentity.getId(), currentIdentity)); - eventBus.post(new IdentityAddedEvent(ownIdentity, currentIdentity)); - } - } - - /* find removed identities. */ - if (oldIdentities.containsKey(ownIdentity)) { - for (Identity oldIdentity : oldIdentities.get(ownIdentity).values()) { - if (!currentIdentities.get(ownIdentity).containsKey(oldIdentity.getId())) { - logger.finest(String.format("Identity removed for %s: %s", ownIdentity.getId(), oldIdentity)); - eventBus.post(new IdentityRemovedEvent(ownIdentity, oldIdentity)); - } - } - - /* check for changes in the contexts. */ - for (Identity oldIdentity : oldIdentities.get(ownIdentity).values()) { - if (!currentIdentities.get(ownIdentity).containsKey(oldIdentity.getId())) { - continue; - } - Identity newIdentity = currentIdentities.get(ownIdentity).get(oldIdentity.getId()); - Set oldContexts = oldIdentity.getContexts(); - Set newContexts = newIdentity.getContexts(); - if (oldContexts.size() != newContexts.size()) { - logger.finest(String.format("Contexts changed for %s: was: %s, is now: %s", ownIdentity.getId(), oldContexts, newContexts)); - eventBus.post(new IdentityUpdatedEvent(ownIdentity, newIdentity)); - continue; - } - for (String oldContext : oldContexts) { - if (!newContexts.contains(oldContext)) { - logger.finest(String.format("Context was removed for %s: %s", ownIdentity.getId(), oldContext)); - eventBus.post(new IdentityUpdatedEvent(ownIdentity, newIdentity)); - break; - } - } - } - - /* check for changes in the properties. */ - for (Identity oldIdentity : oldIdentities.get(ownIdentity).values()) { - if (!currentIdentities.get(ownIdentity).containsKey(oldIdentity.getId())) { - continue; - } - Identity newIdentity = currentIdentities.get(ownIdentity).get(oldIdentity.getId()); - Map oldProperties = oldIdentity.getProperties(); - Map newProperties = newIdentity.getProperties(); - if (oldProperties.size() != newProperties.size()) { - logger.finest(String.format("Properties changed for %s: was: %s, is now: %s", ownIdentity.getId(), oldProperties, newProperties)); - eventBus.post(new IdentityUpdatedEvent(ownIdentity, newIdentity)); - continue; - } - for (Entry oldProperty : oldProperties.entrySet()) { - if (!newProperties.containsKey(oldProperty.getKey()) || !newProperties.get(oldProperty.getKey()).equals(oldProperty.getValue())) { - logger.finest(String.format("Property was removed for %s: %s", ownIdentity.getId(), oldProperty)); - eventBus.post(new IdentityUpdatedEvent(ownIdentity, newIdentity)); - break; - } - } - } - } - } - - /* remember the current set of identities. */ - oldIdentities = currentIdentities; - } - - /* wait a minute before checking again. */ - sleep(60 * 1000); - } - } - - // - // PRIVATE METHODS - // - - /** - * Checks the given new list of own identities for added or removed own - * identities, as compared to {@link #currentOwnIdentities}. - * - * @param newOwnIdentities - * The new own identities - */ - private void checkOwnIdentities(Map newOwnIdentities) { - synchronized (syncObject) { - - /* find removed own identities: */ - for (OwnIdentity oldOwnIdentity : currentOwnIdentities.values()) { - OwnIdentity newOwnIdentity = newOwnIdentities.get(oldOwnIdentity.getId()); - if ((newOwnIdentity == null) || ((context != null) && oldOwnIdentity.hasContext(context) && !newOwnIdentity.hasContext(context))) { - logger.finest(String.format("Own Identity removed: %s", oldOwnIdentity)); - eventBus.post(new OwnIdentityRemovedEvent(new DefaultOwnIdentity(oldOwnIdentity))); - } - } - - /* find added own identities. */ - for (OwnIdentity currentOwnIdentity : newOwnIdentities.values()) { - OwnIdentity oldOwnIdentity = currentOwnIdentities.get(currentOwnIdentity.getId()); - if (((oldOwnIdentity == null) && ((context == null) || currentOwnIdentity.hasContext(context))) || ((oldOwnIdentity != null) && (context != null) && (!oldOwnIdentity.hasContext(context) && currentOwnIdentity.hasContext(context)))) { - logger.finest(String.format("Own Identity added: %s", currentOwnIdentity)); - eventBus.post(new OwnIdentityAddedEvent(new DefaultOwnIdentity(currentOwnIdentity))); - } - } +@ImplementedBy(IdentityManagerImpl.class) +public interface IdentityManager extends Service { - currentOwnIdentities.clear(); - currentOwnIdentities.putAll(newOwnIdentities); - } - } + boolean isConnected(); + Set getAllOwnIdentities(); } diff --git a/src/main/java/net/pterodactylus/sone/freenet/wot/IdentityManagerImpl.java b/src/main/java/net/pterodactylus/sone/freenet/wot/IdentityManagerImpl.java new file mode 100644 index 0000000..4c94749 --- /dev/null +++ b/src/main/java/net/pterodactylus/sone/freenet/wot/IdentityManagerImpl.java @@ -0,0 +1,149 @@ +/* + * Sone - IdentityManager.java - Copyright © 2010–2013 David Roden + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.pterodactylus.sone.freenet.wot; + +import static java.util.logging.Logger.getLogger; + +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; + +import net.pterodactylus.sone.freenet.plugin.PluginException; +import net.pterodactylus.util.service.AbstractService; + +import com.google.common.collect.Sets; +import com.google.common.eventbus.EventBus; +import com.google.inject.Inject; +import com.google.inject.Singleton; + +/** + * The identity manager takes care of loading and storing identities, their + * contexts, and properties. It does so in a way that does not expose errors via + * exceptions but it only logs them and tries to return sensible defaults. + *

+ * It is also responsible for polling identities from the Web of Trust plugin + * and sending events to the {@link EventBus} when {@link Identity}s and + * {@link OwnIdentity}s are discovered or disappearing. + * + * @author David ‘Bombe’ Roden + */ +@Singleton +public class IdentityManagerImpl extends AbstractService implements IdentityManager { + + /** The logger. */ + private static final Logger logger = getLogger("Sone.Identities"); + + /** The event bus. */ + private final EventBus eventBus; + + private final IdentityLoader identityLoader; + + /** The Web of Trust connector. */ + private final WebOfTrustConnector webOfTrustConnector; + + /** The currently known own identities. */ + private final Set currentOwnIdentities = Sets.newHashSet(); + + /** + * Creates a new identity manager. + * + * @param eventBus + * The event bus + * @param webOfTrustConnector + * The Web of Trust connector + */ + @Inject + public IdentityManagerImpl(EventBus eventBus, WebOfTrustConnector webOfTrustConnector, IdentityLoader identityLoader) { + super("Sone Identity Manager", false); + this.eventBus = eventBus; + this.webOfTrustConnector = webOfTrustConnector; + this.identityLoader = identityLoader; + } + + // + // ACCESSORS + // + + /** + * Returns whether the Web of Trust plugin could be reached during the last + * try. + * + * @return {@code true} if the Web of Trust plugin is connected, + * {@code false} otherwise + */ + @Override + public boolean isConnected() { + try { + webOfTrustConnector.ping(); + return true; + } catch (PluginException pe1) { + /* not connected, ignore. */ + return false; + } + } + + /** + * Returns all own identities. + * + * @return All own identities + */ + @Override + public Set getAllOwnIdentities() { + synchronized (currentOwnIdentities) { + return new HashSet(currentOwnIdentities); + } + } + + // + // SERVICE METHODS + // + + /** + * {@inheritDoc} + */ + @Override + protected void serviceRun() { + Map> oldIdentities = new HashMap>(); + + while (!shouldStop()) { + try { + Map> currentIdentities = identityLoader.loadIdentities(); + + IdentityChangeEventSender identityChangeEventSender = new IdentityChangeEventSender(eventBus, oldIdentities); + identityChangeEventSender.detectChanges(currentIdentities); + + oldIdentities = currentIdentities; + + synchronized (currentOwnIdentities) { + currentOwnIdentities.clear(); + currentOwnIdentities.addAll(currentIdentities.keySet()); + } + } catch (WebOfTrustException wote1) { + logger.log(Level.WARNING, "WoT has disappeared!", wote1); + } + + /* wait a minute before checking again. */ + sleep(60 * 1000); + } + } + +} diff --git a/src/main/java/net/pterodactylus/sone/freenet/wot/OwnIdentity.java b/src/main/java/net/pterodactylus/sone/freenet/wot/OwnIdentity.java index 6fc7044..da6409b 100644 --- a/src/main/java/net/pterodactylus/sone/freenet/wot/OwnIdentity.java +++ b/src/main/java/net/pterodactylus/sone/freenet/wot/OwnIdentity.java @@ -32,4 +32,9 @@ public interface OwnIdentity extends Identity { */ public String getInsertUri(); + public OwnIdentity addContext(String context); + public OwnIdentity removeContext(String context); + public OwnIdentity setProperty(String name, String value); + public OwnIdentity removeProperty(String name); + } diff --git a/src/main/java/net/pterodactylus/sone/freenet/wot/Trust.java b/src/main/java/net/pterodactylus/sone/freenet/wot/Trust.java index 6fa37d0..fe76846 100644 --- a/src/main/java/net/pterodactylus/sone/freenet/wot/Trust.java +++ b/src/main/java/net/pterodactylus/sone/freenet/wot/Trust.java @@ -17,6 +17,8 @@ package net.pterodactylus.sone.freenet.wot; +import static com.google.common.base.Objects.equal; + /** * Container class for trust in the web of trust. * @@ -79,9 +81,16 @@ public class Trust { return distance; } - /** - * {@inheritDoc} - */ + @Override + public boolean equals(Object object) { + if (!(object instanceof Trust)) { + return false; + } + Trust trust = (Trust) object; + return equal(getExplicit(), trust.getExplicit()) && equal(getImplicit(), trust.getImplicit()) && equal(getDistance(), trust.getDistance()); + } + + /** {@inheritDoc} */ @Override public String toString() { return getClass().getName() + "[explicit=" + explicit + ",implicit=" + implicit + ",distance=" + distance + "]"; diff --git a/src/main/java/net/pterodactylus/sone/freenet/wot/WebOfTrustConnector.java b/src/main/java/net/pterodactylus/sone/freenet/wot/WebOfTrustConnector.java index 07628e3..3b30a4e 100644 --- a/src/main/java/net/pterodactylus/sone/freenet/wot/WebOfTrustConnector.java +++ b/src/main/java/net/pterodactylus/sone/freenet/wot/WebOfTrustConnector.java @@ -17,6 +17,9 @@ package net.pterodactylus.sone.freenet.wot; +import static java.util.logging.Logger.getLogger; +import static net.pterodactylus.sone.utils.NumberParsers.parseInt; + import java.util.HashMap; import java.util.HashSet; import java.util.Map; @@ -28,12 +31,12 @@ import java.util.logging.Logger; import net.pterodactylus.sone.freenet.plugin.PluginConnector; import net.pterodactylus.sone.freenet.plugin.PluginException; import net.pterodactylus.sone.freenet.plugin.event.ReceivedReplyEvent; -import net.pterodactylus.util.logging.Logging; -import net.pterodactylus.util.number.Numbers; +import com.google.common.base.Optional; import com.google.common.collect.MapMaker; import com.google.common.eventbus.Subscribe; import com.google.inject.Inject; +import com.google.inject.Singleton; import freenet.support.SimpleFieldSet; import freenet.support.api.Bucket; @@ -43,10 +46,11 @@ import freenet.support.api.Bucket; * * @author David ‘Bombe’ Roden */ +@Singleton public class WebOfTrustConnector { /** The logger. */ - private static final Logger logger = Logging.getLogger(WebOfTrustConnector.class); + private static final Logger logger = getLogger("Sone.WoT.Connector"); /** The name of the WoT plugin. */ private static final String WOT_PLUGIN_NAME = "plugins.WebOfTrust.WebOfTrust"; @@ -137,8 +141,8 @@ public class WebOfTrustConnector { * @throws PluginException * if an error occured talking to the Web of Trust plugin */ - public Set loadTrustedIdentities(OwnIdentity ownIdentity, String context) throws PluginException { - Reply reply = performRequest(SimpleFieldSetConstructor.create().put("Message", "GetIdentitiesByScore").put("Truster", ownIdentity.getId()).put("Selection", "+").put("Context", (context == null) ? "" : context).put("WantTrustValues", "true").get()); + public Set loadTrustedIdentities(OwnIdentity ownIdentity, Optional context) throws PluginException { + Reply reply = performRequest(SimpleFieldSetConstructor.create().put("Message", "GetIdentitiesByScore").put("Truster", ownIdentity.getId()).put("Selection", "+").put("Context", context.or("")).put("WantTrustValues", "true").get()); SimpleFieldSet fields = reply.getFields(); Set identities = new HashSet(); int identityCounter = -1; @@ -152,9 +156,9 @@ public class WebOfTrustConnector { DefaultIdentity identity = new DefaultIdentity(id, nickname, requestUri); identity.setContexts(parseContexts("Contexts" + identityCounter + ".", fields)); identity.setProperties(parseProperties("Properties" + identityCounter + ".", fields)); - Integer trust = Numbers.safeParseInteger(fields.get("Trust" + identityCounter), null); - int score = Numbers.safeParseInteger(fields.get("Score" + identityCounter), 0); - int rank = Numbers.safeParseInteger(fields.get("Rank" + identityCounter), 0); + Integer trust = parseInt(fields.get("Trust" + identityCounter), null); + int score = parseInt(fields.get("Score" + identityCounter), 0); + int rank = parseInt(fields.get("Rank" + identityCounter), 0); identity.setTrust(ownIdentity, new Trust(trust, score, rank)); identities.add(identity); } @@ -566,8 +570,7 @@ public class WebOfTrustConnector { * @return The created simple field set constructor */ public static SimpleFieldSetConstructor create(boolean shortLived) { - SimpleFieldSetConstructor simpleFieldSetConstructor = new SimpleFieldSetConstructor(shortLived); - return simpleFieldSetConstructor; + return new SimpleFieldSetConstructor(shortLived); } } diff --git a/src/main/java/net/pterodactylus/sone/freenet/wot/event/IdentityEvent.java b/src/main/java/net/pterodactylus/sone/freenet/wot/event/IdentityEvent.java index 2727226..c262513 100644 --- a/src/main/java/net/pterodactylus/sone/freenet/wot/event/IdentityEvent.java +++ b/src/main/java/net/pterodactylus/sone/freenet/wot/event/IdentityEvent.java @@ -68,4 +68,18 @@ public abstract class IdentityEvent { return identity; } + @Override + public int hashCode() { + return ownIdentity().hashCode() ^ identity().hashCode(); + } + + @Override + public boolean equals(Object object) { + if ((object == null) || !object.getClass().equals(getClass())) { + return false; + } + IdentityEvent identityEvent = (IdentityEvent) object; + return ownIdentity().equals(identityEvent.ownIdentity()) && identity().equals(identityEvent.identity()); + } + } diff --git a/src/main/java/net/pterodactylus/sone/freenet/wot/event/OwnIdentityEvent.java b/src/main/java/net/pterodactylus/sone/freenet/wot/event/OwnIdentityEvent.java index 97179e8..1216273 100644 --- a/src/main/java/net/pterodactylus/sone/freenet/wot/event/OwnIdentityEvent.java +++ b/src/main/java/net/pterodactylus/sone/freenet/wot/event/OwnIdentityEvent.java @@ -52,4 +52,18 @@ public abstract class OwnIdentityEvent { return ownIdentity; } + @Override + public int hashCode() { + return ownIdentity().hashCode(); + } + + @Override + public boolean equals(Object object) { + if ((object == null) || !object.getClass().equals(getClass())) { + return false; + } + OwnIdentityEvent ownIdentityEvent = (OwnIdentityEvent) object; + return ownIdentity().equals(ownIdentityEvent.ownIdentity()); + } + } diff --git a/src/main/java/net/pterodactylus/sone/main/SonePlugin.java b/src/main/java/net/pterodactylus/sone/main/SonePlugin.java index 42eeb45..392cc40 100644 --- a/src/main/java/net/pterodactylus/sone/main/SonePlugin.java +++ b/src/main/java/net/pterodactylus/sone/main/SonePlugin.java @@ -17,7 +17,11 @@ package net.pterodactylus.sone.main; +import static com.google.common.base.Optional.of; +import static java.util.logging.Logger.getLogger; + import java.io.File; +import java.util.logging.Handler; import java.util.logging.Level; import java.util.logging.LogRecord; import java.util.logging.Logger; @@ -25,6 +29,7 @@ import java.util.logging.Logger; import net.pterodactylus.sone.core.Core; import net.pterodactylus.sone.core.FreenetInterface; import net.pterodactylus.sone.core.WebOfTrustUpdater; +import net.pterodactylus.sone.core.WebOfTrustUpdaterImpl; import net.pterodactylus.sone.database.Database; import net.pterodactylus.sone.database.PostBuilderFactory; import net.pterodactylus.sone.database.PostProvider; @@ -34,16 +39,17 @@ import net.pterodactylus.sone.database.memory.MemoryDatabase; import net.pterodactylus.sone.fcp.FcpInterface; import net.pterodactylus.sone.freenet.PluginStoreConfigurationBackend; import net.pterodactylus.sone.freenet.plugin.PluginConnector; +import net.pterodactylus.sone.freenet.wot.Context; import net.pterodactylus.sone.freenet.wot.IdentityManager; +import net.pterodactylus.sone.freenet.wot.IdentityManagerImpl; import net.pterodactylus.sone.freenet.wot.WebOfTrustConnector; import net.pterodactylus.sone.web.WebInterface; import net.pterodactylus.util.config.Configuration; import net.pterodactylus.util.config.ConfigurationException; import net.pterodactylus.util.config.MapConfigurationBackend; -import net.pterodactylus.util.logging.Logging; -import net.pterodactylus.util.logging.LoggingListener; import net.pterodactylus.util.version.Version; +import com.google.common.base.Optional; import com.google.common.eventbus.EventBus; import com.google.inject.AbstractModule; import com.google.inject.Guice; @@ -51,12 +57,11 @@ import com.google.inject.Injector; import com.google.inject.Singleton; import com.google.inject.TypeLiteral; import com.google.inject.matcher.Matchers; -import com.google.inject.name.Names; import com.google.inject.spi.InjectionListener; import com.google.inject.spi.TypeEncounter; import com.google.inject.spi.TypeListener; -import freenet.client.async.DatabaseDisabledException; +import freenet.client.async.PersistenceDisabledException; import freenet.l10n.BaseL10n.LANGUAGE; import freenet.l10n.PluginL10n; import freenet.node.Node; @@ -81,33 +86,40 @@ public class SonePlugin implements FredPlugin, FredPluginFCP, FredPluginL10n, Fr static { /* initialize logging. */ - Logging.setup("sone"); - Logging.addLoggingListener(new LoggingListener() { - + Logger soneLogger = getLogger("Sone"); + soneLogger.setUseParentHandlers(false); + soneLogger.addHandler(new Handler() { @Override - public void logged(LogRecord logRecord) { - Class loggerClass = Logging.getLoggerClass(logRecord.getLoggerName()); + public void publish(LogRecord logRecord) { int recordLevel = logRecord.getLevel().intValue(); if (recordLevel < Level.FINE.intValue()) { - freenet.support.Logger.debug(loggerClass, logRecord.getMessage(), logRecord.getThrown()); + freenet.support.Logger.debug(logRecord.getLoggerName(), logRecord.getMessage(), logRecord.getThrown()); } else if (recordLevel < Level.INFO.intValue()) { - freenet.support.Logger.minor(loggerClass, logRecord.getMessage(), logRecord.getThrown()); + freenet.support.Logger.minor(logRecord.getLoggerName(), logRecord.getMessage(), logRecord.getThrown()); } else if (recordLevel < Level.WARNING.intValue()) { - freenet.support.Logger.normal(loggerClass, logRecord.getMessage(), logRecord.getThrown()); + freenet.support.Logger.normal(logRecord.getLoggerName(), logRecord.getMessage(), logRecord.getThrown()); } else if (recordLevel < Level.SEVERE.intValue()) { - freenet.support.Logger.warning(loggerClass, logRecord.getMessage(), logRecord.getThrown()); + freenet.support.Logger.warning(logRecord.getLoggerName(), logRecord.getMessage(), logRecord.getThrown()); } else { - freenet.support.Logger.error(loggerClass, logRecord.getMessage(), logRecord.getThrown()); + freenet.support.Logger.error(logRecord.getLoggerName(), logRecord.getMessage(), logRecord.getThrown()); } } + + @Override + public void flush() { + } + + @Override + public void close() { + } }); } /** The version. */ - public static final Version VERSION = new Version(0, 8, 9); + public static final Version VERSION = new Version("rc1", 0, 9); /** The logger. */ - private static final Logger logger = Logging.getLogger(SonePlugin.class); + private static final Logger logger = getLogger("Sone.Plugin"); /** The plugin respirator. */ private PluginRespirator pluginRespirator; @@ -189,13 +201,19 @@ public class SonePlugin implements FredPlugin, FredPluginFCP, FredPluginL10n, Fr try { oldConfiguration = new Configuration(new PluginStoreConfigurationBackend(pluginRespirator)); logger.log(Level.INFO, "Plugin store loaded."); - } catch (DatabaseDisabledException dde1) { + } catch (PersistenceDisabledException pde1) { logger.log(Level.SEVERE, "Could not load any configuration, using empty configuration!"); oldConfiguration = new Configuration(new MapConfigurationBackend()); } } - final Configuration startConfiguration = oldConfiguration; + final Configuration startConfiguration; + if ((newConfiguration != null) && (oldConfiguration != newConfiguration)) { + logger.log(Level.INFO, "Setting configuration to file-based configuration."); + startConfiguration = newConfiguration; + } else { + startConfiguration = oldConfiguration; + } final EventBus eventBus = new EventBus(); /* Freenet injector configuration. */ @@ -213,23 +231,12 @@ public class SonePlugin implements FredPlugin, FredPluginFCP, FredPluginL10n, Fr @Override protected void configure() { - bind(Core.class).in(Singleton.class); - bind(MemoryDatabase.class).in(Singleton.class); bind(EventBus.class).toInstance(eventBus); bind(Configuration.class).toInstance(startConfiguration); - bind(FreenetInterface.class).in(Singleton.class); - bind(PluginConnector.class).in(Singleton.class); - bind(WebOfTrustConnector.class).in(Singleton.class); - bind(WebOfTrustUpdater.class).in(Singleton.class); - bind(IdentityManager.class).in(Singleton.class); - bind(String.class).annotatedWith(Names.named("WebOfTrustContext")).toInstance("Sone"); + Context context = new Context("Sone"); + bind(Context.class).toInstance(context); + bind(getOptionalContextTypeLiteral()).toInstance(of(context)); bind(SonePlugin.class).toInstance(SonePlugin.this); - bind(FcpInterface.class).in(Singleton.class); - bind(Database.class).to(MemoryDatabase.class); - bind(PostBuilderFactory.class).to(MemoryDatabase.class); - bind(PostReplyBuilderFactory.class).to(MemoryDatabase.class); - bind(SoneProvider.class).to(Core.class).in(Singleton.class); - bind(PostProvider.class).to(MemoryDatabase.class); bindListener(Matchers.any(), new TypeListener() { @Override @@ -245,6 +252,11 @@ public class SonePlugin implements FredPlugin, FredPluginFCP, FredPluginL10n, Fr }); } + private TypeLiteral> getOptionalContextTypeLiteral() { + return new TypeLiteral>() { + }; + } + }; Injector injector = Guice.createInjector(freenetModule, soneModule); core = injector.getInstance(Core.class); @@ -254,34 +266,15 @@ public class SonePlugin implements FredPlugin, FredPluginFCP, FredPluginL10n, Fr /* create FCP interface. */ fcpInterface = injector.getInstance(FcpInterface.class); - core.setFcpInterface(fcpInterface); /* create the web interface. */ webInterface = injector.getInstance(WebInterface.class); - boolean startupFailed = true; - try { - - /* start core! */ - core.start(); - if ((newConfiguration != null) && (oldConfiguration != newConfiguration)) { - logger.log(Level.INFO, "Setting configuration to file-based configuration."); - core.setConfiguration(newConfiguration); - } - webInterface.start(); - webInterface.setFirstStart(firstStart); - webInterface.setNewConfig(newConfig); - startupFailed = false; - } finally { - if (startupFailed) { - /* - * we let the exception bubble up but shut the logging down so - * that the logfile is not swamped by the installed logging - * handlers of the failed instances. - */ - Logging.shutdown(); - } - } + /* start core! */ + core.start(); + webInterface.start(); + webInterface.setFirstStart(firstStart); + webInterface.setNewConfig(newConfig); } /** @@ -300,9 +293,6 @@ public class SonePlugin implements FredPlugin, FredPluginFCP, FredPluginL10n, Fr webOfTrustConnector.stop(); } catch (Throwable t1) { logger.log(Level.SEVERE, "Error while shutting down!", t1); - } finally { - /* shutdown logger. */ - Logging.shutdown(); } } diff --git a/src/main/java/net/pterodactylus/sone/notify/ListNotificationFilters.java b/src/main/java/net/pterodactylus/sone/notify/ListNotificationFilters.java index f431bb7..f93e1f4 100644 --- a/src/main/java/net/pterodactylus/sone/notify/ListNotificationFilters.java +++ b/src/main/java/net/pterodactylus/sone/notify/ListNotificationFilters.java @@ -59,12 +59,12 @@ public class ListNotificationFilters { List filteredNotifications = new ArrayList(); for (Notification notification : notifications) { if (notification.getId().equals("new-sone-notification")) { - if ((currentSone != null) && (!currentSone.getOptions().getBooleanOption("ShowNotification/NewSones").get())) { + if ((currentSone != null) && !currentSone.getOptions().isShowNewSoneNotifications()) { continue; } filteredNotifications.add(notification); } else if (notification.getId().equals("new-post-notification")) { - if ((currentSone != null) && (!currentSone.getOptions().getBooleanOption("ShowNotification/NewPosts").get())) { + if ((currentSone != null) && !currentSone.getOptions().isShowNewPostNotifications()) { continue; } ListNotification filteredNotification = filterNewPostNotification((ListNotification) notification, currentSone, true); @@ -72,7 +72,7 @@ public class ListNotificationFilters { filteredNotifications.add(filteredNotification); } } else if (notification.getId().equals("new-reply-notification")) { - if ((currentSone != null) && (!currentSone.getOptions().getBooleanOption("ShowNotification/NewReplies").get())) { + if ((currentSone != null) && !currentSone.getOptions().isShowNewReplyNotifications()) { continue; } ListNotification filteredNotification = filterNewReplyNotification((ListNotification) notification, currentSone); @@ -222,10 +222,10 @@ public class ListNotificationFilters { */ public static boolean isPostVisible(Sone sone, Post post) { checkNotNull(post, "post must not be null"); - Sone postSone = post.getSone(); - if (postSone == null) { + if (!post.isLoaded()) { return false; } + Sone postSone = post.getSone(); if (sone != null) { Trust trust = postSone.getIdentity().getTrust((OwnIdentity) sone.getIdentity()); if (trust != null) { diff --git a/src/main/java/net/pterodactylus/sone/template/CollectionAccessor.java b/src/main/java/net/pterodactylus/sone/template/CollectionAccessor.java index 12b4bc2..b5a4255 100644 --- a/src/main/java/net/pterodactylus/sone/template/CollectionAccessor.java +++ b/src/main/java/net/pterodactylus/sone/template/CollectionAccessor.java @@ -45,9 +45,6 @@ public class CollectionAccessor extends ReflectionAccessor { */ @Override public Object get(TemplateContext templateContext, Object object, String member) { - if (object == null) { - return null; - } Collection collection = (Collection) object; if (member.equals("soneNames")) { List sones = new ArrayList(); diff --git a/src/main/java/net/pterodactylus/sone/template/IdentityAccessor.java b/src/main/java/net/pterodactylus/sone/template/IdentityAccessor.java index c08a0a2..efb4d16 100644 --- a/src/main/java/net/pterodactylus/sone/template/IdentityAccessor.java +++ b/src/main/java/net/pterodactylus/sone/template/IdentityAccessor.java @@ -55,9 +55,8 @@ public class IdentityAccessor extends ReflectionAccessor { Identity identity = (Identity) object; if ("uniqueNickname".equals(member)) { int minLength = -1; - boolean found = false; - Set ownIdentities = null; - ownIdentities = core.getIdentityManager().getAllOwnIdentities(); + boolean found; + Set ownIdentities = core.getIdentityManager().getAllOwnIdentities(); do { boolean unique = true; String abbreviatedWantedNickname = getAbbreviatedNickname(identity, ++minLength); diff --git a/src/main/java/net/pterodactylus/sone/template/ImageLinkFilter.java b/src/main/java/net/pterodactylus/sone/template/ImageLinkFilter.java index 7e3723c..9ede851 100644 --- a/src/main/java/net/pterodactylus/sone/template/ImageLinkFilter.java +++ b/src/main/java/net/pterodactylus/sone/template/ImageLinkFilter.java @@ -17,13 +17,16 @@ package net.pterodactylus.sone.template; +import static java.lang.Integer.MAX_VALUE; +import static java.lang.String.valueOf; +import static net.pterodactylus.sone.utils.NumberParsers.parseInt; + import java.io.StringReader; import java.io.StringWriter; import java.util.Map; import net.pterodactylus.sone.core.Core; import net.pterodactylus.sone.data.Image; -import net.pterodactylus.util.number.Numbers; import net.pterodactylus.util.template.Filter; import net.pterodactylus.util.template.Template; import net.pterodactylus.util.template.TemplateContext; @@ -76,11 +79,11 @@ public class ImageLinkFilter implements Filter { if (image == null) { return null; } - String imageClass = String.valueOf(parameters.get("class")); - int maxWidth = Numbers.safeParseInteger(parameters.get("max-width"), Integer.MAX_VALUE); - int maxHeight = Numbers.safeParseInteger(parameters.get("max-height"), Integer.MAX_VALUE); - String mode = String.valueOf(parameters.get("mode")); - String title = String.valueOf(parameters.get("title")); + String imageClass = valueOf(parameters.get("class")); + int maxWidth = parseInt(valueOf(parameters.get("max-width")), MAX_VALUE); + int maxHeight = parseInt(valueOf(parameters.get("max-height")), MAX_VALUE); + String mode = valueOf(parameters.get("mode")); + String title = valueOf(parameters.get("title")); TemplateContext linkTemplateContext = templateContextFactory.createTemplateContext(); linkTemplateContext.set("class", imageClass); diff --git a/src/main/java/net/pterodactylus/sone/template/ParserFilter.java b/src/main/java/net/pterodactylus/sone/template/ParserFilter.java index bd6903b..ec35e2e 100644 --- a/src/main/java/net/pterodactylus/sone/template/ParserFilter.java +++ b/src/main/java/net/pterodactylus/sone/template/ParserFilter.java @@ -17,6 +17,9 @@ package net.pterodactylus.sone.template; +import static java.lang.String.valueOf; +import static net.pterodactylus.sone.utils.NumberParsers.parseInt; + import java.io.IOException; import java.io.StringReader; import java.io.StringWriter; @@ -38,7 +41,6 @@ import net.pterodactylus.sone.text.SonePart; import net.pterodactylus.sone.text.SoneTextParser; import net.pterodactylus.sone.text.SoneTextParserContext; import net.pterodactylus.sone.web.page.FreenetRequest; -import net.pterodactylus.util.number.Numbers; import net.pterodactylus.util.template.Filter; import net.pterodactylus.util.template.Template; import net.pterodactylus.util.template.TemplateContext; @@ -89,9 +91,9 @@ public class ParserFilter implements Filter { */ @Override public Object format(TemplateContext templateContext, Object data, Map parameters) { - String text = String.valueOf(data); - int length = Numbers.safeParseInteger(parameters.get("length"), Numbers.safeParseInteger(templateContext.get(String.valueOf(parameters.get("length"))), -1)); - int cutOffLength = Numbers.safeParseInteger(parameters.get("cut-off-length"), Numbers.safeParseInteger(templateContext.get(String.valueOf(parameters.get("cut-off-length"))), length)); + String text = valueOf(data); + int length = parseInt(valueOf(parameters.get("length")), -1); + int cutOffLength = parseInt(valueOf(parameters.get("cut-off-length")), length); Object sone = parameters.get("sone"); if (sone instanceof String) { sone = core.getSone((String) sone).orNull(); diff --git a/src/main/java/net/pterodactylus/sone/template/PostAccessor.java b/src/main/java/net/pterodactylus/sone/template/PostAccessor.java index d593b51..78db678 100644 --- a/src/main/java/net/pterodactylus/sone/template/PostAccessor.java +++ b/src/main/java/net/pterodactylus/sone/template/PostAccessor.java @@ -67,8 +67,6 @@ public class PostAccessor extends ReflectionAccessor { return !post.isKnown(); } else if (member.equals("bookmarked")) { return core.isBookmarked(post); - } else if (member.equals("loaded")) { - return post.getSone() != null; } return super.get(templateContext, object, member); } diff --git a/src/main/java/net/pterodactylus/sone/template/ProfileAccessor.java b/src/main/java/net/pterodactylus/sone/template/ProfileAccessor.java index 30c0a52..762bc14 100644 --- a/src/main/java/net/pterodactylus/sone/template/ProfileAccessor.java +++ b/src/main/java/net/pterodactylus/sone/template/ProfileAccessor.java @@ -74,7 +74,7 @@ public class ProfileAccessor extends ReflectionAccessor { /* always show your own avatars. */ return avatarId; } - ShowCustomAvatars showCustomAvatars = currentSone.getOptions(). getEnumOption("ShowCustomAvatars").get(); + ShowCustomAvatars showCustomAvatars = currentSone.getOptions().getShowCustomAvatars(); if (showCustomAvatars == ShowCustomAvatars.NEVER) { return null; } diff --git a/src/main/java/net/pterodactylus/sone/template/RequestChangeFilter.java b/src/main/java/net/pterodactylus/sone/template/RequestChangeFilter.java index 0d4f556..a5a4edf 100644 --- a/src/main/java/net/pterodactylus/sone/template/RequestChangeFilter.java +++ b/src/main/java/net/pterodactylus/sone/template/RequestChangeFilter.java @@ -68,8 +68,7 @@ public class RequestChangeFilter implements Filter { if (questionMark == -1) { questionMark = oldUri.length(); } - URI u = new URI(oldUri.substring(0, questionMark) + query.toString()); - return u; + return new URI(oldUri.substring(0, questionMark) + query.toString()); } catch (UnsupportedEncodingException uee1) { /* UTF-8 not supported? I don’t think so. */ } catch (URISyntaxException use1) { diff --git a/src/main/java/net/pterodactylus/sone/template/SoneAccessor.java b/src/main/java/net/pterodactylus/sone/template/SoneAccessor.java index 92be8e0..711332a 100644 --- a/src/main/java/net/pterodactylus/sone/template/SoneAccessor.java +++ b/src/main/java/net/pterodactylus/sone/template/SoneAccessor.java @@ -19,6 +19,7 @@ package net.pterodactylus.sone.template; import static com.google.common.collect.FluentIterable.from; import static java.util.Arrays.asList; +import static java.util.logging.Logger.getLogger; import static net.pterodactylus.sone.data.Album.FLATTENER; import static net.pterodactylus.sone.data.Album.IMAGES; @@ -33,7 +34,6 @@ import net.pterodactylus.sone.freenet.wot.OwnIdentity; import net.pterodactylus.sone.freenet.wot.Trust; import net.pterodactylus.sone.web.WebInterface; import net.pterodactylus.sone.web.ajax.GetTimesAjaxPage; -import net.pterodactylus.util.logging.Logging; import net.pterodactylus.util.template.Accessor; import net.pterodactylus.util.template.ReflectionAccessor; import net.pterodactylus.util.template.TemplateContext; @@ -58,7 +58,7 @@ import net.pterodactylus.util.template.TemplateContext; public class SoneAccessor extends ReflectionAccessor { /** The logger. */ - private static final Logger logger = Logging.getLogger(SoneAccessor.class); + private static final Logger logger = getLogger("Sone.Data"); /** The core. */ private final Core core; diff --git a/src/main/java/net/pterodactylus/sone/text/SoneTextParser.java b/src/main/java/net/pterodactylus/sone/text/SoneTextParser.java index 0b1585a..cc5c59b 100644 --- a/src/main/java/net/pterodactylus/sone/text/SoneTextParser.java +++ b/src/main/java/net/pterodactylus/sone/text/SoneTextParser.java @@ -17,6 +17,8 @@ package net.pterodactylus.sone.text; +import static java.util.logging.Logger.getLogger; + import java.io.BufferedReader; import java.io.IOException; import java.io.Reader; @@ -28,11 +30,10 @@ import java.util.regex.Pattern; import net.pterodactylus.sone.data.Post; import net.pterodactylus.sone.data.Sone; -import net.pterodactylus.sone.data.SoneImpl; +import net.pterodactylus.sone.data.impl.IdOnlySone; import net.pterodactylus.sone.database.PostProvider; import net.pterodactylus.sone.database.SoneProvider; import net.pterodactylus.util.io.Closer; -import net.pterodactylus.util.logging.Logging; import com.google.common.base.Optional; @@ -46,7 +47,7 @@ import freenet.keys.FreenetURI; public class SoneTextParser implements Parser { /** The logger. */ - private static final Logger logger = Logging.getLogger(SoneTextParser.class); + private static final Logger logger = getLogger("Sone.Data.Parser"); /** Pattern to detect whitespace. */ private static final Pattern whitespacePattern = Pattern.compile("[\\u000a\u0020\u00a0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u200b\u200c\u200d\u202f\u205f\u2060\u2800\u3000]"); @@ -249,7 +250,7 @@ public class SoneTextParser implements Parser { * don’t use create=true above, we don’t want * the empty shell. */ - sone = Optional.of(new SoneImpl(soneId, false)); + sone = Optional.of(new IdOnlySone(soneId)); } parts.add(new SonePart(sone.get())); line = line.substring(50); diff --git a/src/main/java/net/pterodactylus/sone/utils/DefaultOption.java b/src/main/java/net/pterodactylus/sone/utils/DefaultOption.java new file mode 100644 index 0000000..0939f21 --- /dev/null +++ b/src/main/java/net/pterodactylus/sone/utils/DefaultOption.java @@ -0,0 +1,84 @@ +package net.pterodactylus.sone.utils; + +import com.google.common.base.Predicate; + +/** + * Basic implementation of an {@link Option}. + * + * @param + * The type of the option + * @author David ‘Bombe’ Roden + */ +public class DefaultOption implements Option { + + /** The default value. */ + private final T defaultValue; + + /** The current value. */ + private volatile T value; + + /** The validator. */ + private Predicate validator; + + /** + * Creates a new default option. + * + * @param defaultValue + * The default value of the option + */ + public DefaultOption(T defaultValue) { + this(defaultValue, null); + } + + /** + * Creates a new default option. + * + * @param defaultValue + * The default value of the option + * @param validator + * The validator for value validation (may be {@code null}) + */ + public DefaultOption(T defaultValue, Predicate validator) { + this.defaultValue = defaultValue; + this.validator = validator; + } + + /** + * {@inheritDoc} + */ + @Override + public T get() { + return (value != null) ? value : defaultValue; + } + + /** + * Returns the real value of the option. This will also return an unset + * value (usually {@code null})! + * + * @return The real value of the option + */ + @Override + public T getReal() { + return value; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean validate(T value) { + return (validator == null) || (value == null) || validator.apply(value); + } + + /** + * {@inheritDoc} + */ + @Override + public void set(T value) { + if ((value != null) && (validator != null) && (!validator.apply(value))) { + throw new IllegalArgumentException("New Value (" + value + ") could not be validated."); + } + this.value = value; + } + +} diff --git a/src/main/java/net/pterodactylus/sone/utils/IntegerRangePredicate.java b/src/main/java/net/pterodactylus/sone/utils/IntegerRangePredicate.java index db6a841..a4022de 100644 --- a/src/main/java/net/pterodactylus/sone/utils/IntegerRangePredicate.java +++ b/src/main/java/net/pterodactylus/sone/utils/IntegerRangePredicate.java @@ -59,4 +59,8 @@ public class IntegerRangePredicate implements Predicate { return (value != null) && (value >= lowerBound) && (value <= upperBound); } + public static IntegerRangePredicate range(int lowerBound, int upperBound) { + return new IntegerRangePredicate(lowerBound, upperBound); + } + } diff --git a/src/main/java/net/pterodactylus/sone/utils/NumberParsers.java b/src/main/java/net/pterodactylus/sone/utils/NumberParsers.java new file mode 100644 index 0000000..9e8afb4 --- /dev/null +++ b/src/main/java/net/pterodactylus/sone/utils/NumberParsers.java @@ -0,0 +1,36 @@ +package net.pterodactylus.sone.utils; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +import com.google.common.primitives.Ints; +import com.google.common.primitives.Longs; + +/** + * Parses numbers from strings. + * + * @author David ‘Bombe’ Roden + */ +public class NumberParsers { + + @Nonnull + public static Integer parseInt(@Nullable String text, + @Nullable Integer defaultValue) { + if (text == null) { + return defaultValue; + } + Integer value = Ints.tryParse(text); + return (value == null) ? defaultValue : value; + } + + @Nonnull + public static Long parseLong(@Nullable String text, + @Nullable Long defaultValue) { + if (text == null) { + return defaultValue; + } + Long value = Longs.tryParse(text); + return (value == null) ? defaultValue : value; + } + +} diff --git a/src/main/java/net/pterodactylus/sone/utils/Option.java b/src/main/java/net/pterodactylus/sone/utils/Option.java new file mode 100644 index 0000000..9149c07 --- /dev/null +++ b/src/main/java/net/pterodactylus/sone/utils/Option.java @@ -0,0 +1,49 @@ +package net.pterodactylus.sone.utils; + +/** + * Contains current and default value of an option. + * + * @param + * The type of the option + * @author David ‘Bombe’ Roden + */ +public interface Option { + + /** + * Returns the current value of the option. If the current value is not + * set (usually {@code null}), the default value is returned. + * + * @return The current value of the option + */ + public T get(); + + /** + * Returns the real value of the option. This will also return an unset + * value (usually {@code null})! + * + * @return The real value of the option + */ + public T getReal(); + + /** + * Validates the given value. Note that {@code null} is always a valid + * value! + * + * @param value + * The value to validate + * @return {@code true} if this option does not have a validator, or the + * validator validates this object, {@code false} otherwise + */ + public boolean validate(T value); + + /** + * Sets the current value of the option. + * + * @param value + * The new value of the option + * @throws IllegalArgumentException + * if the value is not valid for this option + */ + public void set(T value) throws IllegalArgumentException; + +} diff --git a/src/main/java/net/pterodactylus/sone/utils/Optionals.java b/src/main/java/net/pterodactylus/sone/utils/Optionals.java new file mode 100644 index 0000000..67132aa --- /dev/null +++ b/src/main/java/net/pterodactylus/sone/utils/Optionals.java @@ -0,0 +1,32 @@ +package net.pterodactylus.sone.utils; + +import com.google.common.base.Function; +import com.google.common.base.Optional; +import com.google.common.base.Predicate; + +/** + * Helper methods for dealing with {@link Optional}s. + * + * @author David ‘Bombe’ Roden + */ +public class Optionals { + + public static Predicate> isPresent() { + return new Predicate>() { + @Override + public boolean apply(Optional input) { + return input.isPresent(); + } + }; + } + + public static Function, T> get() { + return new Function, T>() { + @Override + public T apply(Optional input) { + return input.get(); + } + }; + } + +} diff --git a/src/main/java/net/pterodactylus/sone/web/BookmarkPage.java b/src/main/java/net/pterodactylus/sone/web/BookmarkPage.java index 042a683..c0a8907 100644 --- a/src/main/java/net/pterodactylus/sone/web/BookmarkPage.java +++ b/src/main/java/net/pterodactylus/sone/web/BookmarkPage.java @@ -17,11 +17,14 @@ package net.pterodactylus.sone.web; +import net.pterodactylus.sone.data.Post; import net.pterodactylus.sone.web.page.FreenetRequest; import net.pterodactylus.util.template.Template; import net.pterodactylus.util.template.TemplateContext; import net.pterodactylus.util.web.Method; +import com.google.common.base.Optional; + /** * Page that lets the user bookmark a post. * @@ -52,7 +55,10 @@ public class BookmarkPage extends SoneTemplatePage { if (request.getMethod() == Method.POST) { String id = request.getHttpRequest().getPartAsStringFailsafe("post", 36); String returnPage = request.getHttpRequest().getPartAsStringFailsafe("returnPage", 256); - webInterface.getCore().bookmarkPost(id); + Optional post = webInterface.getCore().getPost(id); + if (post.isPresent()) { + webInterface.getCore().bookmarkPost(post.get()); + } throw new RedirectException(returnPage); } } diff --git a/src/main/java/net/pterodactylus/sone/web/BookmarksPage.java b/src/main/java/net/pterodactylus/sone/web/BookmarksPage.java index 10b6dc3..0f53572 100644 --- a/src/main/java/net/pterodactylus/sone/web/BookmarksPage.java +++ b/src/main/java/net/pterodactylus/sone/web/BookmarksPage.java @@ -17,6 +17,8 @@ package net.pterodactylus.sone.web; +import static net.pterodactylus.sone.utils.NumberParsers.parseInt; + import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -26,7 +28,6 @@ import java.util.Set; import net.pterodactylus.sone.data.Post; import net.pterodactylus.sone.web.page.FreenetRequest; import net.pterodactylus.util.collection.Pagination; -import net.pterodactylus.util.number.Numbers; import net.pterodactylus.util.template.Template; import net.pterodactylus.util.template.TemplateContext; @@ -67,12 +68,12 @@ public class BookmarksPage extends SoneTemplatePage { @Override public boolean apply(Post post) { - return post.getSone() != null; + return post.isLoaded(); } }); List sortedPosts = new ArrayList(loadedPosts); Collections.sort(sortedPosts, Post.TIME_COMPARATOR); - Pagination pagination = new Pagination(sortedPosts, webInterface.getCore().getPreferences().getPostsPerPage()).setPage(Numbers.safeParseInteger(request.getHttpRequest().getParam("page"), 0)); + Pagination pagination = new Pagination(sortedPosts, webInterface.getCore().getPreferences().getPostsPerPage()).setPage(parseInt(request.getHttpRequest().getParam("page"), 0)); templateContext.set("pagination", pagination); templateContext.set("posts", pagination.getItems()); templateContext.set("postsNotLoaded", allPosts.size() != loadedPosts.size()); diff --git a/src/main/java/net/pterodactylus/sone/web/CreateAlbumPage.java b/src/main/java/net/pterodactylus/sone/web/CreateAlbumPage.java index a8d024a..1c599e4 100644 --- a/src/main/java/net/pterodactylus/sone/web/CreateAlbumPage.java +++ b/src/main/java/net/pterodactylus/sone/web/CreateAlbumPage.java @@ -18,6 +18,7 @@ package net.pterodactylus.sone.web; import net.pterodactylus.sone.data.Album; +import net.pterodactylus.sone.data.Album.Modifier.AlbumTitleMustNotBeEmpty; import net.pterodactylus.sone.data.Sone; import net.pterodactylus.sone.text.TextFilter; import net.pterodactylus.sone.web.page.FreenetRequest; @@ -63,12 +64,16 @@ public class CreateAlbumPage extends SoneTemplatePage { String description = request.getHttpRequest().getPartAsStringFailsafe("description", 256).trim(); Sone currentSone = getCurrentSone(request.getToadletContext()); String parentId = request.getHttpRequest().getPartAsStringFailsafe("parent", 36); - Album parent = webInterface.getCore().getAlbum(parentId, false); + Album parent = webInterface.getCore().getAlbum(parentId); if (parentId.equals("")) { parent = currentSone.getRootAlbum(); } Album album = webInterface.getCore().createAlbum(currentSone, parent); - album.modify().setTitle(name).setDescription(TextFilter.filter(request.getHttpRequest().getHeader("host"), description)).update(); + try { + album.modify().setTitle(name).setDescription(TextFilter.filter(request.getHttpRequest().getHeader("host"), description)).update(); + } catch (AlbumTitleMustNotBeEmpty atmnbe) { + throw new RedirectException("emptyAlbumTitle.html"); + } webInterface.getCore().touchConfiguration(); throw new RedirectException("imageBrowser.html?album=" + album.getId()); } diff --git a/src/main/java/net/pterodactylus/sone/web/CreatePostPage.java b/src/main/java/net/pterodactylus/sone/web/CreatePostPage.java index 83913b4..6530a5b 100644 --- a/src/main/java/net/pterodactylus/sone/web/CreatePostPage.java +++ b/src/main/java/net/pterodactylus/sone/web/CreatePostPage.java @@ -63,13 +63,13 @@ public class CreatePostPage extends SoneTemplatePage { String senderId = request.getHttpRequest().getPartAsStringFailsafe("sender", 43); String recipientId = request.getHttpRequest().getPartAsStringFailsafe("recipient", 43); Sone currentSone = getCurrentSone(request.getToadletContext()); - Sone sender = webInterface.getCore().getLocalSone(senderId, false); + Sone sender = webInterface.getCore().getLocalSone(senderId); if (sender == null) { sender = currentSone; } Optional recipient = webInterface.getCore().getSone(recipientId); text = TextFilter.filter(request.getHttpRequest().getHeader("host"), text); - webInterface.getCore().createPost(sender, recipient, System.currentTimeMillis(), text); + webInterface.getCore().createPost(sender, recipient, text); throw new RedirectException(returnPage); } templateContext.set("errorTextEmpty", true); diff --git a/src/main/java/net/pterodactylus/sone/web/CreateReplyPage.java b/src/main/java/net/pterodactylus/sone/web/CreateReplyPage.java index c8979c1..55903d8 100644 --- a/src/main/java/net/pterodactylus/sone/web/CreateReplyPage.java +++ b/src/main/java/net/pterodactylus/sone/web/CreateReplyPage.java @@ -66,7 +66,7 @@ public class CreateReplyPage extends SoneTemplatePage { } if (text.length() > 0) { String senderId = request.getHttpRequest().getPartAsStringFailsafe("sender", 43); - Sone sender = webInterface.getCore().getLocalSone(senderId, false); + Sone sender = webInterface.getCore().getLocalSone(senderId); if (sender == null) { sender = getCurrentSone(request.getToadletContext()); } diff --git a/src/main/java/net/pterodactylus/sone/web/CreateSonePage.java b/src/main/java/net/pterodactylus/sone/web/CreateSonePage.java index fe8c32e..b2d4578 100644 --- a/src/main/java/net/pterodactylus/sone/web/CreateSonePage.java +++ b/src/main/java/net/pterodactylus/sone/web/CreateSonePage.java @@ -17,6 +17,8 @@ package net.pterodactylus.sone.web; +import static java.util.logging.Logger.getLogger; + import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; @@ -29,7 +31,6 @@ import net.pterodactylus.sone.core.Core; import net.pterodactylus.sone.data.Sone; import net.pterodactylus.sone.freenet.wot.OwnIdentity; import net.pterodactylus.sone.web.page.FreenetRequest; -import net.pterodactylus.util.logging.Logging; import net.pterodactylus.util.template.Template; import net.pterodactylus.util.template.TemplateContext; import net.pterodactylus.util.web.Method; @@ -43,7 +44,7 @@ import freenet.clients.http.ToadletContext; public class CreateSonePage extends SoneTemplatePage { /** The logger. */ - private static final Logger logger = Logging.getLogger(CreateSonePage.class); + private static final Logger logger = getLogger("Sone.Web.CreateSone"); /** * Creates a new “create Sone” page. diff --git a/src/main/java/net/pterodactylus/sone/web/DeleteAlbumPage.java b/src/main/java/net/pterodactylus/sone/web/DeleteAlbumPage.java index 391e9ff..98e8909 100644 --- a/src/main/java/net/pterodactylus/sone/web/DeleteAlbumPage.java +++ b/src/main/java/net/pterodactylus/sone/web/DeleteAlbumPage.java @@ -50,7 +50,7 @@ public class DeleteAlbumPage extends SoneTemplatePage { super.processTemplate(request, templateContext); if (request.getMethod() == Method.POST) { String albumId = request.getHttpRequest().getPartAsStringFailsafe("album", 36); - Album album = webInterface.getCore().getAlbum(albumId, false); + Album album = webInterface.getCore().getAlbum(albumId); if (album == null) { throw new RedirectException("invalid.html"); } @@ -68,7 +68,7 @@ public class DeleteAlbumPage extends SoneTemplatePage { throw new RedirectException("imageBrowser.html?album=" + parentAlbum.getId()); } String albumId = request.getHttpRequest().getParam("album"); - Album album = webInterface.getCore().getAlbum(albumId, false); + Album album = webInterface.getCore().getAlbum(albumId); if (album == null) { throw new RedirectException("invalid.html"); } diff --git a/src/main/java/net/pterodactylus/sone/web/DeletePostPage.java b/src/main/java/net/pterodactylus/sone/web/DeletePostPage.java index 6ef86f0..5a309fa 100644 --- a/src/main/java/net/pterodactylus/sone/web/DeletePostPage.java +++ b/src/main/java/net/pterodactylus/sone/web/DeletePostPage.java @@ -63,7 +63,6 @@ public class DeletePostPage extends SoneTemplatePage { } templateContext.set("post", post.get()); templateContext.set("returnPage", returnPage); - return; } else if (request.getMethod() == Method.POST) { String postId = request.getHttpRequest().getPartAsStringFailsafe("post", 36); String returnPage = request.getHttpRequest().getPartAsStringFailsafe("returnPage", 256); diff --git a/src/main/java/net/pterodactylus/sone/web/EditAlbumPage.java b/src/main/java/net/pterodactylus/sone/web/EditAlbumPage.java index 9e4aae9..66434e5 100644 --- a/src/main/java/net/pterodactylus/sone/web/EditAlbumPage.java +++ b/src/main/java/net/pterodactylus/sone/web/EditAlbumPage.java @@ -18,6 +18,7 @@ package net.pterodactylus.sone.web; import net.pterodactylus.sone.data.Album; +import net.pterodactylus.sone.data.Album.Modifier.AlbumTitleMustNotBeEmpty; import net.pterodactylus.sone.text.TextFilter; import net.pterodactylus.sone.web.page.FreenetRequest; import net.pterodactylus.util.template.Template; @@ -51,7 +52,7 @@ public class EditAlbumPage extends SoneTemplatePage { super.processTemplate(request, templateContext); if (request.getMethod() == Method.POST) { String albumId = request.getHttpRequest().getPartAsStringFailsafe("album", 36); - Album album = webInterface.getCore().getAlbum(albumId, false); + Album album = webInterface.getCore().getAlbum(albumId); if (album == null) { throw new RedirectException("invalid.html"); } @@ -73,12 +74,12 @@ public class EditAlbumPage extends SoneTemplatePage { } album.modify().setAlbumImage(albumImageId).update(); String title = request.getHttpRequest().getPartAsStringFailsafe("title", 100).trim(); - if (title.length() == 0) { - templateContext.set("titleMissing", true); - return; - } String description = request.getHttpRequest().getPartAsStringFailsafe("description", 1000).trim(); - album.modify().setTitle(title).setDescription(TextFilter.filter(request.getHttpRequest().getHeader("host"), description)).update(); + try { + album.modify().setTitle(title).setDescription(TextFilter.filter(request.getHttpRequest().getHeader("host"), description)).update(); + } catch (AlbumTitleMustNotBeEmpty atmnbe) { + throw new RedirectException("emptyAlbumTitle.html"); + } webInterface.getCore().touchConfiguration(); throw new RedirectException("imageBrowser.html?album=" + album.getId()); } diff --git a/src/main/java/net/pterodactylus/sone/web/EditImagePage.java b/src/main/java/net/pterodactylus/sone/web/EditImagePage.java index 9a29c85..178add1 100644 --- a/src/main/java/net/pterodactylus/sone/web/EditImagePage.java +++ b/src/main/java/net/pterodactylus/sone/web/EditImagePage.java @@ -71,7 +71,7 @@ public class EditImagePage extends SoneTemplatePage { String title = request.getHttpRequest().getPartAsStringFailsafe("title", 100).trim(); String description = request.getHttpRequest().getPartAsStringFailsafe("description", 1024).trim(); if (title.length() == 0) { - templateContext.set("titleMissing", true); + throw new RedirectException("emptyImageTitle.html"); } image.modify().setTitle(title).setDescription(TextFilter.filter(request.getHttpRequest().getHeader("host"), description)).update(); } diff --git a/src/main/java/net/pterodactylus/sone/web/EditProfilePage.java b/src/main/java/net/pterodactylus/sone/web/EditProfilePage.java index d0ddb26..162d637 100644 --- a/src/main/java/net/pterodactylus/sone/web/EditProfilePage.java +++ b/src/main/java/net/pterodactylus/sone/web/EditProfilePage.java @@ -18,15 +18,15 @@ package net.pterodactylus.sone.web; import static net.pterodactylus.sone.text.TextFilter.filter; +import static net.pterodactylus.sone.utils.NumberParsers.parseInt; import java.util.List; import net.pterodactylus.sone.data.Profile; +import net.pterodactylus.sone.data.Profile.DuplicateField; import net.pterodactylus.sone.data.Profile.Field; import net.pterodactylus.sone.data.Sone; -import net.pterodactylus.sone.text.TextFilter; import net.pterodactylus.sone.web.page.FreenetRequest; -import net.pterodactylus.util.number.Numbers; import net.pterodactylus.util.template.Template; import net.pterodactylus.util.template.TemplateContext; import net.pterodactylus.util.web.Method; @@ -77,9 +77,9 @@ public class EditProfilePage extends SoneTemplatePage { firstName = request.getHttpRequest().getPartAsStringFailsafe("first-name", 256).trim(); middleName = request.getHttpRequest().getPartAsStringFailsafe("middle-name", 256).trim(); lastName = request.getHttpRequest().getPartAsStringFailsafe("last-name", 256).trim(); - birthDay = Numbers.safeParseInteger(request.getHttpRequest().getPartAsStringFailsafe("birth-day", 256).trim()); - birthMonth = Numbers.safeParseInteger(request.getHttpRequest().getPartAsStringFailsafe("birth-month", 256).trim()); - birthYear = Numbers.safeParseInteger(request.getHttpRequest().getPartAsStringFailsafe("birth-year", 256).trim()); + birthDay = parseInt(request.getHttpRequest().getPartAsStringFailsafe("birth-day", 256).trim(), null); + birthMonth = parseInt(request.getHttpRequest().getPartAsStringFailsafe("birth-month", 256).trim(), null); + birthYear = parseInt(request.getHttpRequest().getPartAsStringFailsafe("birth-year", 256).trim(), null); avatarId = request.getHttpRequest().getPartAsStringFailsafe("avatarId", 36); profile.setFirstName(firstName.length() > 0 ? firstName : null); profile.setMiddleName(middleName.length() > 0 ? middleName : null); @@ -99,10 +99,9 @@ public class EditProfilePage extends SoneTemplatePage { try { profile.addField(fieldName); currentSone.setProfile(profile); - fields = profile.getFields(); webInterface.getCore().touchConfiguration(); throw new RedirectException("editProfile.html#profile-fields"); - } catch (IllegalArgumentException iae1) { + } catch (DuplicateField df1) { templateContext.set("fieldName", fieldName); templateContext.set("duplicateFieldName", true); } diff --git a/src/main/java/net/pterodactylus/sone/web/ImageBrowserPage.java b/src/main/java/net/pterodactylus/sone/web/ImageBrowserPage.java index 766f018..60b22d5 100644 --- a/src/main/java/net/pterodactylus/sone/web/ImageBrowserPage.java +++ b/src/main/java/net/pterodactylus/sone/web/ImageBrowserPage.java @@ -21,6 +21,7 @@ import static com.google.common.collect.FluentIterable.from; import static net.pterodactylus.sone.data.Album.FLATTENER; import static net.pterodactylus.sone.data.Album.NOT_EMPTY; import static net.pterodactylus.sone.data.Album.TITLE_COMPARATOR; +import static net.pterodactylus.sone.utils.NumberParsers.parseInt; import java.net.URI; import java.util.ArrayList; @@ -34,7 +35,6 @@ import net.pterodactylus.sone.data.Image; import net.pterodactylus.sone.data.Sone; import net.pterodactylus.sone.web.page.FreenetRequest; import net.pterodactylus.util.collection.Pagination; -import net.pterodactylus.util.number.Numbers; import net.pterodactylus.util.template.Template; import net.pterodactylus.util.template.TemplateContext; @@ -69,7 +69,7 @@ public class ImageBrowserPage extends SoneTemplatePage { super.processTemplate(request, templateContext); String albumId = request.getHttpRequest().getParam("album", null); if (albumId != null) { - Album album = webInterface.getCore().getAlbum(albumId, false); + Album album = webInterface.getCore().getAlbum(albumId); templateContext.set("albumRequested", true); templateContext.set("album", album); templateContext.set("page", request.getHttpRequest().getParam("page")); @@ -97,7 +97,7 @@ public class ImageBrowserPage extends SoneTemplatePage { albums.addAll(from(sone.getRootAlbum().getAlbums()).transformAndConcat(FLATTENER).filter(NOT_EMPTY).toList()); } Collections.sort(albums, TITLE_COMPARATOR); - Pagination albumPagination = new Pagination(albums, 12).setPage(Numbers.safeParseInteger(request.getHttpRequest().getParam("page"), 0)); + Pagination albumPagination = new Pagination(albums, 12).setPage(parseInt(request.getHttpRequest().getParam("page"), 0)); templateContext.set("albumPagination", albumPagination); templateContext.set("albums", albumPagination.getItems()); return; diff --git a/src/main/java/net/pterodactylus/sone/web/IndexPage.java b/src/main/java/net/pterodactylus/sone/web/IndexPage.java index e1a52b9..e242f52 100644 --- a/src/main/java/net/pterodactylus/sone/web/IndexPage.java +++ b/src/main/java/net/pterodactylus/sone/web/IndexPage.java @@ -17,6 +17,8 @@ package net.pterodactylus.sone.web; +import static net.pterodactylus.sone.utils.NumberParsers.parseInt; + import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -27,7 +29,6 @@ import net.pterodactylus.sone.data.Sone; import net.pterodactylus.sone.notify.ListNotificationFilters; import net.pterodactylus.sone.web.page.FreenetRequest; import net.pterodactylus.util.collection.Pagination; -import net.pterodactylus.util.number.Numbers; import net.pterodactylus.util.template.Template; import net.pterodactylus.util.template.TemplateContext; @@ -90,7 +91,7 @@ public class IndexPage extends SoneTemplatePage { allPosts = Collections2.filter(allPosts, Post.FUTURE_POSTS_FILTER); List sortedPosts = new ArrayList(allPosts); Collections.sort(sortedPosts, Post.TIME_COMPARATOR); - Pagination pagination = new Pagination(sortedPosts, webInterface.getCore().getPreferences().getPostsPerPage()).setPage(Numbers.safeParseInteger(request.getHttpRequest().getParam("page"), 0)); + Pagination pagination = new Pagination(sortedPosts, webInterface.getCore().getPreferences().getPostsPerPage()).setPage(parseInt(request.getHttpRequest().getParam("page"), 0)); templateContext.set("pagination", pagination); templateContext.set("posts", pagination.getItems()); } diff --git a/src/main/java/net/pterodactylus/sone/web/KnownSonesPage.java b/src/main/java/net/pterodactylus/sone/web/KnownSonesPage.java index dd1b1fc..d2e7abe 100644 --- a/src/main/java/net/pterodactylus/sone/web/KnownSonesPage.java +++ b/src/main/java/net/pterodactylus/sone/web/KnownSonesPage.java @@ -17,6 +17,8 @@ package net.pterodactylus.sone.web; +import static net.pterodactylus.sone.utils.NumberParsers.parseInt; + import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -25,7 +27,6 @@ import java.util.List; import net.pterodactylus.sone.data.Sone; import net.pterodactylus.sone.web.page.FreenetRequest; import net.pterodactylus.util.collection.Pagination; -import net.pterodactylus.util.number.Numbers; import net.pterodactylus.util.template.Template; import net.pterodactylus.util.template.TemplateContext; @@ -41,6 +42,9 @@ import com.google.common.collect.Ordering; */ public class KnownSonesPage extends SoneTemplatePage { + private static final String defaultSortField = "activity"; + private static final String defaultSortOrder = "desc"; + /** * Creates a “known Sones” page. * @@ -63,11 +67,11 @@ public class KnownSonesPage extends SoneTemplatePage { @Override protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException { super.processTemplate(request, templateContext); - String sortField = request.getHttpRequest().getParam("sort"); - String sortOrder = request.getHttpRequest().getParam("order"); + String sortField = request.getHttpRequest().getParam("sort", defaultSortField); + String sortOrder = request.getHttpRequest().getParam("order", defaultSortOrder); String filter = request.getHttpRequest().getParam("filter"); - templateContext.set("sort", (sortField != null) ? sortField : "name"); - templateContext.set("order", (sortOrder != null) ? sortOrder : "asc"); + templateContext.set("sort", sortField); + templateContext.set("order", sortOrder); templateContext.set("filter", filter); final Sone currentSone = getCurrentSone(request.getToadletContext(), false); Collection knownSones = Collections2.filter(webInterface.getCore().getSones(), Sone.EMPTY_SONE_FILTER); @@ -140,7 +144,7 @@ public class KnownSonesPage extends SoneTemplatePage { Collections.sort(sortedSones, Sone.NICE_NAME_COMPARATOR); } } - Pagination sonePagination = new Pagination(sortedSones, 25).setPage(Numbers.safeParseInteger(request.getHttpRequest().getParam("page"), 0)); + Pagination sonePagination = new Pagination(sortedSones, 25).setPage(parseInt(request.getHttpRequest().getParam("page"), 0)); templateContext.set("pagination", sonePagination); templateContext.set("knownSones", sonePagination.getItems()); } diff --git a/src/main/java/net/pterodactylus/sone/web/LockSonePage.java b/src/main/java/net/pterodactylus/sone/web/LockSonePage.java index 604b176..f72252f 100644 --- a/src/main/java/net/pterodactylus/sone/web/LockSonePage.java +++ b/src/main/java/net/pterodactylus/sone/web/LockSonePage.java @@ -53,7 +53,7 @@ public class LockSonePage extends SoneTemplatePage { protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException { super.processTemplate(request, templateContext); String soneId = request.getHttpRequest().getPartAsStringFailsafe("sone", 44); - Sone sone = webInterface.getCore().getLocalSone(soneId, false); + Sone sone = webInterface.getCore().getLocalSone(soneId); if (sone != null) { webInterface.getCore().lockSone(sone); } diff --git a/src/main/java/net/pterodactylus/sone/web/LoginPage.java b/src/main/java/net/pterodactylus/sone/web/LoginPage.java index a837bd1..58e33a1 100644 --- a/src/main/java/net/pterodactylus/sone/web/LoginPage.java +++ b/src/main/java/net/pterodactylus/sone/web/LoginPage.java @@ -17,6 +17,8 @@ package net.pterodactylus.sone.web; +import static java.util.logging.Logger.getLogger; + import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -25,7 +27,6 @@ import java.util.logging.Logger; import net.pterodactylus.sone.data.Sone; import net.pterodactylus.sone.freenet.wot.OwnIdentity; import net.pterodactylus.sone.web.page.FreenetRequest; -import net.pterodactylus.util.logging.Logging; import net.pterodactylus.util.template.Template; import net.pterodactylus.util.template.TemplateContext; import net.pterodactylus.util.web.Method; @@ -40,7 +41,7 @@ public class LoginPage extends SoneTemplatePage { /** The logger. */ @SuppressWarnings("unused") - private static final Logger logger = Logging.getLogger(LoginPage.class); + private static final Logger logger = getLogger("Sone.Web.Login"); /** * Creates a new login page. @@ -70,7 +71,7 @@ public class LoginPage extends SoneTemplatePage { templateContext.set("sones", localSones); if (request.getMethod() == Method.POST) { String soneId = request.getHttpRequest().getPartAsStringFailsafe("sone-id", 100); - Sone selectedSone = webInterface.getCore().getLocalSone(soneId, false); + Sone selectedSone = webInterface.getCore().getLocalSone(soneId); if (selectedSone != null) { setCurrentSone(request.getToadletContext(), selectedSone); String target = request.getHttpRequest().getParam("target"); diff --git a/src/main/java/net/pterodactylus/sone/web/NewPage.java b/src/main/java/net/pterodactylus/sone/web/NewPage.java index 71200d8..5977349 100644 --- a/src/main/java/net/pterodactylus/sone/web/NewPage.java +++ b/src/main/java/net/pterodactylus/sone/web/NewPage.java @@ -17,6 +17,8 @@ package net.pterodactylus.sone.web; +import static net.pterodactylus.sone.utils.NumberParsers.parseInt; + import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; @@ -30,7 +32,6 @@ import net.pterodactylus.sone.data.PostReply; import net.pterodactylus.sone.notify.ListNotificationFilters; import net.pterodactylus.sone.web.page.FreenetRequest; import net.pterodactylus.util.collection.Pagination; -import net.pterodactylus.util.number.Numbers; import net.pterodactylus.util.template.Template; import net.pterodactylus.util.template.TemplateContext; @@ -77,7 +78,7 @@ public class NewPage extends SoneTemplatePage { Collections.sort(sortedPosts, Post.TIME_COMPARATOR); /* paginate them. */ - Pagination pagination = new Pagination(sortedPosts, webInterface.getCore().getPreferences().getPostsPerPage()).setPage(Numbers.safeParseInteger(request.getHttpRequest().getParam("page"), 0)); + Pagination pagination = new Pagination(sortedPosts, webInterface.getCore().getPreferences().getPostsPerPage()).setPage(parseInt(request.getHttpRequest().getParam("page"), 0)); templateContext.set("pagination", pagination); templateContext.set("posts", pagination.getItems()); } diff --git a/src/main/java/net/pterodactylus/sone/web/OptionsPage.java b/src/main/java/net/pterodactylus/sone/web/OptionsPage.java index 094a9cf..c38f97b 100644 --- a/src/main/java/net/pterodactylus/sone/web/OptionsPage.java +++ b/src/main/java/net/pterodactylus/sone/web/OptionsPage.java @@ -17,6 +17,8 @@ package net.pterodactylus.sone.web; +import static net.pterodactylus.sone.utils.NumberParsers.parseInt; + import java.util.ArrayList; import java.util.List; @@ -25,7 +27,6 @@ import net.pterodactylus.sone.data.Sone; import net.pterodactylus.sone.data.Sone.ShowCustomAvatars; import net.pterodactylus.sone.fcp.FcpInterface.FullAccessRequired; import net.pterodactylus.sone.web.page.FreenetRequest; -import net.pterodactylus.util.number.Numbers; import net.pterodactylus.util.template.Template; import net.pterodactylus.util.template.TemplateContext; import net.pterodactylus.util.web.Method; @@ -65,44 +66,44 @@ public class OptionsPage extends SoneTemplatePage { List fieldErrors = new ArrayList(); if (currentSone != null) { boolean autoFollow = request.getHttpRequest().isPartSet("auto-follow"); - currentSone.getOptions().getBooleanOption("AutoFollow").set(autoFollow); + currentSone.getOptions().setAutoFollow(autoFollow); boolean enableSoneInsertNotifications = request.getHttpRequest().isPartSet("enable-sone-insert-notifications"); - currentSone.getOptions().getBooleanOption("EnableSoneInsertNotifications").set(enableSoneInsertNotifications); + currentSone.getOptions().setSoneInsertNotificationEnabled(enableSoneInsertNotifications); boolean showNotificationNewSones = request.getHttpRequest().isPartSet("show-notification-new-sones"); - currentSone.getOptions().getBooleanOption("ShowNotification/NewSones").set(showNotificationNewSones); + currentSone.getOptions().setShowNewSoneNotifications(showNotificationNewSones); boolean showNotificationNewPosts = request.getHttpRequest().isPartSet("show-notification-new-posts"); - currentSone.getOptions().getBooleanOption("ShowNotification/NewPosts").set(showNotificationNewPosts); + currentSone.getOptions().setShowNewPostNotifications(showNotificationNewPosts); boolean showNotificationNewReplies = request.getHttpRequest().isPartSet("show-notification-new-replies"); - currentSone.getOptions().getBooleanOption("ShowNotification/NewReplies").set(showNotificationNewReplies); + currentSone.getOptions().setShowNewReplyNotifications(showNotificationNewReplies); String showCustomAvatars = request.getHttpRequest().getPartAsStringFailsafe("show-custom-avatars", 32); - currentSone.getOptions(). getEnumOption("ShowCustomAvatars").set(ShowCustomAvatars.valueOf(showCustomAvatars)); + currentSone.getOptions().setShowCustomAvatars(ShowCustomAvatars.valueOf(showCustomAvatars)); webInterface.getCore().touchConfiguration(); } - Integer insertionDelay = Numbers.safeParseInteger(request.getHttpRequest().getPartAsStringFailsafe("insertion-delay", 16)); + Integer insertionDelay = parseInt(request.getHttpRequest().getPartAsStringFailsafe("insertion-delay", 16), null); if (!preferences.validateInsertionDelay(insertionDelay)) { fieldErrors.add("insertion-delay"); } else { preferences.setInsertionDelay(insertionDelay); } - Integer postsPerPage = Numbers.safeParseInteger(request.getHttpRequest().getPartAsStringFailsafe("posts-per-page", 4), null); + Integer postsPerPage = parseInt(request.getHttpRequest().getPartAsStringFailsafe("posts-per-page", 4), null); if (!preferences.validatePostsPerPage(postsPerPage)) { fieldErrors.add("posts-per-page"); } else { preferences.setPostsPerPage(postsPerPage); } - Integer imagesPerPage = Numbers.safeParseInteger(request.getHttpRequest().getPartAsStringFailsafe("images-per-page", 4), null); + Integer imagesPerPage = parseInt(request.getHttpRequest().getPartAsStringFailsafe("images-per-page", 4), null); if (!preferences.validateImagesPerPage(imagesPerPage)) { fieldErrors.add("images-per-page"); } else { preferences.setImagesPerPage(imagesPerPage); } - Integer charactersPerPost = Numbers.safeParseInteger(request.getHttpRequest().getPartAsStringFailsafe("characters-per-post", 10), null); + Integer charactersPerPost = parseInt(request.getHttpRequest().getPartAsStringFailsafe("characters-per-post", 10), null); if (!preferences.validateCharactersPerPost(charactersPerPost)) { fieldErrors.add("characters-per-post"); } else { preferences.setCharactersPerPost(charactersPerPost); } - Integer postCutOffLength = Numbers.safeParseInteger(request.getHttpRequest().getPartAsStringFailsafe("post-cut-off-length", 10), null); + Integer postCutOffLength = parseInt(request.getHttpRequest().getPartAsStringFailsafe("post-cut-off-length", 10), null); if (!preferences.validatePostCutOffLength(postCutOffLength)) { fieldErrors.add("post-cut-off-length"); } else { @@ -110,13 +111,13 @@ public class OptionsPage extends SoneTemplatePage { } boolean requireFullAccess = request.getHttpRequest().isPartSet("require-full-access"); preferences.setRequireFullAccess(requireFullAccess); - Integer positiveTrust = Numbers.safeParseInteger(request.getHttpRequest().getPartAsStringFailsafe("positive-trust", 3)); + Integer positiveTrust = parseInt(request.getHttpRequest().getPartAsStringFailsafe("positive-trust", 3), null); if (!preferences.validatePositiveTrust(positiveTrust)) { fieldErrors.add("positive-trust"); } else { preferences.setPositiveTrust(positiveTrust); } - Integer negativeTrust = Numbers.safeParseInteger(request.getHttpRequest().getPartAsStringFailsafe("negative-trust", 4)); + Integer negativeTrust = parseInt(request.getHttpRequest().getPartAsStringFailsafe("negative-trust", 4), null); if (!preferences.validateNegativeTrust(negativeTrust)) { fieldErrors.add("negative-trust"); } else { @@ -129,7 +130,7 @@ public class OptionsPage extends SoneTemplatePage { preferences.setTrustComment(trustComment); boolean fcpInterfaceActive = request.getHttpRequest().isPartSet("fcp-interface-active"); preferences.setFcpInterfaceActive(fcpInterfaceActive); - Integer fcpFullAccessRequiredInteger = Numbers.safeParseInteger(request.getHttpRequest().getPartAsStringFailsafe("fcp-full-access-required", 1), preferences.getFcpFullAccessRequired().ordinal()); + Integer fcpFullAccessRequiredInteger = parseInt(request.getHttpRequest().getPartAsStringFailsafe("fcp-full-access-required", 1), preferences.getFcpFullAccessRequired().ordinal()); FullAccessRequired fcpFullAccessRequired = FullAccessRequired.values()[fcpFullAccessRequiredInteger]; preferences.setFcpFullAccessRequired(fcpFullAccessRequired); webInterface.getCore().touchConfiguration(); @@ -139,12 +140,12 @@ public class OptionsPage extends SoneTemplatePage { templateContext.set("fieldErrors", fieldErrors); } if (currentSone != null) { - templateContext.set("auto-follow", currentSone.getOptions().getBooleanOption("AutoFollow").get()); - templateContext.set("enable-sone-insert-notifications", currentSone.getOptions().getBooleanOption("EnableSoneInsertNotifications").get()); - templateContext.set("show-notification-new-sones", currentSone.getOptions().getBooleanOption("ShowNotification/NewSones").get()); - templateContext.set("show-notification-new-posts", currentSone.getOptions().getBooleanOption("ShowNotification/NewPosts").get()); - templateContext.set("show-notification-new-replies", currentSone.getOptions().getBooleanOption("ShowNotification/NewReplies").get()); - templateContext.set("show-custom-avatars", currentSone.getOptions(). getEnumOption("ShowCustomAvatars").get().name()); + templateContext.set("auto-follow", currentSone.getOptions().isAutoFollow()); + templateContext.set("enable-sone-insert-notifications", currentSone.getOptions().isSoneInsertNotificationEnabled()); + templateContext.set("show-notification-new-sones", currentSone.getOptions().isShowNewSoneNotifications()); + templateContext.set("show-notification-new-posts", currentSone.getOptions().isShowNewPostNotifications()); + templateContext.set("show-notification-new-replies", currentSone.getOptions().isShowNewReplyNotifications()); + templateContext.set("show-custom-avatars", currentSone.getOptions().getShowCustomAvatars().name()); } templateContext.set("insertion-delay", preferences.getInsertionDelay()); templateContext.set("posts-per-page", preferences.getPostsPerPage()); diff --git a/src/main/java/net/pterodactylus/sone/web/RescuePage.java b/src/main/java/net/pterodactylus/sone/web/RescuePage.java index 6888277..8c325d2 100644 --- a/src/main/java/net/pterodactylus/sone/web/RescuePage.java +++ b/src/main/java/net/pterodactylus/sone/web/RescuePage.java @@ -17,10 +17,11 @@ package net.pterodactylus.sone.web; +import static net.pterodactylus.sone.utils.NumberParsers.parseLong; + import net.pterodactylus.sone.core.SoneRescuer; import net.pterodactylus.sone.data.Sone; import net.pterodactylus.sone.web.page.FreenetRequest; -import net.pterodactylus.util.number.Numbers; import net.pterodactylus.util.template.Template; import net.pterodactylus.util.template.TemplateContext; import net.pterodactylus.util.web.Method; @@ -59,7 +60,7 @@ public class RescuePage extends SoneTemplatePage { SoneRescuer soneRescuer = webInterface.getCore().getSoneRescuer(currentSone); if (request.getMethod() == Method.POST) { if ("true".equals(request.getHttpRequest().getPartAsStringFailsafe("fetch", 4))) { - long edition = Numbers.safeParseLong(request.getHttpRequest().getPartAsStringFailsafe("edition", 8), -1L); + long edition = parseLong(request.getHttpRequest().getPartAsStringFailsafe("edition", 8), -1L); if (edition > -1) { soneRescuer.setEdition(edition); } diff --git a/src/main/java/net/pterodactylus/sone/web/SearchPage.java b/src/main/java/net/pterodactylus/sone/web/SearchPage.java index e9241a1..3efba95 100644 --- a/src/main/java/net/pterodactylus/sone/web/SearchPage.java +++ b/src/main/java/net/pterodactylus/sone/web/SearchPage.java @@ -17,6 +17,10 @@ package net.pterodactylus.sone.web; +import static com.google.common.base.Optional.fromNullable; +import static com.google.common.primitives.Ints.tryParse; +import static java.util.logging.Logger.getLogger; + import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -36,8 +40,6 @@ import net.pterodactylus.sone.data.Reply; import net.pterodactylus.sone.data.Sone; import net.pterodactylus.sone.web.page.FreenetRequest; import net.pterodactylus.util.collection.Pagination; -import net.pterodactylus.util.logging.Logging; -import net.pterodactylus.util.number.Numbers; import net.pterodactylus.util.template.Template; import net.pterodactylus.util.template.TemplateContext; import net.pterodactylus.util.text.StringEscaper; @@ -62,7 +64,7 @@ import com.google.common.collect.Ordering; public class SearchPage extends SoneTemplatePage { /** The logger. */ - private static final Logger logger = Logging.getLogger(SearchPage.class); + private static final Logger logger = getLogger("Sone.Web.Search"); /** Short-term cache. */ private final LoadingCache, Set>> hitCache = CacheBuilder.newBuilder().expireAfterWrite(5, TimeUnit.MINUTES).build(new CacheLoader, Set>>() { @@ -149,8 +151,8 @@ public class SearchPage extends SoneTemplatePage { List resultPosts = FluentIterable.from(sortedPostHits).transform(new HitMapper()).toList(); /* pagination. */ - Pagination sonePagination = new Pagination(resultSones, webInterface.getCore().getPreferences().getPostsPerPage()).setPage(Numbers.safeParseInteger(request.getHttpRequest().getParam("sonePage"), 0)); - Pagination postPagination = new Pagination(resultPosts, webInterface.getCore().getPreferences().getPostsPerPage()).setPage(Numbers.safeParseInteger(request.getHttpRequest().getParam("postPage"), 0)); + Pagination sonePagination = new Pagination(resultSones, webInterface.getCore().getPreferences().getPostsPerPage()).setPage(fromNullable(tryParse(request.getHttpRequest().getParam("sonePage"))).or(0)); + Pagination postPagination = new Pagination(resultPosts, webInterface.getCore().getPreferences().getPostsPerPage()).setPage(fromNullable(tryParse(request.getHttpRequest().getParam("postPage"))).or(0)); templateContext.set("sonePagination", sonePagination); templateContext.set("soneHits", sonePagination.getItems()); @@ -201,7 +203,7 @@ public class SearchPage extends SoneTemplatePage { * @return The parsed phrases */ private static List parseSearchPhrases(String query) { - List parsedPhrases = null; + List parsedPhrases; try { parsedPhrases = StringEscaper.parseLine(query); } catch (TextException te1) { @@ -354,7 +356,7 @@ public class SearchPage extends SoneTemplatePage { */ private String getAlbumId(String phrase) { String albumId = phrase.startsWith("album://") ? phrase.substring(8) : phrase; - return (webInterface.getCore().getAlbum(albumId, false) != null) ? albumId : null; + return (webInterface.getCore().getAlbum(albumId) != null) ? albumId : null; } /** @@ -581,7 +583,7 @@ public class SearchPage extends SoneTemplatePage { @Override public boolean apply(Hit hit) { - return (hit == null) ? false : hit.getScore() > 0; + return (hit != null) && (hit.getScore() > 0); } }; diff --git a/src/main/java/net/pterodactylus/sone/web/UnbookmarkPage.java b/src/main/java/net/pterodactylus/sone/web/UnbookmarkPage.java index b568be1..72ff2fc 100644 --- a/src/main/java/net/pterodactylus/sone/web/UnbookmarkPage.java +++ b/src/main/java/net/pterodactylus/sone/web/UnbookmarkPage.java @@ -25,6 +25,8 @@ import net.pterodactylus.util.template.Template; import net.pterodactylus.util.template.TemplateContext; import net.pterodactylus.util.web.Method; +import com.google.common.base.Optional; + /** * Page that lets the user unbookmark a post. * @@ -55,15 +57,18 @@ public class UnbookmarkPage extends SoneTemplatePage { if (request.getMethod() == Method.POST) { String id = request.getHttpRequest().getPartAsStringFailsafe("post", 36); String returnPage = request.getHttpRequest().getPartAsStringFailsafe("returnPage", 256); - webInterface.getCore().unbookmarkPost(id); + Optional post = webInterface.getCore().getPost(id); + if (post.isPresent()) { + webInterface.getCore().unbookmarkPost(post.get()); + } throw new RedirectException(returnPage); } String id = request.getHttpRequest().getParam("post"); if (id.equals("allNotLoaded")) { Set posts = webInterface.getCore().getBookmarkedPosts(); for (Post post : posts) { - if (post.getSone() == null) { - webInterface.getCore().unbookmark(post); + if (post.isLoaded()) { + webInterface.getCore().unbookmarkPost(post); } } throw new RedirectException("bookmarks.html"); diff --git a/src/main/java/net/pterodactylus/sone/web/UnlockSonePage.java b/src/main/java/net/pterodactylus/sone/web/UnlockSonePage.java index ed92246..8491163 100644 --- a/src/main/java/net/pterodactylus/sone/web/UnlockSonePage.java +++ b/src/main/java/net/pterodactylus/sone/web/UnlockSonePage.java @@ -52,7 +52,7 @@ public class UnlockSonePage extends SoneTemplatePage { protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException { super.processTemplate(request, templateContext); String soneId = request.getHttpRequest().getPartAsStringFailsafe("sone", 44); - Sone sone = webInterface.getCore().getLocalSone(soneId, false); + Sone sone = webInterface.getCore().getLocalSone(soneId); if (sone != null) { webInterface.getCore().unlockSone(sone); } diff --git a/src/main/java/net/pterodactylus/sone/web/UploadImagePage.java b/src/main/java/net/pterodactylus/sone/web/UploadImagePage.java index 3844082..5e23a78 100644 --- a/src/main/java/net/pterodactylus/sone/web/UploadImagePage.java +++ b/src/main/java/net/pterodactylus/sone/web/UploadImagePage.java @@ -17,6 +17,8 @@ package net.pterodactylus.sone.web; +import static java.util.logging.Logger.getLogger; + import java.awt.Image; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; @@ -31,12 +33,12 @@ import javax.imageio.ImageReader; import javax.imageio.stream.ImageInputStream; import net.pterodactylus.sone.data.Album; +import net.pterodactylus.sone.data.Image.Modifier.ImageTitleMustNotBeEmpty; import net.pterodactylus.sone.data.Sone; import net.pterodactylus.sone.data.TemporaryImage; import net.pterodactylus.sone.text.TextFilter; import net.pterodactylus.sone.web.page.FreenetRequest; import net.pterodactylus.util.io.Closer; -import net.pterodactylus.util.logging.Logging; import net.pterodactylus.util.template.Template; import net.pterodactylus.util.template.TemplateContext; import net.pterodactylus.util.web.Method; @@ -54,7 +56,7 @@ import freenet.support.api.HTTPUploadedFile; public class UploadImagePage extends SoneTemplatePage { /** The logger. */ - private static final Logger logger = Logging.getLogger(UploadImagePage.class); + private static final Logger logger = getLogger("Sone.Web.UploadImage"); /** * Creates a new “upload image” page. @@ -81,14 +83,12 @@ public class UploadImagePage extends SoneTemplatePage { if (request.getMethod() == Method.POST) { Sone currentSone = getCurrentSone(request.getToadletContext()); String parentId = request.getHttpRequest().getPartAsStringFailsafe("parent", 36); - Album parent = webInterface.getCore().getAlbum(parentId, false); + Album parent = webInterface.getCore().getAlbum(parentId); if (parent == null) { - /* TODO - signal error */ - return; + throw new RedirectException("noPermission.html"); } if (!currentSone.equals(parent.getSone())) { - /* TODO - signal error. */ - return; + throw new RedirectException("noPermission.html"); } String name = request.getHttpRequest().getPartAsStringFailsafe("title", 200); String description = request.getHttpRequest().getPartAsStringFailsafe("description", 4000); @@ -96,7 +96,6 @@ public class UploadImagePage extends SoneTemplatePage { Bucket fileBucket = uploadedFile.getData(); InputStream imageInputStream = null; ByteArrayOutputStream imageDataOutputStream = null; - net.pterodactylus.sone.data.Image image = null; try { imageInputStream = fileBucket.getInputStream(); /* TODO - check length */ @@ -122,11 +121,13 @@ public class UploadImagePage extends SoneTemplatePage { } String mimeType = getMimeType(imageData); TemporaryImage temporaryImage = webInterface.getCore().createTemporaryImage(mimeType, imageData); - image = webInterface.getCore().createImage(currentSone, parent, temporaryImage); + net.pterodactylus.sone.data.Image image = webInterface.getCore().createImage(currentSone, parent, temporaryImage); image.modify().setTitle(name).setDescription(TextFilter.filter(request.getHttpRequest().getHeader("host"), description)).setWidth(uploadedImage.getWidth(null)).setHeight(uploadedImage.getHeight(null)).update(); } catch (IOException ioe1) { logger.log(Level.WARNING, "Could not read uploaded image!", ioe1); return; + } catch (ImageTitleMustNotBeEmpty itmnbe) { + throw new RedirectException("emptyImageTitle.html"); } finally { Closer.close(imageDataInputStream); Closer.flush(uploadedImage); diff --git a/src/main/java/net/pterodactylus/sone/web/ViewSonePage.java b/src/main/java/net/pterodactylus/sone/web/ViewSonePage.java index a94b4af..74461e3 100644 --- a/src/main/java/net/pterodactylus/sone/web/ViewSonePage.java +++ b/src/main/java/net/pterodactylus/sone/web/ViewSonePage.java @@ -17,6 +17,8 @@ package net.pterodactylus.sone.web; +import static net.pterodactylus.sone.utils.NumberParsers.parseInt; + import java.net.URI; import java.util.ArrayList; import java.util.Collections; @@ -32,7 +34,6 @@ import net.pterodactylus.sone.data.Sone; import net.pterodactylus.sone.template.SoneAccessor; import net.pterodactylus.sone.web.page.FreenetRequest; import net.pterodactylus.util.collection.Pagination; -import net.pterodactylus.util.number.Numbers; import net.pterodactylus.util.template.Template; import net.pterodactylus.util.template.TemplateContext; @@ -91,7 +92,7 @@ public class ViewSonePage extends SoneTemplatePage { List sonePosts = sone.get().getPosts(); sonePosts.addAll(webInterface.getCore().getDirectedPosts(sone.get().getId())); Collections.sort(sonePosts, Post.TIME_COMPARATOR); - Pagination postPagination = new Pagination(sonePosts, webInterface.getCore().getPreferences().getPostsPerPage()).setPage(Numbers.safeParseInteger(request.getHttpRequest().getParam("postPage"), 0)); + Pagination postPagination = new Pagination(sonePosts, webInterface.getCore().getPreferences().getPostsPerPage()).setPage(parseInt(request.getHttpRequest().getParam("postPage"), 0)); templateContext.set("postPagination", postPagination); templateContext.set("posts", postPagination.getItems()); Set replies = sone.get().getReplies(); @@ -113,7 +114,7 @@ public class ViewSonePage extends SoneTemplatePage { }); - Pagination repliedPostPagination = new Pagination(posts, webInterface.getCore().getPreferences().getPostsPerPage()).setPage(Numbers.safeParseInteger(request.getHttpRequest().getParam("repliedPostPage"), 0)); + Pagination repliedPostPagination = new Pagination(posts, webInterface.getCore().getPreferences().getPostsPerPage()).setPage(parseInt(request.getHttpRequest().getParam("repliedPostPage"), 0)); templateContext.set("repliedPostPagination", repliedPostPagination); templateContext.set("repliedPosts", repliedPostPagination.getItems()); } diff --git a/src/main/java/net/pterodactylus/sone/web/WebInterface.java b/src/main/java/net/pterodactylus/sone/web/WebInterface.java index 593193c..9fe5bba 100644 --- a/src/main/java/net/pterodactylus/sone/web/WebInterface.java +++ b/src/main/java/net/pterodactylus/sone/web/WebInterface.java @@ -17,6 +17,9 @@ package net.pterodactylus.sone.web; +import static java.util.logging.Logger.getLogger; +import static net.pterodactylus.util.template.TemplateParser.parse; + import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; @@ -125,7 +128,7 @@ import net.pterodactylus.sone.web.ajax.UntrustAjaxPage; import net.pterodactylus.sone.web.page.FreenetRequest; import net.pterodactylus.sone.web.page.PageToadlet; import net.pterodactylus.sone.web.page.PageToadletFactory; -import net.pterodactylus.util.logging.Logging; +import net.pterodactylus.util.io.Closer; import net.pterodactylus.util.notify.Notification; import net.pterodactylus.util.notify.NotificationManager; import net.pterodactylus.util.notify.TemplateNotification; @@ -143,7 +146,6 @@ import net.pterodactylus.util.template.ReplaceFilter; import net.pterodactylus.util.template.StoreFilter; import net.pterodactylus.util.template.Template; import net.pterodactylus.util.template.TemplateContextFactory; -import net.pterodactylus.util.template.TemplateParser; import net.pterodactylus.util.template.TemplateProvider; import net.pterodactylus.util.template.XmlFilter; import net.pterodactylus.util.web.RedirectPage; @@ -171,7 +173,7 @@ import freenet.support.api.HTTPRequest; public class WebInterface { /** The logger. */ - private static final Logger logger = Logging.getLogger(WebInterface.class); + private static final Logger logger = getLogger("Sone.Web.Main"); /** The notification manager. */ private final NotificationManager notificationManager = new NotificationManager(); @@ -287,40 +289,55 @@ public class WebInterface { templateContextFactory.addTemplateObject("formPassword", formPassword); /* create notifications. */ - Template newSoneNotificationTemplate = TemplateParser.parse(createReader("/templates/notify/newSoneNotification.html")); + Template newSoneNotificationTemplate = parseTemplate("/templates/notify/newSoneNotification.html"); newSoneNotification = new ListNotification("new-sone-notification", "sones", newSoneNotificationTemplate, false); - Template newPostNotificationTemplate = TemplateParser.parse(createReader("/templates/notify/newPostNotification.html")); + Template newPostNotificationTemplate = parseTemplate("/templates/notify/newPostNotification.html"); newPostNotification = new ListNotification("new-post-notification", "posts", newPostNotificationTemplate, false); - Template localPostNotificationTemplate = TemplateParser.parse(createReader("/templates/notify/newPostNotification.html")); + Template localPostNotificationTemplate = parseTemplate("/templates/notify/newPostNotification.html"); localPostNotification = new ListNotification("local-post-notification", "posts", localPostNotificationTemplate, false); - Template newReplyNotificationTemplate = TemplateParser.parse(createReader("/templates/notify/newReplyNotification.html")); + Template newReplyNotificationTemplate = parseTemplate("/templates/notify/newReplyNotification.html"); newReplyNotification = new ListNotification("new-reply-notification", "replies", newReplyNotificationTemplate, false); - Template localReplyNotificationTemplate = TemplateParser.parse(createReader("/templates/notify/newReplyNotification.html")); + Template localReplyNotificationTemplate = parseTemplate("/templates/notify/newReplyNotification.html"); localReplyNotification = new ListNotification("local-reply-notification", "replies", localReplyNotificationTemplate, false); - Template mentionNotificationTemplate = TemplateParser.parse(createReader("/templates/notify/mentionNotification.html")); + Template mentionNotificationTemplate = parseTemplate("/templates/notify/mentionNotification.html"); mentionNotification = new ListNotification("mention-notification", "posts", mentionNotificationTemplate, false); - Template lockedSonesTemplate = TemplateParser.parse(createReader("/templates/notify/lockedSonesNotification.html")); + Template lockedSonesTemplate = parseTemplate("/templates/notify/lockedSonesNotification.html"); lockedSonesNotification = new ListNotification("sones-locked-notification", "sones", lockedSonesTemplate); - Template newVersionTemplate = TemplateParser.parse(createReader("/templates/notify/newVersionNotification.html")); + Template newVersionTemplate = parseTemplate("/templates/notify/newVersionNotification.html"); newVersionNotification = new TemplateNotification("new-version-notification", newVersionTemplate); - Template insertingImagesTemplate = TemplateParser.parse(createReader("/templates/notify/inserting-images-notification.html")); + Template insertingImagesTemplate = parseTemplate("/templates/notify/inserting-images-notification.html"); insertingImagesNotification = new ListNotification("inserting-images-notification", "images", insertingImagesTemplate); - Template insertedImagesTemplate = TemplateParser.parse(createReader("/templates/notify/inserted-images-notification.html")); + Template insertedImagesTemplate = parseTemplate("/templates/notify/inserted-images-notification.html"); insertedImagesNotification = new ListNotification("inserted-images-notification", "images", insertedImagesTemplate); - Template imageInsertFailedTemplate = TemplateParser.parse(createReader("/templates/notify/image-insert-failed-notification.html")); + Template imageInsertFailedTemplate = parseTemplate("/templates/notify/image-insert-failed-notification.html"); imageInsertFailedNotification = new ListNotification("image-insert-failed-notification", "images", imageInsertFailedTemplate); } + private Template parseTemplate(String resourceName) { + InputStream templateInputStream = null; + Reader reader = null; + try { + templateInputStream = getClass().getResourceAsStream(resourceName); + reader = new InputStreamReader(templateInputStream, "UTF-8"); + return parse(reader); + } catch (UnsupportedEncodingException uee1) { + throw new RuntimeException("UTF-8 not supported."); + } finally { + Closer.close(reader); + Closer.close(templateInputStream); + } + } + // // ACCESSORS // @@ -423,7 +440,7 @@ public class WebInterface { if (soneId == null) { return null; } - return getCore().getLocalSone(soneId, false); + return getCore().getLocalSone(soneId); } /** @@ -509,7 +526,7 @@ public class WebInterface { */ public void setFirstStart(boolean firstStart) { if (firstStart) { - Template firstStartNotificationTemplate = TemplateParser.parse(createReader("/templates/notify/firstStartNotification.html")); + Template firstStartNotificationTemplate = parseTemplate("/templates/notify/firstStartNotification.html"); Notification firstStartNotification = new TemplateNotification("first-start-notification", firstStartNotificationTemplate); notificationManager.addNotification(firstStartNotification); } @@ -524,7 +541,7 @@ public class WebInterface { */ public void setNewConfig(boolean newConfig) { if (newConfig && !hasFirstStartNotification()) { - Template configNotReadNotificationTemplate = TemplateParser.parse(createReader("/templates/notify/configNotReadNotification.html")); + Template configNotReadNotificationTemplate = parseTemplate("/templates/notify/configNotReadNotification.html"); Notification configNotReadNotification = new TemplateNotification("config-not-read-notification", configNotReadNotificationTemplate); notificationManager.addNotification(configNotReadNotification); } @@ -555,7 +572,7 @@ public class WebInterface { registerToadlets(); /* notification templates. */ - Template startupNotificationTemplate = TemplateParser.parse(createReader("/templates/notify/startupNotification.html")); + Template startupNotificationTemplate = parseTemplate("/templates/notify/startupNotification.html"); final TemplateNotification startupNotification = new TemplateNotification("startup-notification", startupNotificationTemplate); notificationManager.addNotification(startupNotification); @@ -568,7 +585,7 @@ public class WebInterface { } }, 2, TimeUnit.MINUTES); - Template wotMissingNotificationTemplate = TemplateParser.parse(createReader("/templates/notify/wotMissingNotification.html")); + Template wotMissingNotificationTemplate = parseTemplate("/templates/notify/wotMissingNotification.html"); final TemplateNotification wotMissingNotification = new TemplateNotification("wot-missing-notification", wotMissingNotificationTemplate); ticker.scheduleAtFixedRate(new Runnable() { @@ -601,36 +618,38 @@ public class WebInterface { * Register all toadlets. */ private void registerToadlets() { - Template emptyTemplate = TemplateParser.parse(new StringReader("")); - Template loginTemplate = TemplateParser.parse(createReader("/templates/login.html")); - Template indexTemplate = TemplateParser.parse(createReader("/templates/index.html")); - Template newTemplate = TemplateParser.parse(createReader("/templates/new.html")); - Template knownSonesTemplate = TemplateParser.parse(createReader("/templates/knownSones.html")); - Template createSoneTemplate = TemplateParser.parse(createReader("/templates/createSone.html")); - Template createPostTemplate = TemplateParser.parse(createReader("/templates/createPost.html")); - Template createReplyTemplate = TemplateParser.parse(createReader("/templates/createReply.html")); - Template bookmarksTemplate = TemplateParser.parse(createReader("/templates/bookmarks.html")); - Template searchTemplate = TemplateParser.parse(createReader("/templates/search.html")); - Template editProfileTemplate = TemplateParser.parse(createReader("/templates/editProfile.html")); - Template editProfileFieldTemplate = TemplateParser.parse(createReader("/templates/editProfileField.html")); - Template deleteProfileFieldTemplate = TemplateParser.parse(createReader("/templates/deleteProfileField.html")); - Template viewSoneTemplate = TemplateParser.parse(createReader("/templates/viewSone.html")); - Template viewPostTemplate = TemplateParser.parse(createReader("/templates/viewPost.html")); - Template deletePostTemplate = TemplateParser.parse(createReader("/templates/deletePost.html")); - Template deleteReplyTemplate = TemplateParser.parse(createReader("/templates/deleteReply.html")); - Template deleteSoneTemplate = TemplateParser.parse(createReader("/templates/deleteSone.html")); - Template imageBrowserTemplate = TemplateParser.parse(createReader("/templates/imageBrowser.html")); - Template createAlbumTemplate = TemplateParser.parse(createReader("/templates/createAlbum.html")); - Template deleteAlbumTemplate = TemplateParser.parse(createReader("/templates/deleteAlbum.html")); - Template deleteImageTemplate = TemplateParser.parse(createReader("/templates/deleteImage.html")); - Template noPermissionTemplate = TemplateParser.parse(createReader("/templates/noPermission.html")); - Template optionsTemplate = TemplateParser.parse(createReader("/templates/options.html")); - Template rescueTemplate = TemplateParser.parse(createReader("/templates/rescue.html")); - Template aboutTemplate = TemplateParser.parse(createReader("/templates/about.html")); - Template invalidTemplate = TemplateParser.parse(createReader("/templates/invalid.html")); - Template postTemplate = TemplateParser.parse(createReader("/templates/include/viewPost.html")); - Template replyTemplate = TemplateParser.parse(createReader("/templates/include/viewReply.html")); - Template openSearchTemplate = TemplateParser.parse(createReader("/templates/xml/OpenSearch.xml")); + Template emptyTemplate = parse(new StringReader("")); + Template loginTemplate = parseTemplate("/templates/login.html"); + Template indexTemplate = parseTemplate("/templates/index.html"); + Template newTemplate = parseTemplate("/templates/new.html"); + Template knownSonesTemplate = parseTemplate("/templates/knownSones.html"); + Template createSoneTemplate = parseTemplate("/templates/createSone.html"); + Template createPostTemplate = parseTemplate("/templates/createPost.html"); + Template createReplyTemplate = parseTemplate("/templates/createReply.html"); + Template bookmarksTemplate = parseTemplate("/templates/bookmarks.html"); + Template searchTemplate = parseTemplate("/templates/search.html"); + Template editProfileTemplate = parseTemplate("/templates/editProfile.html"); + Template editProfileFieldTemplate = parseTemplate("/templates/editProfileField.html"); + Template deleteProfileFieldTemplate = parseTemplate("/templates/deleteProfileField.html"); + Template viewSoneTemplate = parseTemplate("/templates/viewSone.html"); + Template viewPostTemplate = parseTemplate("/templates/viewPost.html"); + Template deletePostTemplate = parseTemplate("/templates/deletePost.html"); + Template deleteReplyTemplate = parseTemplate("/templates/deleteReply.html"); + Template deleteSoneTemplate = parseTemplate("/templates/deleteSone.html"); + Template imageBrowserTemplate = parseTemplate("/templates/imageBrowser.html"); + Template createAlbumTemplate = parseTemplate("/templates/createAlbum.html"); + Template deleteAlbumTemplate = parseTemplate("/templates/deleteAlbum.html"); + Template deleteImageTemplate = parseTemplate("/templates/deleteImage.html"); + Template noPermissionTemplate = parseTemplate("/templates/noPermission.html"); + Template emptyImageTitleTemplate = parseTemplate("/templates/emptyImageTitle.html"); + Template emptyAlbumTitleTemplate = parseTemplate("/templates/emptyAlbumTitle.html"); + Template optionsTemplate = parseTemplate("/templates/options.html"); + Template rescueTemplate = parseTemplate("/templates/rescue.html"); + Template aboutTemplate = parseTemplate("/templates/about.html"); + Template invalidTemplate = parseTemplate("/templates/invalid.html"); + Template postTemplate = parseTemplate("/templates/include/viewPost.html"); + Template replyTemplate = parseTemplate("/templates/include/viewReply.html"); + Template openSearchTemplate = parseTemplate("/templates/xml/OpenSearch.xml"); PageToadletFactory pageToadletFactory = new PageToadletFactory(sonePlugin.pluginRespirator().getHLSimpleClient(), "/Sone/"); pageToadlets.add(pageToadletFactory.createPageToadlet(new RedirectPage("", "index.html"))); @@ -675,6 +694,8 @@ public class WebInterface { pageToadlets.add(pageToadletFactory.createPageToadlet(new RescuePage(rescueTemplate, this), "Rescue")); pageToadlets.add(pageToadletFactory.createPageToadlet(new AboutPage(aboutTemplate, this, SonePlugin.VERSION), "About")); pageToadlets.add(pageToadletFactory.createPageToadlet(new SoneTemplatePage("noPermission.html", noPermissionTemplate, "Page.NoPermission.Title", this))); + pageToadlets.add(pageToadletFactory.createPageToadlet(new SoneTemplatePage("emptyImageTitle.html", emptyImageTitleTemplate, "Page.EmptyImageTitle.Title", this))); + pageToadlets.add(pageToadletFactory.createPageToadlet(new SoneTemplatePage("emptyAlbumTitle.html", emptyAlbumTitleTemplate, "Page.EmptyAlbumTitle.Title", this))); pageToadlets.add(pageToadletFactory.createPageToadlet(new DismissNotificationPage(emptyTemplate, this))); pageToadlets.add(pageToadletFactory.createPageToadlet(new SoneTemplatePage("invalid.html", invalidTemplate, "Page.Invalid.Title", this))); pageToadlets.add(pageToadletFactory.createPageToadlet(new StaticPage("css/", "/static/css/", "text/css"))); @@ -736,22 +757,6 @@ public class WebInterface { } /** - * Creates a {@link Reader} from the {@link InputStream} for the resource - * with the given name. - * - * @param resourceName - * The name of the resource - * @return A {@link Reader} for the resource - */ - private Reader createReader(String resourceName) { - try { - return new InputStreamReader(getClass().getResourceAsStream(resourceName), "UTF-8"); - } catch (UnsupportedEncodingException uee1) { - return null; - } - } - - /** * Returns all {@link Sone#isLocal() local Sone}s that are referenced by * {@link SonePart}s in the given text (after parsing it using * {@link SoneTextParser}). @@ -788,7 +793,7 @@ public class WebInterface { synchronized (soneInsertNotifications) { TemplateNotification templateNotification = soneInsertNotifications.get(sone); if (templateNotification == null) { - templateNotification = new TemplateNotification(TemplateParser.parse(createReader("/templates/notify/soneInsertNotification.html"))); + templateNotification = new TemplateNotification(parseTemplate("/templates/notify/soneInsertNotification.html")); templateNotification.set("insertSone", sone); soneInsertNotifications.put(sone, templateNotification); } @@ -796,6 +801,23 @@ public class WebInterface { } } + private boolean localSoneMentionedInNewPostOrReply(Post post) { + if (!post.getSone().isLocal()) { + if (!getMentionedSones(post.getText()).isEmpty() && !post.isKnown()) { + return true; + } + } + for (PostReply postReply : getCore().getReplies(post.getId())) { + if (postReply.getSone().isLocal()) { + continue; + } + if (!getMentionedSones(postReply.getText()).isEmpty() && !postReply.isKnown()) { + return true; + } + } + return false; + } + // // EVENT HANDLERS // @@ -857,7 +879,7 @@ public class WebInterface { } if (!hasFirstStartNotification()) { notificationManager.addNotification(isLocal ? localReplyNotification : newReplyNotification); - if (!getMentionedSones(reply.getText()).isEmpty() && !isLocal && reply.getPost().isPresent() && (reply.getTime() <= System.currentTimeMillis())) { + if (reply.getPost().isPresent() && localSoneMentionedInNewPostOrReply(reply.getPost().get())) { mentionNotification.add(reply.getPost().get()); notificationManager.addNotification(mentionNotification); } @@ -887,7 +909,9 @@ public class WebInterface { public void markPostKnown(MarkPostKnownEvent markPostKnownEvent) { newPostNotification.remove(markPostKnownEvent.post()); localPostNotification.remove(markPostKnownEvent.post()); - mentionNotification.remove(markPostKnownEvent.post()); + if (!localSoneMentionedInNewPostOrReply(markPostKnownEvent.post())) { + mentionNotification.remove(markPostKnownEvent.post()); + } } /** @@ -898,9 +922,12 @@ public class WebInterface { */ @Subscribe public void markReplyKnown(MarkPostReplyKnownEvent markPostReplyKnownEvent) { - newReplyNotification.remove(markPostReplyKnownEvent.postReply()); - localReplyNotification.remove(markPostReplyKnownEvent.postReply()); - mentionNotification.remove(markPostReplyKnownEvent.postReply().getPost().get()); + PostReply postReply = markPostReplyKnownEvent.postReply(); + newReplyNotification.remove(postReply); + localReplyNotification.remove(postReply); + if (postReply.getPost().isPresent() && !localSoneMentionedInNewPostOrReply(postReply.getPost().get())) { + mentionNotification.remove(postReply.getPost().get()); + } } /** @@ -924,7 +951,9 @@ public class WebInterface { public void postRemoved(PostRemovedEvent postRemovedEvent) { newPostNotification.remove(postRemovedEvent.post()); localPostNotification.remove(postRemovedEvent.post()); - mentionNotification.remove(postRemovedEvent.post()); + if (!localSoneMentionedInNewPostOrReply(postRemovedEvent.post())) { + mentionNotification.remove(postRemovedEvent.post()); + } } /** @@ -938,14 +967,8 @@ public class WebInterface { PostReply reply = postReplyRemovedEvent.postReply(); newReplyNotification.remove(reply); localReplyNotification.remove(reply); - if (!getMentionedSones(reply.getText()).isEmpty() && reply.getPost().isPresent()) { - boolean isMentioned = false; - for (PostReply existingReply : getCore().getReplies(reply.getPostId())) { - isMentioned |= !reply.isKnown() && !getMentionedSones(existingReply.getText()).isEmpty(); - } - if (!isMentioned) { - mentionNotification.remove(reply.getPost().get()); - } + if (reply.getPost().isPresent() && !localSoneMentionedInNewPostOrReply(reply.getPost().get())) { + mentionNotification.remove(reply.getPost().get()); } } @@ -992,7 +1015,7 @@ public class WebInterface { public void soneInserting(SoneInsertingEvent soneInsertingEvent) { TemplateNotification soneInsertNotification = getSoneInsertNotification(soneInsertingEvent.sone()); soneInsertNotification.set("soneStatus", "inserting"); - if (soneInsertingEvent.sone().getOptions().getBooleanOption("EnableSoneInsertNotifications").get()) { + if (soneInsertingEvent.sone().getOptions().isSoneInsertNotificationEnabled()) { notificationManager.addNotification(soneInsertNotification); } } @@ -1008,7 +1031,7 @@ public class WebInterface { TemplateNotification soneInsertNotification = getSoneInsertNotification(soneInsertedEvent.sone()); soneInsertNotification.set("soneStatus", "inserted"); soneInsertNotification.set("insertDuration", soneInsertedEvent.insertDuration() / 1000); - if (soneInsertedEvent.sone().getOptions().getBooleanOption("EnableSoneInsertNotifications").get()) { + if (soneInsertedEvent.sone().getOptions().isSoneInsertNotificationEnabled()) { notificationManager.addNotification(soneInsertNotification); } } @@ -1024,7 +1047,7 @@ public class WebInterface { TemplateNotification soneInsertNotification = getSoneInsertNotification(soneInsertAbortedEvent.sone()); soneInsertNotification.set("soneStatus", "insert-aborted"); soneInsertNotification.set("insert-error", soneInsertAbortedEvent.cause()); - if (soneInsertAbortedEvent.sone().getOptions().getBooleanOption("EnableSoneInsertNotifications").get()) { + if (soneInsertAbortedEvent.sone().getOptions().isSoneInsertNotificationEnabled()) { notificationManager.addNotification(soneInsertNotification); } } diff --git a/src/main/java/net/pterodactylus/sone/web/ajax/BookmarkAjaxPage.java b/src/main/java/net/pterodactylus/sone/web/ajax/BookmarkAjaxPage.java index acee365..42719d3 100644 --- a/src/main/java/net/pterodactylus/sone/web/ajax/BookmarkAjaxPage.java +++ b/src/main/java/net/pterodactylus/sone/web/ajax/BookmarkAjaxPage.java @@ -17,9 +17,12 @@ package net.pterodactylus.sone.web.ajax; +import net.pterodactylus.sone.data.Post; import net.pterodactylus.sone.web.WebInterface; import net.pterodactylus.sone.web.page.FreenetRequest; +import com.google.common.base.Optional; + /** * AJAX page that lets the user bookmark a post. * @@ -50,7 +53,10 @@ public class BookmarkAjaxPage extends JsonPage { if ((id == null) || (id.length() == 0)) { return createErrorJsonObject("invalid-post-id"); } - webInterface.getCore().bookmarkPost(id); + Optional post = webInterface.getCore().getPost(id); + if (post.isPresent()) { + webInterface.getCore().bookmarkPost(post.get()); + } return createSuccessJsonObject(); } diff --git a/src/main/java/net/pterodactylus/sone/web/ajax/CreatePostAjaxPage.java b/src/main/java/net/pterodactylus/sone/web/ajax/CreatePostAjaxPage.java index 423d2df..5cb1047 100644 --- a/src/main/java/net/pterodactylus/sone/web/ajax/CreatePostAjaxPage.java +++ b/src/main/java/net/pterodactylus/sone/web/ajax/CreatePostAjaxPage.java @@ -54,7 +54,7 @@ public class CreatePostAjaxPage extends JsonPage { String recipientId = request.getHttpRequest().getParam("recipient"); Optional recipient = webInterface.getCore().getSone(recipientId); String senderId = request.getHttpRequest().getParam("sender"); - Sone sender = webInterface.getCore().getLocalSone(senderId, false); + Sone sender = webInterface.getCore().getLocalSone(senderId); if (sender == null) { sender = sone; } diff --git a/src/main/java/net/pterodactylus/sone/web/ajax/CreateReplyAjaxPage.java b/src/main/java/net/pterodactylus/sone/web/ajax/CreateReplyAjaxPage.java index ea2a2c2..3449dec 100644 --- a/src/main/java/net/pterodactylus/sone/web/ajax/CreateReplyAjaxPage.java +++ b/src/main/java/net/pterodactylus/sone/web/ajax/CreateReplyAjaxPage.java @@ -55,7 +55,7 @@ public class CreateReplyAjaxPage extends JsonPage { String postId = request.getHttpRequest().getParam("post"); String text = request.getHttpRequest().getParam("text").trim(); String senderId = request.getHttpRequest().getParam("sender"); - Sone sender = webInterface.getCore().getLocalSone(senderId, false); + Sone sender = webInterface.getCore().getLocalSone(senderId); if (sender == null) { sender = getCurrentSone(request.getToadletContext()); } diff --git a/src/main/java/net/pterodactylus/sone/web/ajax/EditAlbumAjaxPage.java b/src/main/java/net/pterodactylus/sone/web/ajax/EditAlbumAjaxPage.java index 74a73b8..238206a 100644 --- a/src/main/java/net/pterodactylus/sone/web/ajax/EditAlbumAjaxPage.java +++ b/src/main/java/net/pterodactylus/sone/web/ajax/EditAlbumAjaxPage.java @@ -49,7 +49,7 @@ public class EditAlbumAjaxPage extends JsonPage { @Override protected JsonReturnObject createJsonObject(FreenetRequest request) { String albumId = request.getHttpRequest().getParam("album"); - Album album = webInterface.getCore().getAlbum(albumId, false); + Album album = webInterface.getCore().getAlbum(albumId); if (album == null) { return createErrorJsonObject("invalid-album-id"); } @@ -68,9 +68,13 @@ public class EditAlbumAjaxPage extends JsonPage { } String title = request.getHttpRequest().getParam("title").trim(); String description = request.getHttpRequest().getParam("description").trim(); - album.modify().setTitle(title).setDescription(TextFilter.filter(request.getHttpRequest().getHeader("host"), description)).update(); - webInterface.getCore().touchConfiguration(); - return createSuccessJsonObject().put("albumId", album.getId()).put("title", album.getTitle()).put("description", album.getDescription()); + try { + album.modify().setTitle(title).setDescription(TextFilter.filter(request.getHttpRequest().getHeader("host"), description)).update(); + webInterface.getCore().touchConfiguration(); + return createSuccessJsonObject().put("albumId", album.getId()).put("title", album.getTitle()).put("description", album.getDescription()); + } catch (IllegalStateException e) { + return createErrorJsonObject("invalid-album-title"); + } } } diff --git a/src/main/java/net/pterodactylus/sone/web/ajax/EditImageAjaxPage.java b/src/main/java/net/pterodactylus/sone/web/ajax/EditImageAjaxPage.java index 0c45225..79de200 100644 --- a/src/main/java/net/pterodactylus/sone/web/ajax/EditImageAjaxPage.java +++ b/src/main/java/net/pterodactylus/sone/web/ajax/EditImageAjaxPage.java @@ -77,6 +77,9 @@ public class EditImageAjaxPage extends JsonPage { return createSuccessJsonObject().put("sourceImageId", image.getId()).put("destinationImageId", swappedImage.getId()); } String title = request.getHttpRequest().getParam("title").trim(); + if (title.isEmpty()) { + return createErrorJsonObject("invalid-image-title"); + } String description = request.getHttpRequest().getParam("description").trim(); image.modify().setTitle(title).setDescription(TextFilter.filter(request.getHttpRequest().getHeader("host"), description)).update(); webInterface.getCore().touchConfiguration(); diff --git a/src/main/java/net/pterodactylus/sone/web/ajax/GetNotificationsAjaxPage.java b/src/main/java/net/pterodactylus/sone/web/ajax/GetNotificationsAjaxPage.java index e8107ef..a25e665 100644 --- a/src/main/java/net/pterodactylus/sone/web/ajax/GetNotificationsAjaxPage.java +++ b/src/main/java/net/pterodactylus/sone/web/ajax/GetNotificationsAjaxPage.java @@ -147,9 +147,9 @@ public class GetNotificationsAjaxPage extends JsonPage { private static JsonNode createJsonOptions(Sone currentSone) { ObjectNode options = new ObjectNode(instance); if (currentSone != null) { - options.put("ShowNotification/NewSones", currentSone.getOptions().getBooleanOption("ShowNotification/NewSones").get()); - options.put("ShowNotification/NewPosts", currentSone.getOptions().getBooleanOption("ShowNotification/NewPosts").get()); - options.put("ShowNotification/NewReplies", currentSone.getOptions().getBooleanOption("ShowNotification/NewReplies").get()); + options.put("ShowNotification/NewSones", currentSone.getOptions().isShowNewSoneNotifications()); + options.put("ShowNotification/NewPosts", currentSone.getOptions().isShowNewPostNotifications()); + options.put("ShowNotification/NewReplies", currentSone.getOptions().isShowNewReplyNotifications()); } return options; } diff --git a/src/main/java/net/pterodactylus/sone/web/ajax/GetStatusAjaxPage.java b/src/main/java/net/pterodactylus/sone/web/ajax/GetStatusAjaxPage.java index e43910c..cc095a2 100644 --- a/src/main/java/net/pterodactylus/sone/web/ajax/GetStatusAjaxPage.java +++ b/src/main/java/net/pterodactylus/sone/web/ajax/GetStatusAjaxPage.java @@ -192,9 +192,9 @@ public class GetStatusAjaxPage extends JsonPage { private static JsonNode createJsonOptions(Sone currentSone) { ObjectNode options = new ObjectNode(instance); if (currentSone != null) { - options.put("ShowNotification/NewSones", currentSone.getOptions().getBooleanOption("ShowNotification/NewSones").get()); - options.put("ShowNotification/NewPosts", currentSone.getOptions().getBooleanOption("ShowNotification/NewPosts").get()); - options.put("ShowNotification/NewReplies", currentSone.getOptions().getBooleanOption("ShowNotification/NewReplies").get()); + options.put("ShowNotification/NewSones", currentSone.getOptions().isShowNewSoneNotifications()); + options.put("ShowNotification/NewPosts", currentSone.getOptions().isShowNewPostNotifications()); + options.put("ShowNotification/NewReplies", currentSone.getOptions().isShowNewReplyNotifications()); } return options; } diff --git a/src/main/java/net/pterodactylus/sone/web/ajax/JsonPage.java b/src/main/java/net/pterodactylus/sone/web/ajax/JsonPage.java index 3c7f587..6d6d92a 100644 --- a/src/main/java/net/pterodactylus/sone/web/ajax/JsonPage.java +++ b/src/main/java/net/pterodactylus/sone/web/ajax/JsonPage.java @@ -17,6 +17,8 @@ package net.pterodactylus.sone.web.ajax; +import static java.util.logging.Logger.getLogger; + import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStreamWriter; @@ -30,7 +32,6 @@ import net.pterodactylus.sone.web.WebInterface; import net.pterodactylus.sone.web.page.FreenetPage; import net.pterodactylus.sone.web.page.FreenetRequest; import net.pterodactylus.util.io.Closer; -import net.pterodactylus.util.logging.Logging; import net.pterodactylus.util.web.Page; import net.pterodactylus.util.web.Response; @@ -47,7 +48,7 @@ import freenet.clients.http.ToadletContext; public abstract class JsonPage implements FreenetPage { /** The logger. */ - private static final Logger logger = Logging.getLogger(JsonPage.class); + private static final Logger logger = getLogger("Sone.Web.Ajax"); /** The JSON serializer. */ private static final ObjectMapper objectMapper = new ObjectMapper(); diff --git a/src/main/java/net/pterodactylus/sone/web/ajax/LockSoneAjaxPage.java b/src/main/java/net/pterodactylus/sone/web/ajax/LockSoneAjaxPage.java index 68668b7..cc04338 100644 --- a/src/main/java/net/pterodactylus/sone/web/ajax/LockSoneAjaxPage.java +++ b/src/main/java/net/pterodactylus/sone/web/ajax/LockSoneAjaxPage.java @@ -45,7 +45,7 @@ public class LockSoneAjaxPage extends JsonPage { @Override protected JsonReturnObject createJsonObject(FreenetRequest request) { String soneId = request.getHttpRequest().getParam("sone"); - Sone sone = webInterface.getCore().getLocalSone(soneId, false); + Sone sone = webInterface.getCore().getLocalSone(soneId); if (sone == null) { return createErrorJsonObject("invalid-sone-id"); } diff --git a/src/main/java/net/pterodactylus/sone/web/ajax/UnbookmarkAjaxPage.java b/src/main/java/net/pterodactylus/sone/web/ajax/UnbookmarkAjaxPage.java index 65bb14d..064bedc 100644 --- a/src/main/java/net/pterodactylus/sone/web/ajax/UnbookmarkAjaxPage.java +++ b/src/main/java/net/pterodactylus/sone/web/ajax/UnbookmarkAjaxPage.java @@ -17,9 +17,12 @@ package net.pterodactylus.sone.web.ajax; +import net.pterodactylus.sone.data.Post; import net.pterodactylus.sone.web.WebInterface; import net.pterodactylus.sone.web.page.FreenetRequest; +import com.google.common.base.Optional; + /** * AJAX page that lets the user unbookmark a post. * @@ -50,7 +53,10 @@ public class UnbookmarkAjaxPage extends JsonPage { if ((id == null) || (id.length() == 0)) { return createErrorJsonObject("invalid-post-id"); } - webInterface.getCore().unbookmarkPost(id); + Optional post = webInterface.getCore().getPost(id); + if (post.isPresent()) { + webInterface.getCore().unbookmarkPost(post.get()); + } return createSuccessJsonObject(); } diff --git a/src/main/java/net/pterodactylus/sone/web/ajax/UnlockSoneAjaxPage.java b/src/main/java/net/pterodactylus/sone/web/ajax/UnlockSoneAjaxPage.java index f92132d..3a91f81 100644 --- a/src/main/java/net/pterodactylus/sone/web/ajax/UnlockSoneAjaxPage.java +++ b/src/main/java/net/pterodactylus/sone/web/ajax/UnlockSoneAjaxPage.java @@ -45,7 +45,7 @@ public class UnlockSoneAjaxPage extends JsonPage { @Override protected JsonReturnObject createJsonObject(FreenetRequest request) { String soneId = request.getHttpRequest().getParam("sone"); - Sone sone = webInterface.getCore().getLocalSone(soneId, false); + Sone sone = webInterface.getCore().getLocalSone(soneId); if (sone == null) { return createErrorJsonObject("invalid-sone-id"); } diff --git a/src/main/java/net/pterodactylus/sone/web/page/FreenetTemplatePage.java b/src/main/java/net/pterodactylus/sone/web/page/FreenetTemplatePage.java index 625978b..40e19b1 100644 --- a/src/main/java/net/pterodactylus/sone/web/page/FreenetTemplatePage.java +++ b/src/main/java/net/pterodactylus/sone/web/page/FreenetTemplatePage.java @@ -17,6 +17,8 @@ package net.pterodactylus.sone.web.page; +import static java.util.logging.Logger.getLogger; + import java.io.IOException; import java.io.StringWriter; import java.net.URI; @@ -28,7 +30,6 @@ import java.util.Map.Entry; import java.util.logging.Level; import java.util.logging.Logger; -import net.pterodactylus.util.logging.Logging; import net.pterodactylus.util.template.Template; import net.pterodactylus.util.template.TemplateContext; import net.pterodactylus.util.template.TemplateContextFactory; @@ -51,7 +52,7 @@ import freenet.support.HTMLNode; public class FreenetTemplatePage implements FreenetPage, LinkEnabledCallback { /** The logger. */ - private static final Logger logger = Logging.getLogger(FreenetTemplatePage.class); + private static final Logger logger = getLogger("Sone.Web.Freenet"); /** The path of the page. */ private final String path; @@ -156,7 +157,7 @@ public class FreenetTemplatePage implements FreenetPage, LinkEnabledCallback { long start = System.nanoTime(); processTemplate(request, templateContext); long finish = System.nanoTime(); - logger.log(Level.FINEST, String.format("Template was rendered in %.2fms.", ((finish - start) / 1000) / 1000.0)); + logger.log(Level.FINEST, String.format("Template was rendered in %.2fms.", (finish - start) / 1000000.0)); } catch (RedirectException re1) { return new RedirectResponse(re1.getTarget()); } diff --git a/src/main/java/net/pterodactylus/sone/web/page/PageToadlet.java b/src/main/java/net/pterodactylus/sone/web/page/PageToadlet.java index a40a543..fc58339 100644 --- a/src/main/java/net/pterodactylus/sone/web/page/PageToadlet.java +++ b/src/main/java/net/pterodactylus/sone/web/page/PageToadlet.java @@ -193,7 +193,7 @@ public class PageToadlet extends Toadlet implements LinkEnabledCallback, LinkFil */ @Override public boolean isLinkExcepted(URI link) { - return (page instanceof FreenetPage) ? ((FreenetPage) page).isLinkExcepted(link) : false; + return (page instanceof FreenetPage) && ((FreenetPage) page).isLinkExcepted(link); } } diff --git a/src/main/resources/i18n/sone.de.properties b/src/main/resources/i18n/sone.de.properties index 1d53d8a..6e871f9 100644 --- a/src/main/resources/i18n/sone.de.properties +++ b/src/main/resources/i18n/sone.de.properties @@ -57,7 +57,7 @@ Page.Options.Option.InsertionDelay.Description=Anzahl der Sekunden, die vor dem Page.Options.Option.PostsPerPage.Description=Anzahl der Nachrichten pro Seite. Page.Options.Option.ImagesPerPage.Description=Anzahl der Bilder pro Seite. Page.Options.Option.CharactersPerPost.Description=Die Anzahl der Zeichen, die eine Nachricht enthalten muss, damit sie gekürzt angezeigt wird (-1 für „nie kürzen“). Die Anzahl der tatsächlich angezeigten Zeichen wird in der nächsten Option konfiguriert. -Page.Options.Option.PostCutOffLength.Description=Die Anzahl der Zeichen, die von einer gekürzten Nachricht sichtbar sind (siehe Option hierüber). +Page.Options.Option.PostCutOffLength.Description=Die Anzahl der Zeichen, die von einer gekürzten Nachricht sichtbar sind (siehe Option hierüber). Wird ignoriert, wenn die Option hierüber deaktiviert ist, bzw. auf -1 steht. Page.Options.Option.RequireFullAccess.Description=Zugriff auf Sone für alle Rechner, die keinen vollen Zugriff haben, unterbinden. Page.Options.Section.TrustOptions.Title=Vertrauenseinstellungen Page.Options.Option.PositiveTrust.Description=Die Menge an positivem Vertrauen, die bei einem Klick auf den Haken unter einer Nachricht zugewiesen werden soll. @@ -305,6 +305,14 @@ Page.NoPermission.Title=Unberechtigter Zugriff - Sone Page.NoPermission.Page.Title=Unberechtigter Zugriff Page.NoPermission.Text.NoPermission=Sie haben versucht, etwas zu tun, zu dem Sie nicht berechtigt sind. Bitte unterlassen Sie das, da wir sonst gezwungen sind, Gegenmaßnahmen zu ergreifen! +Page.EmptyImageTitle.Title=Bildtitel muss gesetzt sein - Sone +Page.EmptyImageTitle.Page.Title=Bildtitel muss gesetzt sein +Page.EmptyImageTitle.Text.EmptyImageTitle=Das Bild muss einen Titel haben. Bitte gehen Sie zur vorherigen Seite zurück und geben Sie einen Titel ein. + +Page.EmptyAlbumTitle.Title=Albumtitel muss gesetzt sein - Sone +Page.EmptyAlbumTitle.Page.Title=Albumtitel muss gesetzt sein +Page.EmptyAlbumTitle.Text.EmptyAlbumTitle=Das Album muss einen Titel haben. Bitte gehen Sie zur vorherigen Seite zurück und geben Sie einen Titel ein. + Page.DismissNotification.Title=Benachrichtigung ausblenden - Sone Page.WotPluginMissing.Text.WotRequired=Da das „Web of Trust“ ein integraler Bestandteil von Sone ist, muss das „Web of Trust“ Plugin geladen sein, damit Sone funktionieren kann. diff --git a/src/main/resources/i18n/sone.en.properties b/src/main/resources/i18n/sone.en.properties index a7b9ab4..40a12b1 100644 --- a/src/main/resources/i18n/sone.en.properties +++ b/src/main/resources/i18n/sone.en.properties @@ -57,7 +57,7 @@ Page.Options.Option.InsertionDelay.Description=The number of seconds the Sone in Page.Options.Option.PostsPerPage.Description=The number of posts to display on a page before pagination controls are being shown. Page.Options.Option.ImagesPerPage.Description=The number of images to display on a page before pagination controls are being shown. Page.Options.Option.CharactersPerPost.Description=The number of characters to display from a post before cutting it off and showing a link to expand it (-1 to disable). The actual length of the snippet is determined by the option below. -Page.Options.Option.PostCutOffLength.Description=The number of characters that are displayed if a post is deemed to long (see option above). +Page.Options.Option.PostCutOffLength.Description=The number of characters that are displayed if a post is deemed too long (see option above). Ignored if “number of characters to display” is disabled (set to -1). Page.Options.Option.RequireFullAccess.Description=Whether to deny access to Sone to any host that has not been granted full access. Page.Options.Section.TrustOptions.Title=Trust Settings Page.Options.Option.PositiveTrust.Description=The amount of positive trust you want to assign to other Sones by clicking the checkmark below a post or reply. @@ -305,6 +305,14 @@ Page.NoPermission.Title=Unauthorized Access - Sone Page.NoPermission.Page.Title=Unauthorized Access Page.NoPermission.Text.NoPermission=You tried to do something that you do not have sufficient authorization for. Please refrain from such actions in the future or we will be forced to take counter-measures! +Page.EmptyImageTitle.Title=Title Must Not Be Empty - Sone +Page.EmptyImageTitle.Page.Title=Title Must Not Be Empty +Page.EmptyImageTitle.Text.EmptyImageTitle=You have to give your image a title. Please go back to the previous page and enter a title. + +Page.EmptyAlbumTitle.Title=Title Must Not Be Empty - Sone +Page.EmptyAlbumTitle.Page.Title=Title Must Not Be Empty +Page.EmptyAlbumTitle.Text.EmptyAlbumTitle=You have to give your album a title. Please go back to the previous page and enter a title. + Page.DismissNotification.Title=Dismiss Notification - Sone Page.WotPluginMissing.Text.WotRequired=Because the Web of Trust is an integral part of Sone, the Web of Trust plugin has to be loaded in order to run Sone. diff --git a/src/main/resources/i18n/sone.fr.properties b/src/main/resources/i18n/sone.fr.properties index d6dd410..ac8fa69 100644 --- a/src/main/resources/i18n/sone.fr.properties +++ b/src/main/resources/i18n/sone.fr.properties @@ -305,6 +305,14 @@ Page.NoPermission.Title=Accès non autorisé - Sone Page.NoPermission.Page.Title=Accès non autorisé Page.NoPermission.Text.NoPermission=Vous avez tenté une action pour laquelle vous n'avez pas les droits suffisants. Veuillez vous abstenir de ces actions dans le futur ou nous serons forcés de prendre des contre-mesures! +Page.EmptyImageTitle.Title=Title Must Not Be Empty - Sone +Page.EmptyImageTitle.Page.Title=Title Must Not Be Empty +Page.EmptyImageTitle.Text.EmptyImageTitle=You have to give your image a title. Please go back to the previous page and enter a title. + +Page.EmptyAlbumTitle.Title=Title Must Not Be Empty - Sone +Page.EmptyAlbumTitle.Page.Title=Title Must Not Be Empty +Page.EmptyAlbumTitle.Text.EmptyAlbumTitle=You have to give your album a title. Please go back to the previous page and enter a title. + Page.DismissNotification.Title=Effacer la notification - Sone Page.WotPluginMissing.Text.LoadPlugin=Veuillez charger le plugin Web of Trust dans le {link}plugin manager{/link}. @@ -455,4 +463,4 @@ Notification.Mention.Text=Vous avez été mentionné dans les messages suivants: Notification.SoneIsInserting.Text=Your Sone sone://{0} is now being inserted. Notification.SoneIsInserted.Text=Your Sone sone://{0} has been inserted in {1,number} {1,choice,0#seconds|1#second|1 + +

<%= Page.EmptyAlbumTitle.Page.Title|l10n|html>

+ +

<%= Page.EmptyAlbumTitle.Text.EmptyAlbumTitle|l10n|html>

+ +<%include include/tail.html> diff --git a/src/main/resources/templates/emptyImageTitle.html b/src/main/resources/templates/emptyImageTitle.html new file mode 100644 index 0000000..59f15e5 --- /dev/null +++ b/src/main/resources/templates/emptyImageTitle.html @@ -0,0 +1,7 @@ +<%include include/head.html> + +

<%= Page.EmptyImageTitle.Page.Title|l10n|html>

+ +

<%= Page.EmptyImageTitle.Text.EmptyImageTitle|l10n|html>

+ +<%include include/tail.html> diff --git a/src/main/resources/templates/imageBrowser.html b/src/main/resources/templates/imageBrowser.html index da8d032..7dd4c29 100644 --- a/src/main/resources/templates/imageBrowser.html +++ b/src/main/resources/templates/imageBrowser.html @@ -116,13 +116,20 @@ title = $(":input[name='title']:enabled", this.form).val(); description = $(":input[name='description']:enabled", this.form).val(); ajaxGet("editImage.ajax", { "formPassword": getFormPassword(), "image": imageId, "title": title, "description": description }, function(data) { - if (data && data.success) { - getImage(data.imageId).find(".image-title").text(data.title); - getImage(data.imageId).find(".image-description").html(data.parsedDescription); - getImage(data.imageId).find(":input[name='title']").attr("defaultValue", title); - getImage(data.imageId).find(":input[name='description']").attr("defaultValue", data.description); + var imageElement = getImage(data.imageId); + var imageTitleInput = imageElement.find(":input[name='title']"); + var imageDescriptionInput = imageElement.find(":input[name='description']"); + if (data && data.success) { + imageElement.find(".image-title").text(data.title); + imageElement.find(".image-description").html(data.parsedDescription); + imageTitleInput.attr("defaultValue", data.title); + imageDescriptionInput.attr("defaultValue", data.description); cancelImageEditing(); - } + } else if (data && !data.success) { + imageTitleInput.attr("value", imageTitleInput.attr("defaultValue")); + imageDescriptionInput.attr("value", imageDescriptionInput.attr("defaultValue")); + cancelImageEditing(); + } }); return false; }); @@ -232,13 +239,20 @@ title = $(":input[name='title']:enabled", this.form).val(); description = $(":input[name='description']:enabled", this.form).val(); ajaxGet("editAlbum.ajax", { "formPassword": getFormPassword(), "album": albumId, "title": title, "description": description }, function(data) { - if (data && data.success) { - getAlbum(data.albumId).find(".album-title").text(data.title); - getAlbum(data.albumId).find(".album-description").text(data.description); - getAlbum(data.albumId).find(":input[name='title']").attr("defaultValue", title); - getAlbum(data.albumId).find(":input[name='description']").attr("defaultValue", description); - cancelAlbumEditing(); - } + if (data) { + var albumTitleField = getAlbum(data.albumId).find(".album-title"); + var albumDescriptionField = getAlbum(data.albumId).find(".album-description"); + if (data.success) { + albumTitleField.text(data.title); + albumDescriptionField.text(data.description); + getAlbum(data.albumId).find(":input[name='title']").attr("defaultValue", title); + getAlbum(data.albumId).find(":input[name='description']").attr("defaultValue", description); + } else { + albumTitleField.attr("value", albumTitleField.attr("defaultValue")); + albumDescriptionField.attr("value", albumDescriptionField.attr("defaultValue")); + } + cancelAlbumEditing(); + } }); return false; }); diff --git a/src/main/resources/templates/include/viewSone.html b/src/main/resources/templates/include/viewSone.html index 1ca0023..60b8ff7 100644 --- a/src/main/resources/templates/include/viewSone.html +++ b/src/main/resources/templates/include/viewSone.html @@ -10,7 +10,7 @@
(<%= View.Sone.Stats.Posts|l10n 0=sone.posts.size>, <%= View.Sone.Stats.Replies|l10n 0=sone.replies.size><%if ! sone.allImages.size|match value==0>, <%= View.Sone.Stats.Images|l10n 0=sone.allImages.size><%/if>)
-
<% sone.requestUri|substring start==4 length==43|html>
+
<% sone.id|html>
<%if sone.local>
diff --git a/src/test/java/net/pterodactylus/sone/Matchers.java b/src/test/java/net/pterodactylus/sone/Matchers.java new file mode 100644 index 0000000..c16322c --- /dev/null +++ b/src/test/java/net/pterodactylus/sone/Matchers.java @@ -0,0 +1,396 @@ +/* + * Sone - Matchers.java - Copyright © 2013 David Roden + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.pterodactylus.sone; + +import static java.util.regex.Pattern.compile; + +import java.io.IOException; +import java.io.InputStream; + +import net.pterodactylus.sone.data.Album; +import net.pterodactylus.sone.data.Image; +import net.pterodactylus.sone.data.Post; +import net.pterodactylus.sone.data.PostReply; + +import com.google.common.base.Optional; +import org.hamcrest.Description; +import org.hamcrest.Matcher; +import org.hamcrest.TypeSafeDiagnosingMatcher; +import org.hamcrest.TypeSafeMatcher; + +/** + * Matchers used throughout the tests. + * + * @author David ‘Bombe’ Roden + */ +public class Matchers { + + public static Matcher matchesRegex(final String regex) { + return new TypeSafeMatcher() { + @Override + protected boolean matchesSafely(String item) { + return compile(regex).matcher(item).matches(); + } + + @Override + public void describeTo(Description description) { + description.appendText("matches: ").appendValue(regex); + } + }; + } + + public static Matcher delivers(final byte[] data) { + return new TypeSafeMatcher() { + byte[] readData = new byte[data.length]; + + @Override + protected boolean matchesSafely(InputStream inputStream) { + int offset = 0; + try { + while (true) { + int r = inputStream.read(); + if (r == -1) { + return offset == data.length; + } + if (offset == data.length) { + return false; + } + if (data[offset] != (readData[offset] = (byte) r)) { + return false; + } + offset++; + } + } catch (IOException ioe1) { + return false; + } + } + + @Override + public void describeTo(Description description) { + description.appendValue(data); + } + + @Override + protected void describeMismatchSafely(InputStream item, + Description mismatchDescription) { + mismatchDescription.appendValue(readData); + } + }; + } + + public static Matcher isPost(String postId, long time, + String text, Optional recipient) { + return new PostMatcher(postId, time, text, recipient); + } + + public static Matcher isPostWithId(String postId) { + return new PostIdMatcher(postId); + } + + public static Matcher isPostReply(String postReplyId, + String postId, long time, String text) { + return new PostReplyMatcher(postReplyId, postId, time, text); + } + + public static Matcher isAlbum(final String albumId, + final String parentAlbumId, + final String title, final String albumDescription, + final String imageId) { + return new TypeSafeDiagnosingMatcher() { + @Override + protected boolean matchesSafely(Album album, + Description mismatchDescription) { + if (!album.getId().equals(albumId)) { + mismatchDescription.appendText("ID is ") + .appendValue(album.getId()); + return false; + } + if (parentAlbumId == null) { + if (album.getParent() != null) { + mismatchDescription.appendText("has parent album"); + return false; + } + } else { + if (album.getParent() == null) { + mismatchDescription.appendText("has no parent album"); + return false; + } + if (!album.getParent().getId().equals(parentAlbumId)) { + mismatchDescription.appendText("parent album is ") + .appendValue(album.getParent().getId()); + return false; + } + } + if (!title.equals(album.getTitle())) { + mismatchDescription.appendText("has title ") + .appendValue(album.getTitle()); + return false; + } + if (!albumDescription.equals(album.getDescription())) { + mismatchDescription.appendText("has description ") + .appendValue(album.getDescription()); + return false; + } + if (imageId == null) { + if (album.getAlbumImage() != null) { + mismatchDescription.appendText("has album image"); + return false; + } + } else { + if (album.getAlbumImage() == null) { + mismatchDescription.appendText("has no album image"); + return false; + } + if (!album.getAlbumImage().getId().equals(imageId)) { + mismatchDescription.appendText("has album image ") + .appendValue(album.getAlbumImage().getId()); + return false; + } + } + return true; + } + + @Override + public void describeTo(Description description) { + description.appendText("is album ").appendValue(albumId); + if (parentAlbumId == null) { + description.appendText(", has no parent"); + } else { + description.appendText(", has parent ") + .appendValue(parentAlbumId); + } + description.appendText(", has title ").appendValue(title); + description.appendText(", has description ") + .appendValue(albumDescription); + if (imageId == null) { + description.appendText(", has no album image"); + } else { + description.appendText(", has album image ") + .appendValue(imageId); + } + } + }; + } + + public static Matcher isImage(final String id, + final long creationTime, + final String key, final String title, + final String imageDescription, + final int width, final int height) { + return new TypeSafeDiagnosingMatcher() { + @Override + protected boolean matchesSafely(Image image, + Description mismatchDescription) { + if (!image.getId().equals(id)) { + mismatchDescription.appendText("ID is ") + .appendValue(image.getId()); + return false; + } + if (image.getCreationTime() != creationTime) { + mismatchDescription.appendText("created at @") + .appendValue(image.getCreationTime()); + return false; + } + if (!image.getKey().equals(key)) { + mismatchDescription.appendText("key is ") + .appendValue(image.getKey()); + return false; + } + if (!image.getTitle().equals(title)) { + mismatchDescription.appendText("title is ") + .appendValue(image.getTitle()); + return false; + } + if (!image.getDescription().equals(imageDescription)) { + mismatchDescription.appendText("description is ") + .appendValue(image.getDescription()); + return false; + } + if (image.getWidth() != width) { + mismatchDescription.appendText("width is ") + .appendValue(image.getWidth()); + return false; + } + if (image.getHeight() != height) { + mismatchDescription.appendText("height is ") + .appendValue(image.getHeight()); + return false; + } + return true; + } + + @Override + public void describeTo(Description description) { + description.appendText("image with ID ").appendValue(id); + description.appendText(", created at @") + .appendValue(creationTime); + description.appendText(", has key ").appendValue(key); + description.appendText(", has title ").appendValue(title); + description.appendText(", has description ") + .appendValue(imageDescription); + description.appendText(", has width ").appendValue(width); + description.appendText(", has height ").appendValue(height); + } + }; + } + + private static class PostMatcher extends TypeSafeDiagnosingMatcher { + + private final String postId; + private final long time; + private final String text; + private final Optional recipient; + + private PostMatcher(String postId, long time, String text, + Optional recipient) { + this.postId = postId; + this.time = time; + this.text = text; + this.recipient = recipient; + } + + @Override + protected boolean matchesSafely(Post post, + Description mismatchDescription) { + if (!post.getId().equals(postId)) { + mismatchDescription.appendText("ID is not ") + .appendValue(postId); + return false; + } + if (post.getTime() != time) { + mismatchDescription.appendText("Time is not @") + .appendValue(time); + return false; + } + if (!post.getText().equals(text)) { + mismatchDescription.appendText("Text is not ") + .appendValue(text); + return false; + } + if (recipient.isPresent()) { + if (!post.getRecipientId().isPresent()) { + mismatchDescription.appendText( + "Recipient not present"); + return false; + } + if (!post.getRecipientId().get().equals(recipient.get())) { + mismatchDescription.appendText("Recipient is not ") + .appendValue(recipient.get()); + return false; + } + } else { + if (post.getRecipientId().isPresent()) { + mismatchDescription.appendText("Recipient is present"); + return false; + } + } + return true; + } + + @Override + public void describeTo(Description description) { + description.appendText("is post with ID ") + .appendValue(postId); + description.appendText(", created at @").appendValue(time); + description.appendText(", text ").appendValue(text); + if (recipient.isPresent()) { + description.appendText(", directed at ") + .appendValue(recipient.get()); + } + } + + } + + private static class PostIdMatcher extends TypeSafeDiagnosingMatcher { + + private final String id; + + private PostIdMatcher(String id) { + this.id = id; + } + + @Override + protected boolean matchesSafely(Post item, + Description mismatchDescription) { + if (!item.getId().equals(id)) { + mismatchDescription.appendText("post has ID ").appendValue(item.getId()); + return false; + } + return true; + } + + @Override + public void describeTo(Description description) { + description.appendText("post with ID ").appendValue(id); + } + + } + + private static class PostReplyMatcher + extends TypeSafeDiagnosingMatcher { + + private final String postReplyId; + private final String postId; + private final long time; + private final String text; + + private PostReplyMatcher(String postReplyId, String postId, long time, + String text) { + this.postReplyId = postReplyId; + this.postId = postId; + this.time = time; + this.text = text; + } + + @Override + protected boolean matchesSafely(PostReply postReply, + Description mismatchDescription) { + if (!postReply.getId().equals(postReplyId)) { + mismatchDescription.appendText("is post reply ") + .appendValue(postReply.getId()); + return false; + } + if (!postReply.getPostId().equals(postId)) { + mismatchDescription.appendText("is reply to ") + .appendValue(postReply.getPostId()); + return false; + } + if (postReply.getTime() != time) { + mismatchDescription.appendText("is created at @").appendValue( + postReply.getTime()); + return false; + } + if (!postReply.getText().equals(text)) { + mismatchDescription.appendText("says ") + .appendValue(postReply.getText()); + return false; + } + return true; + } + + @Override + public void describeTo(Description description) { + description.appendText("is post reply ").appendValue(postReplyId); + description.appendText(", replies to post ").appendValue(postId); + description.appendText(", is created at @").appendValue(time); + description.appendText(", says ").appendValue(text); + } + + } + +} diff --git a/src/test/java/net/pterodactylus/sone/TestAlbumBuilder.java b/src/test/java/net/pterodactylus/sone/TestAlbumBuilder.java new file mode 100644 index 0000000..9890a70 --- /dev/null +++ b/src/test/java/net/pterodactylus/sone/TestAlbumBuilder.java @@ -0,0 +1,139 @@ +package net.pterodactylus.sone; + +import static java.util.UUID.randomUUID; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.List; + +import net.pterodactylus.sone.data.Album; +import net.pterodactylus.sone.data.Album.Modifier; +import net.pterodactylus.sone.data.Image; +import net.pterodactylus.sone.data.Sone; +import net.pterodactylus.sone.database.AlbumBuilder; + +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +/** + * {@link AlbumBuilder} that returns a mocked {@link Album}. + * + * @author David ‘Bombe’ Roden + */ +public class TestAlbumBuilder implements AlbumBuilder { + + private final Album album = mock(Album.class); + private final List albums = new ArrayList(); + private final List images = new ArrayList(); + private Album parentAlbum; + private String title; + private String description; + private String imageId; + + public TestAlbumBuilder() { + when(album.getTitle()).thenAnswer(new Answer() { + @Override + public String answer(InvocationOnMock invocation) { + return title; + } + }); + when(album.getDescription()).thenAnswer(new Answer() { + @Override + public String answer(InvocationOnMock invocation) { + return description; + } + }); + when(album.getAlbumImage()).thenAnswer(new Answer() { + @Override + public Image answer(InvocationOnMock invocation) { + if (imageId == null) { + return null; + } + Image image = mock(Image.class); + when(image.getId()).thenReturn(imageId); + return image; + } + }); + when(album.getAlbums()).thenReturn(albums); + when(album.getImages()).thenReturn(images); + doAnswer(new Answer() { + @Override + public Void answer(InvocationOnMock invocation) { + albums.add((Album) invocation.getArguments()[0]); + ((Album) invocation.getArguments()[0]).setParent(album); + return null; + } + }).when(album).addAlbum(any(Album.class)); + doAnswer(new Answer() { + @Override + public Void answer(InvocationOnMock invocation) { + images.add((Image) invocation.getArguments()[0]); + return null; + } + }).when(album).addImage(any(Image.class)); + doAnswer(new Answer() { + @Override + public Void answer(InvocationOnMock invocation) { + parentAlbum = (Album) invocation.getArguments()[0]; + return null; + } + }).when(album).setParent(any(Album.class)); + when(album.getParent()).thenAnswer(new Answer() { + @Override + public Album answer(InvocationOnMock invocation) { + return parentAlbum; + } + }); + when(album.modify()).thenReturn(new Modifier() { + @Override + public Modifier setTitle(String title) { + TestAlbumBuilder.this.title = title; + return this; + } + + @Override + public Modifier setDescription(String description) { + TestAlbumBuilder.this.description = description; + return this; + } + + @Override + public Modifier setAlbumImage(String imageId) { + TestAlbumBuilder.this.imageId = imageId; + return this; + } + + @Override + public Album update() throws IllegalStateException { + return album; + } + }); + } + + @Override + public AlbumBuilder randomId() { + when(album.getId()).thenReturn(randomUUID().toString()); + return this; + } + + @Override + public AlbumBuilder withId(String id) { + when(album.getId()).thenReturn(id); + return this; + } + + @Override + public AlbumBuilder by(Sone sone) { + when(album.getSone()).thenReturn(sone); + return this; + } + + @Override + public Album build() throws IllegalStateException { + return album; + } + +} diff --git a/src/test/java/net/pterodactylus/sone/TestImageBuilder.java b/src/test/java/net/pterodactylus/sone/TestImageBuilder.java new file mode 100644 index 0000000..48a8fad --- /dev/null +++ b/src/test/java/net/pterodactylus/sone/TestImageBuilder.java @@ -0,0 +1,105 @@ +package net.pterodactylus.sone; + +import static java.util.UUID.randomUUID; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import net.pterodactylus.sone.data.Image; +import net.pterodactylus.sone.data.Sone; +import net.pterodactylus.sone.database.ImageBuilder; + +/** + * {@link ImageBuilder} implementation that returns a mocked {@link Image}. + * + * @author David ‘Bombe’ Roden + */ +public class TestImageBuilder implements ImageBuilder { + + private final Image image; + + public TestImageBuilder() { + image = mock(Image.class); + Image.Modifier imageModifier = new Image.Modifier() { + private Sone sone = image.getSone(); + private long creationTime = image.getCreationTime(); + private String key = image.getKey(); + private String title = image.getTitle(); + private String description = image.getDescription(); + private int width = image.getWidth(); + private int height = image.getHeight(); + + @Override + public Image.Modifier setSone(Sone sone) { + this.sone = sone; + return this; + } + + @Override + public Image.Modifier setCreationTime(long creationTime) { + this.creationTime = creationTime; + return this; + } + + @Override + public Image.Modifier setKey(String key) { + this.key = key; + return this; + } + + @Override + public Image.Modifier setTitle(String title) { + this.title = title; + return this; + } + + @Override + public Image.Modifier setDescription(String description) { + this.description = description; + return this; + } + + @Override + public Image.Modifier setWidth(int width) { + this.width = width; + return this; + } + + @Override + public Image.Modifier setHeight(int height) { + this.height = height; + return this; + } + + @Override + public Image update() throws IllegalStateException { + when(image.getSone()).thenReturn(sone); + when(image.getCreationTime()).thenReturn(creationTime); + when(image.getKey()).thenReturn(key); + when(image.getTitle()).thenReturn(title); + when(image.getDescription()).thenReturn(description); + when(image.getWidth()).thenReturn(width); + when(image.getHeight()).thenReturn(height); + return image; + } + }; + when(image.modify()).thenReturn(imageModifier); + } + + @Override + public ImageBuilder randomId() { + when(image.getId()).thenReturn(randomUUID().toString()); + return this; + } + + @Override + public ImageBuilder withId(String id) { + when(image.getId()).thenReturn(id); + return this; + } + + @Override + public Image build() throws IllegalStateException { + return image; + } + +} diff --git a/src/test/java/net/pterodactylus/sone/TestPostBuilder.java b/src/test/java/net/pterodactylus/sone/TestPostBuilder.java new file mode 100644 index 0000000..29c4c40 --- /dev/null +++ b/src/test/java/net/pterodactylus/sone/TestPostBuilder.java @@ -0,0 +1,78 @@ +package net.pterodactylus.sone; + +import static com.google.common.base.Optional.fromNullable; +import static java.lang.System.currentTimeMillis; +import static java.util.UUID.randomUUID; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import net.pterodactylus.sone.data.Post; +import net.pterodactylus.sone.data.Sone; +import net.pterodactylus.sone.database.PostBuilder; + +/** + * {@link PostBuilder} implementation that returns a mocked {@link Post}. + * + * @author David ‘Bombe’ Roden + */ +public class TestPostBuilder implements PostBuilder { + + private final Post post = mock(Post.class); + private String recipientId = null; + + @Override + public PostBuilder copyPost(Post post) throws NullPointerException { + return this; + } + + @Override + public PostBuilder from(String senderId) { + final Sone sone = mock(Sone.class); + when(sone.getId()).thenReturn(senderId); + when(post.getSone()).thenReturn(sone); + return this; + } + + @Override + public PostBuilder randomId() { + when(post.getId()).thenReturn(randomUUID().toString()); + return this; + } + + @Override + public PostBuilder withId(String id) { + when(post.getId()).thenReturn(id); + return this; + } + + @Override + public PostBuilder currentTime() { + when(post.getTime()).thenReturn(currentTimeMillis()); + return this; + } + + @Override + public PostBuilder withTime(long time) { + when(post.getTime()).thenReturn(time); + return this; + } + + @Override + public PostBuilder withText(String text) { + when(post.getText()).thenReturn(text); + return this; + } + + @Override + public PostBuilder to(String recipientId) { + this.recipientId = recipientId; + return this; + } + + @Override + public Post build() throws IllegalStateException { + when(post.getRecipientId()).thenReturn(fromNullable(recipientId)); + return post; + } + +} diff --git a/src/test/java/net/pterodactylus/sone/TestPostReplyBuilder.java b/src/test/java/net/pterodactylus/sone/TestPostReplyBuilder.java new file mode 100644 index 0000000..6b929c7 --- /dev/null +++ b/src/test/java/net/pterodactylus/sone/TestPostReplyBuilder.java @@ -0,0 +1,70 @@ +package net.pterodactylus.sone; + +import static java.lang.System.currentTimeMillis; +import static java.util.UUID.randomUUID; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import net.pterodactylus.sone.data.PostReply; +import net.pterodactylus.sone.data.Sone; +import net.pterodactylus.sone.database.PostReplyBuilder; + +/** + * {@link PostReplyBuilder} that returns a mocked {@link PostReply}. + * + * @author David ‘Bombe’ Roden + */ +public class TestPostReplyBuilder implements PostReplyBuilder { + + private final PostReply postReply = mock(PostReply.class); + + @Override + public PostReplyBuilder to(String postId) { + when(postReply.getPostId()).thenReturn(postId); + return this; + } + + @Override + public PostReply build() throws IllegalStateException { + return postReply; + } + + @Override + public PostReplyBuilder randomId() { + when(postReply.getId()).thenReturn(randomUUID().toString()); + return this; + } + + @Override + public PostReplyBuilder withId(String id) { + when(postReply.getId()).thenReturn(id); + return this; + } + + @Override + public PostReplyBuilder from(String senderId) { + Sone sone = mock(Sone.class); + when(sone.getId()).thenReturn(senderId); + when(postReply.getSone()).thenReturn(sone); + return this; + } + + @Override + public PostReplyBuilder currentTime() { + when(postReply.getTime()).thenReturn(currentTimeMillis()); + return this; + } + + @Override + public PostReplyBuilder withTime(long time) { + when(postReply.getTime()).thenReturn(time); + return this; + } + + @Override + public PostReplyBuilder withText(String text) { + when(postReply.getText()).thenReturn(text); + return this; + } + +} diff --git a/src/test/java/net/pterodactylus/sone/TestUtil.java b/src/test/java/net/pterodactylus/sone/TestUtil.java new file mode 100644 index 0000000..fa7879d --- /dev/null +++ b/src/test/java/net/pterodactylus/sone/TestUtil.java @@ -0,0 +1,56 @@ +package net.pterodactylus.sone; + +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; + +/** + * Utilities for testing. + * + * @author David ‘Bombe’ Roden + */ +public class TestUtil { + + public static void setFinalField(Object object, String fieldName, Object value) { + try { + Field clientCoreField = object.getClass().getField(fieldName); + clientCoreField.setAccessible(true); + Field modifiersField = Field.class.getDeclaredField("modifiers"); + modifiersField.setAccessible(true); + modifiersField.setInt(clientCoreField, clientCoreField.getModifiers() & ~Modifier.FINAL); + clientCoreField.set(object, value); + } catch (NoSuchFieldException e) { + throw new RuntimeException(e); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + + public static T getPrivateField(Object object, String fieldName) { + try { + Field field = object.getClass().getDeclaredField(fieldName); + field.setAccessible(true); + return (T) field.get(object); + } catch (NoSuchFieldException e) { + throw new RuntimeException(e); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + + public static T callPrivateMethod(Object object, String methodName) { + try { + Method method = object.getClass().getDeclaredMethod(methodName, new Class[0]); + method.setAccessible(true); + return (T) method.invoke(object); + } catch (NoSuchMethodException e) { + throw new RuntimeException(e); + } catch (InvocationTargetException e) { + throw new RuntimeException(e); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + +} diff --git a/src/test/java/net/pterodactylus/sone/TestValue.java b/src/test/java/net/pterodactylus/sone/TestValue.java new file mode 100644 index 0000000..43cf0a6 --- /dev/null +++ b/src/test/java/net/pterodactylus/sone/TestValue.java @@ -0,0 +1,59 @@ +package net.pterodactylus.sone; + +import java.util.concurrent.atomic.AtomicReference; + +import net.pterodactylus.util.config.ConfigurationException; +import net.pterodactylus.util.config.Value; + +import com.google.common.base.Objects; + +/** + * Simple {@link Value} implementation. + * + * @author David ‘Bombe’ Roden + */ +public class TestValue implements Value { + + private final AtomicReference value = new AtomicReference(); + + public TestValue(T originalValue) { + value.set(originalValue); + } + + @Override + public T getValue() throws ConfigurationException { + return value.get(); + } + + @Override + public T getValue(T defaultValue) { + final T realValue = value.get(); + return (realValue != null) ? realValue : defaultValue; + } + + @Override + public void setValue(T newValue) throws ConfigurationException { + value.set(newValue); + } + + @Override + public int hashCode() { + return value.hashCode(); + } + + @Override + public boolean equals(Object obj) { + return (obj instanceof TestValue) && Objects.equal(value.get(), + ((TestValue) obj).value.get()); + } + + @Override + public String toString() { + return String.valueOf(value.get()); + } + + public static Value from(T value) { + return new TestValue(value); + } + +} diff --git a/src/test/java/net/pterodactylus/sone/core/ConfigurationSoneParserTest.java b/src/test/java/net/pterodactylus/sone/core/ConfigurationSoneParserTest.java new file mode 100644 index 0000000..7bbfae8 --- /dev/null +++ b/src/test/java/net/pterodactylus/sone/core/ConfigurationSoneParserTest.java @@ -0,0 +1,524 @@ +package net.pterodactylus.sone.core; + +import static com.google.common.base.Optional.of; +import static net.pterodactylus.sone.Matchers.isAlbum; +import static net.pterodactylus.sone.Matchers.isImage; +import static net.pterodactylus.sone.Matchers.isPost; +import static net.pterodactylus.sone.Matchers.isPostReply; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.emptyIterable; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; +import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import net.pterodactylus.sone.TestAlbumBuilder; +import net.pterodactylus.sone.TestImageBuilder; +import net.pterodactylus.sone.TestPostBuilder; +import net.pterodactylus.sone.TestPostReplyBuilder; +import net.pterodactylus.sone.TestValue; +import net.pterodactylus.sone.core.ConfigurationSoneParser.InvalidAlbumFound; +import net.pterodactylus.sone.core.ConfigurationSoneParser.InvalidImageFound; +import net.pterodactylus.sone.core.ConfigurationSoneParser.InvalidParentAlbumFound; +import net.pterodactylus.sone.core.ConfigurationSoneParser.InvalidPostFound; +import net.pterodactylus.sone.core.ConfigurationSoneParser.InvalidPostReplyFound; +import net.pterodactylus.sone.data.Album; +import net.pterodactylus.sone.data.Post; +import net.pterodactylus.sone.data.PostReply; +import net.pterodactylus.sone.data.Profile; +import net.pterodactylus.sone.data.Profile.Field; +import net.pterodactylus.sone.data.Sone; +import net.pterodactylus.sone.database.AlbumBuilder; +import net.pterodactylus.sone.database.AlbumBuilderFactory; +import net.pterodactylus.sone.database.ImageBuilder; +import net.pterodactylus.sone.database.ImageBuilderFactory; +import net.pterodactylus.sone.database.PostBuilder; +import net.pterodactylus.sone.database.PostBuilderFactory; +import net.pterodactylus.sone.database.PostReplyBuilder; +import net.pterodactylus.sone.database.PostReplyBuilderFactory; +import net.pterodactylus.util.config.Configuration; + +import com.google.common.base.Optional; +import org.hamcrest.Matchers; +import org.junit.Test; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +/** + * Unit test for {@link ConfigurationSoneParser}. + * + * @author David ‘Bombe’ Roden + */ +public class ConfigurationSoneParserTest { + + private final Configuration configuration = mock(Configuration.class); + private final Sone sone = mock(Sone.class); + private final ConfigurationSoneParser configurationSoneParser; + + public ConfigurationSoneParserTest() { + when(sone.getId()).thenReturn("1"); + configurationSoneParser = + new ConfigurationSoneParser(configuration, sone); + } + + @Test + public void emptyProfileIsLoadedCorrectly() { + setupEmptyProfile(); + Profile profile = configurationSoneParser.parseProfile(); + assertThat(profile, notNullValue()); + assertThat(profile.getFirstName(), nullValue()); + assertThat(profile.getMiddleName(), nullValue()); + assertThat(profile.getLastName(), nullValue()); + assertThat(profile.getBirthDay(), nullValue()); + assertThat(profile.getBirthMonth(), nullValue()); + assertThat(profile.getBirthYear(), nullValue()); + assertThat(profile.getFields(), emptyIterable()); + } + + private void setupEmptyProfile() { + when(configuration.getStringValue(anyString())).thenReturn( + TestValue.from(null)); + when(configuration.getIntValue(anyString())).thenReturn( + TestValue.from(null)); + } + + @Test + public void filledProfileWithFieldsIsParsedCorrectly() { + setupFilledProfile(); + Profile profile = configurationSoneParser.parseProfile(); + assertThat(profile, notNullValue()); + assertThat(profile.getFirstName(), is("First")); + assertThat(profile.getMiddleName(), is("M.")); + assertThat(profile.getLastName(), is("Last")); + assertThat(profile.getBirthDay(), is(18)); + assertThat(profile.getBirthMonth(), is(12)); + assertThat(profile.getBirthYear(), is(1976)); + final List fields = profile.getFields(); + assertThat(fields, hasSize(2)); + assertThat(fields.get(0).getName(), is("Field1")); + assertThat(fields.get(0).getValue(), is("Value1")); + assertThat(fields.get(1).getName(), is("Field2")); + assertThat(fields.get(1).getValue(), is("Value2")); + } + + private void setupFilledProfile() { + setupString("Sone/1/Profile/FirstName", "First"); + setupString("Sone/1/Profile/MiddleName", "M."); + setupString("Sone/1/Profile/LastName", "Last"); + setupInteger("Sone/1/Profile/BirthDay", 18); + setupInteger("Sone/1/Profile/BirthMonth", 12); + setupInteger("Sone/1/Profile/BirthYear", 1976); + setupString("Sone/1/Profile/Fields/0/Name", "Field1"); + setupString("Sone/1/Profile/Fields/0/Value", "Value1"); + setupString("Sone/1/Profile/Fields/1/Name", "Field2"); + setupString("Sone/1/Profile/Fields/1/Value", "Value2"); + setupString("Sone/1/Profile/Fields/2/Name", null); + } + + private void setupString(String nodeName, String value) { + when(configuration.getStringValue(eq(nodeName))).thenReturn( + TestValue.from(value)); + } + + private void setupInteger(String nodeName, Integer value) { + when(configuration.getIntValue(eq(nodeName))).thenReturn( + TestValue.from(value)); + } + + @Test + public void postsAreParsedCorrectly() { + setupCompletePosts(); + PostBuilderFactory postBuilderFactory = createPostBuilderFactory(); + Collection posts = + configurationSoneParser.parsePosts(postBuilderFactory); + assertThat(posts, + Matchers.containsInAnyOrder( + isPost("P0", 1000L, "T0", Optional.absent()), + isPost("P1", 1001L, "T1", + of("1234567890123456789012345678901234567890123")))); + } + + private PostBuilderFactory createPostBuilderFactory() { + PostBuilderFactory postBuilderFactory = + mock(PostBuilderFactory.class); + when(postBuilderFactory.newPostBuilder()).thenAnswer( + new Answer() { + @Override + public PostBuilder answer(InvocationOnMock invocation) + throws Throwable { + return new TestPostBuilder(); + } + }); + return postBuilderFactory; + } + + private void setupCompletePosts() { + setupPost("0", "P0", 1000L, "T0", null); + setupPost("1", "P1", 1001L, "T1", + "1234567890123456789012345678901234567890123"); + setupPost("2", null, 0L, null, null); + } + + private void setupPost(String postNumber, String postId, long time, + String text, String recipientId) { + setupString("Sone/1/Posts/" + postNumber + "/ID", postId); + setupLong("Sone/1/Posts/" + postNumber + "/Time", time); + setupString("Sone/1/Posts/" + postNumber + "/Text", text); + setupString("Sone/1/Posts/" + postNumber + "/Recipient", recipientId); + } + + private void setupLong(String nodeName, Long value) { + when(configuration.getLongValue(eq(nodeName))).thenReturn( + TestValue.from(value)); + } + + @Test(expected = InvalidPostFound.class) + public void postWithoutTimeIsRecognized() { + setupPostWithoutTime(); + configurationSoneParser.parsePosts(createPostBuilderFactory()); + } + + private void setupPostWithoutTime() { + setupPost("0", "P0", 0L, "T0", null); + } + + @Test(expected = InvalidPostFound.class) + public void postWithoutTextIsRecognized() { + setupPostWithoutText(); + configurationSoneParser.parsePosts(createPostBuilderFactory()); + } + + private void setupPostWithoutText() { + setupPost("0", "P0", 1000L, null, null); + } + + @Test + public void postWithInvalidRecipientIdIsRecognized() { + setupPostWithInvalidRecipientId(); + Collection posts = configurationSoneParser.parsePosts( + createPostBuilderFactory()); + assertThat(posts, contains( + isPost("P0", 1000L, "T0", Optional.absent()))); + } + + private void setupPostWithInvalidRecipientId() { + setupPost("0", "P0", 1000L, "T0", "123"); + setupPost("1", null, 0L, null, null); + } + + @Test + public void postRepliesAreParsedCorrectly() { + setupPostReplies(); + PostReplyBuilderFactory postReplyBuilderFactory = + new PostReplyBuilderFactory() { + @Override + public PostReplyBuilder newPostReplyBuilder() { + return new TestPostReplyBuilder(); + } + }; + Collection postReplies = + configurationSoneParser.parsePostReplies( + postReplyBuilderFactory); + assertThat(postReplies, hasSize(2)); + assertThat(postReplies, + containsInAnyOrder(isPostReply("R0", "P0", 1000L, "T0"), + isPostReply("R1", "P1", 1001L, "T1"))); + } + + private void setupPostReplies() { + setupPostReply("0", "R0", "P0", 1000L, "T0"); + setupPostReply("1", "R1", "P1", 1001L, "T1"); + setupPostReply("2", null, null, 0L, null); + } + + private void setupPostReply(String postReplyNumber, String postReplyId, + String postId, long time, String text) { + setupString("Sone/1/Replies/" + postReplyNumber + "/ID", postReplyId); + setupString("Sone/1/Replies/" + postReplyNumber + "/Post/ID", postId); + setupLong("Sone/1/Replies/" + postReplyNumber + "/Time", time); + setupString("Sone/1/Replies/" + postReplyNumber + "/Text", text); + } + + @Test(expected = InvalidPostReplyFound.class) + public void missingPostIdIsRecognized() { + setupPostReplyWithMissingPostId(); + configurationSoneParser.parsePostReplies(null); + } + + private void setupPostReplyWithMissingPostId() { + setupPostReply("0", "R0", null, 1000L, "T0"); + } + + @Test(expected = InvalidPostReplyFound.class) + public void missingPostReplyTimeIsRecognized() { + setupPostReplyWithMissingPostReplyTime(); + configurationSoneParser.parsePostReplies(null); + } + + private void setupPostReplyWithMissingPostReplyTime() { + setupPostReply("0", "R0", "P0", 0L, "T0"); + } + + @Test(expected = InvalidPostReplyFound.class) + public void missingPostReplyTextIsRecognized() { + setupPostReplyWithMissingPostReplyText(); + configurationSoneParser.parsePostReplies(null); + } + + private void setupPostReplyWithMissingPostReplyText() { + setupPostReply("0", "R0", "P0", 1000L, null); + } + + @Test + public void likedPostIdsParsedCorrectly() { + setupLikedPostIds(); + Set likedPostIds = + configurationSoneParser.parseLikedPostIds(); + assertThat(likedPostIds, containsInAnyOrder("P1", "P2", "P3")); + } + + private void setupLikedPostIds() { + setupString("Sone/1/Likes/Post/0/ID", "P1"); + setupString("Sone/1/Likes/Post/1/ID", "P2"); + setupString("Sone/1/Likes/Post/2/ID", "P3"); + setupString("Sone/1/Likes/Post/3/ID", null); + } + + @Test + public void likedPostReplyIdsAreParsedCorrectly() { + setupLikedPostReplyIds(); + Set likedPostReplyIds = + configurationSoneParser.parseLikedPostReplyIds(); + assertThat(likedPostReplyIds, containsInAnyOrder("R1", "R2", "R3")); + } + + private void setupLikedPostReplyIds() { + setupString("Sone/1/Likes/Reply/0/ID", "R1"); + setupString("Sone/1/Likes/Reply/1/ID", "R2"); + setupString("Sone/1/Likes/Reply/2/ID", "R3"); + setupString("Sone/1/Likes/Reply/3/ID", null); + } + + @Test + public void friendsAreParsedCorrectly() { + setupFriends(); + Set friends = configurationSoneParser.parseFriends(); + assertThat(friends, containsInAnyOrder("F1", "F2", "F3")); + } + + private void setupFriends() { + setupString("Sone/1/Friends/0/ID", "F1"); + setupString("Sone/1/Friends/1/ID", "F2"); + setupString("Sone/1/Friends/2/ID", "F3"); + setupString("Sone/1/Friends/3/ID", null); + } + + @Test + public void topLevelAlbumsAreParsedCorrectly() { + setupTopLevelAlbums(); + AlbumBuilderFactory albumBuilderFactory = createAlbumBuilderFactory(); + List topLevelAlbums = + configurationSoneParser.parseTopLevelAlbums( + albumBuilderFactory); + assertThat(topLevelAlbums, hasSize(2)); + Album firstAlbum = topLevelAlbums.get(0); + assertThat(firstAlbum, isAlbum("A1", null, "T1", "D1", "I1")); + assertThat(firstAlbum.getAlbums(), emptyIterable()); + assertThat(firstAlbum.getImages(), emptyIterable()); + Album secondAlbum = topLevelAlbums.get(1); + assertThat(secondAlbum, isAlbum("A2", null, "T2", "D2", null)); + assertThat(secondAlbum.getAlbums(), hasSize(1)); + assertThat(secondAlbum.getImages(), emptyIterable()); + Album thirdAlbum = secondAlbum.getAlbums().get(0); + assertThat(thirdAlbum, isAlbum("A3", "A2", "T3", "D3", "I3")); + assertThat(thirdAlbum.getAlbums(), emptyIterable()); + assertThat(thirdAlbum.getImages(), emptyIterable()); + } + + private void setupTopLevelAlbums() { + setupAlbum(0, "A1", null, "T1", "D1", "I1"); + setupAlbum(1, "A2", null, "T2", "D2", null); + setupAlbum(2, "A3", "A2", "T3", "D3", "I3"); + setupAlbum(3, null, null, null, null, null); + } + + private void setupAlbum(int albumNumber, String albumId, + String parentAlbumId, + String title, String description, String imageId) { + final String albumPrefix = "Sone/1/Albums/" + albumNumber; + setupString(albumPrefix + "/ID", albumId); + setupString(albumPrefix + "/Title", title); + setupString(albumPrefix + "/Description", description); + setupString(albumPrefix + "/Parent", parentAlbumId); + setupString(albumPrefix + "/AlbumImage", imageId); + } + + private AlbumBuilderFactory createAlbumBuilderFactory() { + AlbumBuilderFactory albumBuilderFactory = + mock(AlbumBuilderFactory.class); + when(albumBuilderFactory.newAlbumBuilder()).thenAnswer( + new Answer() { + @Override + public AlbumBuilder answer(InvocationOnMock invocation) { + return new TestAlbumBuilder(); + } + }); + return albumBuilderFactory; + } + + @Test(expected = InvalidAlbumFound.class) + public void albumWithInvalidTitleIsRecognized() { + setupAlbum(0, "A1", null, null, "D1", "I1"); + configurationSoneParser.parseTopLevelAlbums( + createAlbumBuilderFactory()); + } + + @Test(expected = InvalidAlbumFound.class) + public void albumWithInvalidDescriptionIsRecognized() { + setupAlbum(0, "A1", null, "T1", null, "I1"); + configurationSoneParser.parseTopLevelAlbums( + createAlbumBuilderFactory()); + } + + @Test(expected = InvalidParentAlbumFound.class) + public void albumWithInvalidParentIsRecognized() { + setupAlbum(0, "A1", "A0", "T1", "D1", "I1"); + configurationSoneParser.parseTopLevelAlbums( + createAlbumBuilderFactory()); + } + + @Test + public void imagesAreParsedCorrectly() { + setupTopLevelAlbums(); + configurationSoneParser.parseTopLevelAlbums( + createAlbumBuilderFactory()); + setupImages(); + configurationSoneParser.parseImages(createImageBuilderFactory()); + Map albums = configurationSoneParser.getAlbums(); + assertThat(albums.get("A1").getImages(), + contains(isImage("I1", 1000L, "K1", "T1", "D1", 16, 9))); + assertThat(albums.get("A2").getImages(), contains( + isImage("I2", 2000L, "K2", "T2", "D2", 16 * 2, 9 * 2))); + assertThat(albums.get("A3").getImages(), contains( + isImage("I3", 3000L, "K3", "T3", "D3", 16 * 3, 9 * 3))); + } + + private void setupImages() { + setupImage(0, "I1", "A1", 1000L, "K1", "T1", "D1", 16, 9); + setupImage(1, "I2", "A2", 2000L, "K2", "T2", "D2", 16 * 2, 9 * 2); + setupImage(2, "I3", "A3", 3000L, "K3", "T3", "D3", 16 * 3, 9 * 3); + setupImage(3, null, null, 0L, null, null, null, 0, 0); + } + + private void setupImage(int imageNumber, String id, + String parentAlbumId, Long creationTime, String key, String title, + String description, Integer width, Integer height) { + final String imagePrefix = "Sone/1/Images/" + imageNumber; + setupString(imagePrefix + "/ID", id); + setupString(imagePrefix + "/Album", parentAlbumId); + setupLong(imagePrefix + "/CreationTime", creationTime); + setupString(imagePrefix + "/Key", key); + setupString(imagePrefix + "/Title", title); + setupString(imagePrefix + "/Description", description); + setupInteger(imagePrefix + "/Width", width); + setupInteger(imagePrefix + "/Height", height); + } + + private ImageBuilderFactory createImageBuilderFactory() { + ImageBuilderFactory imageBuilderFactory = + mock(ImageBuilderFactory.class); + when(imageBuilderFactory.newImageBuilder()).thenAnswer( + new Answer() { + @Override + public ImageBuilder answer(InvocationOnMock invocation) + throws Throwable { + return new TestImageBuilder(); + } + }); + return imageBuilderFactory; + } + + @Test(expected = InvalidImageFound.class) + public void missingAlbumIdIsRecognized() { + setupTopLevelAlbums(); + configurationSoneParser.parseTopLevelAlbums( + createAlbumBuilderFactory()); + setupImage(0, "I1", null, 1000L, "K1", "T1", "D1", 16, 9); + configurationSoneParser.parseImages(createImageBuilderFactory()); + } + + @Test(expected = InvalidParentAlbumFound.class) + public void invalidAlbumIdIsRecognized() { + setupTopLevelAlbums(); + configurationSoneParser.parseTopLevelAlbums( + createAlbumBuilderFactory()); + setupImage(0, "I1", "A4", 1000L, "K1", "T1", "D1", 16, 9); + configurationSoneParser.parseImages(createImageBuilderFactory()); + } + + @Test(expected = InvalidImageFound.class) + public void missingCreationTimeIsRecognized() { + setupTopLevelAlbums(); + configurationSoneParser.parseTopLevelAlbums( + createAlbumBuilderFactory()); + setupImage(0, "I1", "A1", null, "K1", "T1", "D1", 16, 9); + configurationSoneParser.parseImages(createImageBuilderFactory()); + } + + @Test(expected = InvalidImageFound.class) + public void missingKeyIsRecognized() { + setupTopLevelAlbums(); + configurationSoneParser.parseTopLevelAlbums( + createAlbumBuilderFactory()); + setupImage(0, "I1", "A1", 1000L, null, "T1", "D1", 16, 9); + configurationSoneParser.parseImages(createImageBuilderFactory()); + } + + @Test(expected = InvalidImageFound.class) + public void missingTitleIsRecognized() { + setupTopLevelAlbums(); + configurationSoneParser.parseTopLevelAlbums( + createAlbumBuilderFactory()); + setupImage(0, "I1", "A1", 1000L, "K1", null, "D1", 16, 9); + configurationSoneParser.parseImages(createImageBuilderFactory()); + } + + @Test(expected = InvalidImageFound.class) + public void missingDescriptionIsRecognized() { + setupTopLevelAlbums(); + configurationSoneParser.parseTopLevelAlbums( + createAlbumBuilderFactory()); + setupImage(0, "I1", "A1", 1000L, "K1", "T1", null, 16, 9); + configurationSoneParser.parseImages(createImageBuilderFactory()); + } + + @Test(expected = InvalidImageFound.class) + public void missingWidthIsRecognized() { + setupTopLevelAlbums(); + configurationSoneParser.parseTopLevelAlbums( + createAlbumBuilderFactory()); + setupImage(0, "I1", "A1", 1000L, "K1", "T1", "D1", null, 9); + configurationSoneParser.parseImages(createImageBuilderFactory()); + } + + @Test(expected = InvalidImageFound.class) + public void missingHeightIsRecognized() { + setupTopLevelAlbums(); + configurationSoneParser.parseTopLevelAlbums( + createAlbumBuilderFactory()); + setupImage(0, "I1", "A1", 1000L, "K1", "T1", "D1", 16, null); + configurationSoneParser.parseImages(createImageBuilderFactory()); + } + +} diff --git a/src/test/java/net/pterodactylus/sone/core/CoreTest.java b/src/test/java/net/pterodactylus/sone/core/CoreTest.java new file mode 100644 index 0000000..263c2d0 --- /dev/null +++ b/src/test/java/net/pterodactylus/sone/core/CoreTest.java @@ -0,0 +1,39 @@ +package net.pterodactylus.sone.core; + +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import net.pterodactylus.sone.core.Core.MarkPostKnown; +import net.pterodactylus.sone.core.Core.MarkReplyKnown; +import net.pterodactylus.sone.data.Post; +import net.pterodactylus.sone.data.PostReply; + +import org.junit.Test; + +/** + * Unit test for {@link Core} and its subclasses. + * + * @author David ‘Bombe’ Roden + */ +public class CoreTest { + + @Test + public void markPostKnownMarksPostAsKnown() { + Core core = mock(Core.class); + Post post = mock(Post.class); + MarkPostKnown markPostKnown = core.new MarkPostKnown(post); + markPostKnown.run(); + verify(core).markPostKnown(eq(post)); + } + + @Test + public void markReplyKnownMarksReplyAsKnown() { + Core core = mock(Core.class); + PostReply postReply = mock(PostReply.class); + MarkReplyKnown markReplyKnown = core.new MarkReplyKnown(postReply); + markReplyKnown.run(); + verify(core).markReplyKnown(eq(postReply)); + } + +} diff --git a/src/test/java/net/pterodactylus/sone/core/FreenetInterfaceTest.java b/src/test/java/net/pterodactylus/sone/core/FreenetInterfaceTest.java new file mode 100644 index 0000000..091c93f --- /dev/null +++ b/src/test/java/net/pterodactylus/sone/core/FreenetInterfaceTest.java @@ -0,0 +1,401 @@ +package net.pterodactylus.sone.core; + +import static freenet.keys.InsertableClientSSK.createRandom; +import static freenet.node.RequestStarter.INTERACTIVE_PRIORITY_CLASS; +import static freenet.node.RequestStarter.PREFETCH_PRIORITY_CLASS; +import static net.pterodactylus.sone.Matchers.delivers; +import static net.pterodactylus.sone.TestUtil.setFinalField; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; +import static org.mockito.ArgumentCaptor.forClass; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyBoolean; +import static org.mockito.Matchers.anyShort; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.mockito.Mockito.withSettings; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.util.HashMap; + +import net.pterodactylus.sone.TestUtil; +import net.pterodactylus.sone.core.FreenetInterface.Callback; +import net.pterodactylus.sone.core.FreenetInterface.Fetched; +import net.pterodactylus.sone.core.FreenetInterface.InsertToken; +import net.pterodactylus.sone.core.FreenetInterface.InsertTokenSupplier; +import net.pterodactylus.sone.core.event.ImageInsertAbortedEvent; +import net.pterodactylus.sone.core.event.ImageInsertFailedEvent; +import net.pterodactylus.sone.core.event.ImageInsertFinishedEvent; +import net.pterodactylus.sone.core.event.ImageInsertStartedEvent; +import net.pterodactylus.sone.data.Image; +import net.pterodactylus.sone.data.impl.ImageImpl; +import net.pterodactylus.sone.data.Sone; +import net.pterodactylus.sone.data.TemporaryImage; + +import freenet.client.ClientMetadata; +import freenet.client.FetchException; +import freenet.client.FetchException.FetchExceptionMode; +import freenet.client.FetchResult; +import freenet.client.HighLevelSimpleClient; +import freenet.client.InsertBlock; +import freenet.client.InsertContext; +import freenet.client.InsertException; +import freenet.client.InsertException.InsertExceptionMode; +import freenet.client.async.ClientPutter; +import freenet.client.async.USKCallback; +import freenet.client.async.USKManager; +import freenet.crypt.DummyRandomSource; +import freenet.crypt.RandomSource; +import freenet.keys.FreenetURI; +import freenet.keys.InsertableClientSSK; +import freenet.keys.USK; +import freenet.node.Node; +import freenet.node.NodeClientCore; +import freenet.node.RequestClient; +import freenet.support.Base64; +import freenet.support.api.Bucket; +import freenet.support.io.ArrayBucket; +import freenet.support.io.ResumeFailedException; + +import com.google.common.eventbus.EventBus; +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; + +/** + * Unit test for {@link FreenetInterface}. + * + * @author David ‘Bombe’ Roden + */ +public class FreenetInterfaceTest { + + private final EventBus eventBus = mock(EventBus.class); + private final Node node = mock(Node.class); + private final NodeClientCore nodeClientCore = mock(NodeClientCore.class); + private final HighLevelSimpleClient highLevelSimpleClient = mock(HighLevelSimpleClient.class, withSettings().extraInterfaces(RequestClient.class)); + private final RandomSource randomSource = new DummyRandomSource(); + private final USKManager uskManager = mock(USKManager.class); + private FreenetInterface freenetInterface; + private final Sone sone = mock(Sone.class); + private final ArgumentCaptor callbackCaptor = forClass(USKCallback.class); + private final Image image = mock(Image.class); + private InsertToken insertToken; + private final Bucket bucket = mock(Bucket.class); + + @Before + public void setupFreenetInterface() { + when(nodeClientCore.makeClient(anyShort(), anyBoolean(), anyBoolean())).thenReturn(highLevelSimpleClient); + setFinalField(node, "clientCore", nodeClientCore); + setFinalField(node, "random", randomSource); + setFinalField(nodeClientCore, "uskManager", uskManager); + freenetInterface = new FreenetInterface(eventBus, node); + insertToken = freenetInterface.new InsertToken(image); + insertToken.setBucket(bucket); + } + + @Before + public void setupSone() { + InsertableClientSSK insertSsk = createRandom(randomSource, "test-0"); + when(sone.getId()).thenReturn(Base64.encode(insertSsk.getURI().getRoutingKey())); + when(sone.getRequestUri()).thenReturn(insertSsk.getURI().uskForSSK()); + } + + @Before + public void setupCallbackCaptorAndUskManager() { + doNothing().when(uskManager).subscribe(any(USK.class), callbackCaptor.capture(), anyBoolean(), any(RequestClient.class)); + } + + @Test + public void canFetchUri() throws MalformedURLException, FetchException { + FreenetURI freenetUri = new FreenetURI("KSK@GPLv3.txt"); + FetchResult fetchResult = createFetchResult(); + when(highLevelSimpleClient.fetch(freenetUri)).thenReturn(fetchResult); + Fetched fetched = freenetInterface.fetchUri(freenetUri); + assertThat(fetched, notNullValue()); + assertThat(fetched.getFetchResult(), is(fetchResult)); + assertThat(fetched.getFreenetUri(), is(freenetUri)); + } + + @Test + public void fetchFollowsRedirect() throws MalformedURLException, FetchException { + FreenetURI freenetUri = new FreenetURI("KSK@GPLv2.txt"); + FreenetURI newFreenetUri = new FreenetURI("KSK@GPLv3.txt"); + FetchResult fetchResult = createFetchResult(); + FetchException fetchException = new FetchException(FetchExceptionMode.PERMANENT_REDIRECT, newFreenetUri); + when(highLevelSimpleClient.fetch(freenetUri)).thenThrow(fetchException); + when(highLevelSimpleClient.fetch(newFreenetUri)).thenReturn(fetchResult); + Fetched fetched = freenetInterface.fetchUri(freenetUri); + assertThat(fetched.getFetchResult(), is(fetchResult)); + assertThat(fetched.getFreenetUri(), is(newFreenetUri)); + } + + @Test + public void fetchReturnsNullOnFetchExceptions() throws MalformedURLException, FetchException { + FreenetURI freenetUri = new FreenetURI("KSK@GPLv2.txt"); + FetchException fetchException = new FetchException(FetchExceptionMode.ALL_DATA_NOT_FOUND); + when(highLevelSimpleClient.fetch(freenetUri)).thenThrow(fetchException); + Fetched fetched = freenetInterface.fetchUri(freenetUri); + assertThat(fetched, nullValue()); + } + + private FetchResult createFetchResult() { + ClientMetadata clientMetadata = new ClientMetadata("text/plain"); + Bucket bucket = new ArrayBucket("Some Data.".getBytes()); + return new FetchResult(clientMetadata, bucket); + } + + @Test + public void insertingAnImage() throws SoneException, InsertException, IOException { + TemporaryImage temporaryImage = new TemporaryImage("image-id"); + temporaryImage.setMimeType("image/png"); + byte[] imageData = new byte[] { 1, 2, 3, 4 }; + temporaryImage.setImageData(imageData); + Image image = new ImageImpl("image-id"); + InsertToken insertToken = freenetInterface.new InsertToken(image); + InsertContext insertContext = mock(InsertContext.class); + when(highLevelSimpleClient.getInsertContext(anyBoolean())).thenReturn(insertContext); + ClientPutter clientPutter = mock(ClientPutter.class); + ArgumentCaptor insertBlockCaptor = forClass(InsertBlock.class); + when(highLevelSimpleClient.insert(insertBlockCaptor.capture(), eq((String) null), eq(false), eq(insertContext), eq(insertToken), anyShort())).thenReturn(clientPutter); + freenetInterface.insertImage(temporaryImage, image, insertToken); + assertThat(insertBlockCaptor.getValue().getData().getInputStream(), delivers(new byte[] { 1, 2, 3, 4 })); + assertThat(TestUtil.getPrivateField(insertToken, "clientPutter"), is(clientPutter)); + verify(eventBus).post(any(ImageInsertStartedEvent.class)); + } + + @Test(expected = SoneInsertException.class) + public void insertExceptionCausesASoneException() throws InsertException, SoneException, IOException { + TemporaryImage temporaryImage = new TemporaryImage("image-id"); + temporaryImage.setMimeType("image/png"); + byte[] imageData = new byte[] { 1, 2, 3, 4 }; + temporaryImage.setImageData(imageData); + Image image = new ImageImpl("image-id"); + InsertToken insertToken = freenetInterface.new InsertToken(image); + InsertContext insertContext = mock(InsertContext.class); + when(highLevelSimpleClient.getInsertContext(anyBoolean())).thenReturn(insertContext); + ArgumentCaptor insertBlockCaptor = forClass(InsertBlock.class); + when(highLevelSimpleClient.insert(insertBlockCaptor.capture(), eq((String) null), eq(false), eq(insertContext), eq(insertToken), anyShort())).thenThrow(InsertException.class); + freenetInterface.insertImage(temporaryImage, image, insertToken); + } + + @Test + public void insertingADirectory() throws InsertException, SoneException { + FreenetURI freenetUri = mock(FreenetURI.class); + HashMap manifestEntries = new HashMap(); + String defaultFile = "index.html"; + FreenetURI resultingUri = mock(FreenetURI.class); + when(highLevelSimpleClient.insertManifest(eq(freenetUri), eq(manifestEntries), eq(defaultFile))).thenReturn(resultingUri); + assertThat(freenetInterface.insertDirectory(freenetUri, manifestEntries, defaultFile), is(resultingUri)); + } + + @Test(expected = SoneException.class) + public void insertExceptionIsForwardedAsSoneException() throws InsertException, SoneException { + when(highLevelSimpleClient.insertManifest(any(FreenetURI.class), any(HashMap.class), any(String.class))).thenThrow(InsertException.class); + freenetInterface.insertDirectory(null, null, null); + } + + @Test + public void soneWithWrongRequestUriWillNotBeSubscribed() throws MalformedURLException { + when(sone.getRequestUri()).thenReturn(new FreenetURI("KSK@GPLv3.txt")); + freenetInterface.registerUsk(new FreenetURI("KSK@GPLv3.txt"), null); + verify(uskManager, never()).subscribe(any(USK.class), any(USKCallback.class), anyBoolean(), any(RequestClient.class)); + } + + @Test + public void registeringAUsk() { + FreenetURI freenetUri = createRandom(randomSource, "test-0").getURI().uskForSSK(); + Callback callback = mock(Callback.class); + freenetInterface.registerUsk(freenetUri, callback); + verify(uskManager).subscribe(any(USK.class), any(USKCallback.class), anyBoolean(), eq((RequestClient) highLevelSimpleClient)); + } + + @Test + public void registeringANonUskKeyWillNotBeSubscribed() throws MalformedURLException { + FreenetURI freenetUri = new FreenetURI("KSK@GPLv3.txt"); + Callback callback = mock(Callback.class); + freenetInterface.registerUsk(freenetUri, callback); + verify(uskManager, never()).subscribe(any(USK.class), any(USKCallback.class), anyBoolean(), eq((RequestClient) highLevelSimpleClient)); + } + + @Test + public void registeringAnActiveUskWillSubscribeToItCorrectly() { + FreenetURI freenetUri = createRandom(randomSource, "test-0").getURI().uskForSSK(); + final USKCallback uskCallback = mock(USKCallback.class); + freenetInterface.registerActiveUsk(freenetUri, uskCallback); + verify(uskManager).subscribe(any(USK.class), eq(uskCallback), eq(true), any(RequestClient.class)); + } + + @Test + public void registeringAnInactiveUskWillSubscribeToItCorrectly() { + FreenetURI freenetUri = createRandom(randomSource, "test-0").getURI().uskForSSK(); + final USKCallback uskCallback = mock(USKCallback.class); + freenetInterface.registerPassiveUsk(freenetUri, uskCallback); + verify(uskManager).subscribe(any(USK.class), eq(uskCallback), eq(false), any(RequestClient.class)); + } + + @Test + public void registeringAnActiveNonUskWillNotSubscribeToAUsk() + throws MalformedURLException { + FreenetURI freenetUri = createRandom(randomSource, "test-0").getURI(); + freenetInterface.registerActiveUsk(freenetUri, null); + verify(uskManager, never()).subscribe(any(USK.class), + any(USKCallback.class), anyBoolean(), + eq((RequestClient) highLevelSimpleClient)); + } + + @Test + public void registeringAnInactiveNonUskWillNotSubscribeToAUsk() + throws MalformedURLException { + FreenetURI freenetUri = createRandom(randomSource, "test-0").getURI(); + freenetInterface.registerPassiveUsk(freenetUri, null); + verify(uskManager, never()).subscribe(any(USK.class), + any(USKCallback.class), anyBoolean(), + eq((RequestClient) highLevelSimpleClient)); + } + + @Test + public void unregisteringANotRegisteredUskDoesNothing() { + FreenetURI freenetURI = createRandom(randomSource, "test-0").getURI().uskForSSK(); + freenetInterface.unregisterUsk(freenetURI); + verify(uskManager, never()).unsubscribe(any(USK.class), any(USKCallback.class)); + } + + @Test + public void unregisteringARegisteredUsk() { + FreenetURI freenetURI = createRandom(randomSource, "test-0").getURI().uskForSSK(); + Callback callback = mock(Callback.class); + freenetInterface.registerUsk(freenetURI, callback); + freenetInterface.unregisterUsk(freenetURI); + verify(uskManager).unsubscribe(any(USK.class), any(USKCallback.class)); + } + + @Test + public void unregisteringANotRegisteredSoneDoesNothing() { + freenetInterface.unregisterUsk(sone); + verify(uskManager, never()).unsubscribe(any(USK.class), any(USKCallback.class)); + } + + @Test + public void unregisteringARegisteredSoneUnregistersTheSone() + throws MalformedURLException { + freenetInterface.registerActiveUsk(sone.getRequestUri(), mock(USKCallback.class)); + freenetInterface.unregisterUsk(sone); + verify(uskManager).unsubscribe(any(USK.class), any(USKCallback.class)); + } + + @Test + public void unregisteringASoneWithAWrongRequestKeyWillNotUnsubscribe() throws MalformedURLException { + when(sone.getRequestUri()).thenReturn(new FreenetURI("KSK@GPLv3.txt")); + freenetInterface.registerUsk(sone.getRequestUri(), null); + freenetInterface.unregisterUsk(sone); + verify(uskManager, never()).unsubscribe(any(USK.class), any(USKCallback.class)); + } + + @Test + public void callbackForNormalUskUsesDifferentPriorities() { + Callback callback = mock(Callback.class); + FreenetURI soneUri = createRandom(randomSource, "test-0").getURI().uskForSSK(); + freenetInterface.registerUsk(soneUri, callback); + assertThat(callbackCaptor.getValue().getPollingPriorityNormal(), is(PREFETCH_PRIORITY_CLASS)); + assertThat(callbackCaptor.getValue().getPollingPriorityProgress(), is(INTERACTIVE_PRIORITY_CLASS)); + } + + @Test + public void callbackForNormalUskForwardsImportantParameters() throws MalformedURLException { + Callback callback = mock(Callback.class); + FreenetURI uri = createRandom(randomSource, "test-0").getURI().uskForSSK(); + freenetInterface.registerUsk(uri, callback); + USK key = mock(USK.class); + when(key.getURI()).thenReturn(uri); + callbackCaptor.getValue().onFoundEdition(3, key, null, false, (short) 0, null, true, true); + verify(callback).editionFound(eq(uri), eq(3L), eq(true), eq(true)); + } + + @Test + public void fetchedRetainsUriAndFetchResult() { + FreenetURI freenetUri = mock(FreenetURI.class); + FetchResult fetchResult = mock(FetchResult.class); + Fetched fetched = new Fetched(freenetUri, fetchResult); + assertThat(fetched.getFreenetUri(), is(freenetUri)); + assertThat(fetched.getFetchResult(), is(fetchResult)); + } + + @Test + public void cancellingAnInsertWillFireImageInsertAbortedEvent() { + ClientPutter clientPutter = mock(ClientPutter.class); + insertToken.setClientPutter(clientPutter); + ArgumentCaptor imageInsertStartedEvent = forClass(ImageInsertStartedEvent.class); + verify(eventBus).post(imageInsertStartedEvent.capture()); + assertThat(imageInsertStartedEvent.getValue().image(), is(image)); + insertToken.cancel(); + ArgumentCaptor imageInsertAbortedEvent = forClass(ImageInsertAbortedEvent.class); + verify(eventBus, times(2)).post(imageInsertAbortedEvent.capture()); + verify(bucket).free(); + assertThat(imageInsertAbortedEvent.getValue().image(), is(image)); + } + + @Test + public void failureWithoutExceptionSendsFailedEvent() { + insertToken.onFailure(null, null); + ArgumentCaptor imageInsertFailedEvent = forClass(ImageInsertFailedEvent.class); + verify(eventBus).post(imageInsertFailedEvent.capture()); + verify(bucket).free(); + assertThat(imageInsertFailedEvent.getValue().image(), is(image)); + assertThat(imageInsertFailedEvent.getValue().cause(), nullValue()); + } + + @Test + public void failureSendsFailedEventWithException() { + InsertException insertException = new InsertException(InsertExceptionMode.INTERNAL_ERROR, "Internal error", null); + insertToken.onFailure(insertException, null); + ArgumentCaptor imageInsertFailedEvent = forClass(ImageInsertFailedEvent.class); + verify(eventBus).post(imageInsertFailedEvent.capture()); + verify(bucket).free(); + assertThat(imageInsertFailedEvent.getValue().image(), is(image)); + assertThat(imageInsertFailedEvent.getValue().cause(), is((Throwable) insertException)); + } + + @Test + public void failureBecauseCancelledByUserSendsAbortedEvent() { + InsertException insertException = new InsertException(InsertExceptionMode.CANCELLED, null); + insertToken.onFailure(insertException, null); + ArgumentCaptor imageInsertAbortedEvent = forClass(ImageInsertAbortedEvent.class); + verify(eventBus).post(imageInsertAbortedEvent.capture()); + verify(bucket).free(); + assertThat(imageInsertAbortedEvent.getValue().image(), is(image)); + } + + @Test + public void ignoredMethodsDoNotThrowExceptions() throws ResumeFailedException { + insertToken.onResume(null); + insertToken.onFetchable(null); + insertToken.onGeneratedMetadata(null, null); + } + + @Test + public void generatedUriIsPostedOnSuccess() { + FreenetURI generatedUri = mock(FreenetURI.class); + insertToken.onGeneratedURI(generatedUri, null); + insertToken.onSuccess(null); + ArgumentCaptor imageInsertFinishedEvent = forClass(ImageInsertFinishedEvent.class); + verify(eventBus).post(imageInsertFinishedEvent.capture()); + verify(bucket).free(); + assertThat(imageInsertFinishedEvent.getValue().image(), is(image)); + assertThat(imageInsertFinishedEvent.getValue().resultingUri(), is(generatedUri)); + } + + @Test + public void insertTokenSupplierSuppliesInsertTokens() { + InsertTokenSupplier insertTokenSupplier = freenetInterface.new InsertTokenSupplier(); + assertThat(insertTokenSupplier.apply(image), notNullValue()); + } + +} diff --git a/src/test/java/net/pterodactylus/sone/core/ImageInserterTest.java b/src/test/java/net/pterodactylus/sone/core/ImageInserterTest.java new file mode 100644 index 0000000..0912391 --- /dev/null +++ b/src/test/java/net/pterodactylus/sone/core/ImageInserterTest.java @@ -0,0 +1,58 @@ +package net.pterodactylus.sone.core; + +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import net.pterodactylus.sone.core.FreenetInterface.InsertToken; +import net.pterodactylus.sone.data.Image; +import net.pterodactylus.sone.data.TemporaryImage; + +import com.google.common.base.Function; +import org.junit.Test; + +/** + * Unit test for {@link ImageInserter}. + * + * @author David ‘Bombe’ Roden + */ +public class ImageInserterTest { + + private final TemporaryImage temporaryImage = when(mock(TemporaryImage.class).getId()).thenReturn("image-id").getMock(); + private final Image image = when(mock(Image.class).getId()).thenReturn("image-id").getMock(); + private final FreenetInterface freenetInterface = mock(FreenetInterface.class); + private final InsertToken insertToken = mock(InsertToken.class); + private final Function insertTokenSupplier = when(mock(Function.class).apply(any(Image.class))).thenReturn(insertToken).getMock(); + private final ImageInserter imageInserter = new ImageInserter(freenetInterface, insertTokenSupplier); + + @Test + public void inserterInsertsImage() throws SoneException { + imageInserter.insertImage(temporaryImage, image); + verify(freenetInterface).insertImage(eq(temporaryImage), eq(image), any(InsertToken.class)); + } + + @Test + public void exceptionWhenInsertingImageIsIgnored() throws SoneException { + doThrow(SoneException.class).when(freenetInterface).insertImage(eq(temporaryImage), eq(image), any(InsertToken.class)); + imageInserter.insertImage(temporaryImage, image); + verify(freenetInterface).insertImage(eq(temporaryImage), eq(image), any(InsertToken.class)); + } + + @Test + public void cancellingImageInsertThatIsNotRunningDoesNothing() { + imageInserter.cancelImageInsert(image); + verify(insertToken, never()).cancel(); + } + + @Test + public void cancellingImage() { + imageInserter.insertImage(temporaryImage, image); + imageInserter.cancelImageInsert(image); + verify(insertToken).cancel(); + } + +} diff --git a/src/test/java/net/pterodactylus/sone/core/OptionsTest.java b/src/test/java/net/pterodactylus/sone/core/OptionsTest.java new file mode 100644 index 0000000..cce4df0 --- /dev/null +++ b/src/test/java/net/pterodactylus/sone/core/OptionsTest.java @@ -0,0 +1,55 @@ +package net.pterodactylus.sone.core; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; +import static org.mockito.Mockito.mock; + +import net.pterodactylus.sone.utils.Option; + +import org.junit.Test; + +/** + * Unit test for {@link Options}. + * + * @author David ‘Bombe’ Roden + */ +public class OptionsTest { + + private final Options options = new Options(); + + @Test + public void booleanOptionIsAdded() { + Option booleanOption = mock(Option.class); + options.addBooleanOption("test", booleanOption); + assertThat(options.getBooleanOption("test"), is(booleanOption)); + assertThat(options.getBooleanOption("not-test"), nullValue()); + } + + @Test + public void integerOptionIsAdded() { + Option integerOption = mock(Option.class); + options.addIntegerOption("test", integerOption); + assertThat(options.getIntegerOption("test"), is(integerOption)); + assertThat(options.getIntegerOption("not-test"), nullValue()); + } + + @Test + public void stringOptionIsAdded() { + Option stringOption = mock(Option.class); + options.addStringOption("test", stringOption); + assertThat(options.getStringOption("test"), is(stringOption)); + assertThat(options.getStringOption("not-test"), nullValue()); + } + + @Test + public void enumOptionIsAdded() { + Option enumOption = mock(Option.class); + options.addEnumOption("test", enumOption); + assertThat(options.getEnumOption("test"), is(enumOption)); + assertThat(options.getEnumOption("not-test"), nullValue()); + } + + private enum TestEnum {TEST, NOT_TEST} + +} diff --git a/src/test/java/net/pterodactylus/sone/core/PreferencesLoaderTest.java b/src/test/java/net/pterodactylus/sone/core/PreferencesLoaderTest.java new file mode 100644 index 0000000..e12fe93 --- /dev/null +++ b/src/test/java/net/pterodactylus/sone/core/PreferencesLoaderTest.java @@ -0,0 +1,82 @@ +package net.pterodactylus.sone.core; + +import static net.pterodactylus.sone.fcp.FcpInterface.FullAccessRequired.WRITING; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import net.pterodactylus.sone.TestValue; +import net.pterodactylus.util.config.Configuration; + +import com.google.common.eventbus.EventBus; +import org.junit.Before; +import org.junit.Test; + +/** + * Unit test for {@link PreferencesLoader}. + * + * @author David ‘Bombe’ Roden + */ +public class PreferencesLoaderTest { + + private final EventBus eventBus = mock(EventBus.class); + private final Preferences preferences = new Preferences(eventBus); + private final Configuration configuration = mock(Configuration.class); + private final PreferencesLoader preferencesLoader = + new PreferencesLoader(preferences); + + @Before + public void setupConfiguration() { + setupIntValue("InsertionDelay", 15); + setupIntValue("PostsPerPage", 25); + setupIntValue("ImagesPerPage", 12); + setupIntValue("CharactersPerPost", 150); + setupIntValue("PostCutOffLength", 300); + setupBooleanValue("RequireFullAccess", true); + setupIntValue("PositiveTrust", 50); + setupIntValue("NegativeTrust", -50); + when(configuration.getStringValue("Option/TrustComment")).thenReturn( + TestValue.from("Trusted")); + setupBooleanValue("ActivateFcpInterface", true); + setupIntValue("FcpFullAccessRequired", 1); + } + + private void setupIntValue(String optionName, int value) { + when(configuration.getIntValue("Option/" + optionName)).thenReturn( + TestValue.from(value)); + } + + private void setupBooleanValue(String optionName, boolean value) { + when(configuration.getBooleanValue( + "Option/" + optionName)).thenReturn( + TestValue.from(value)); + } + + @Test + public void configurationIsLoadedCorrectly() { + setupConfiguration(); + preferencesLoader.loadFrom(configuration); + assertThat(preferences.getInsertionDelay(), is(15)); + assertThat(preferences.getPostsPerPage(), is(25)); + assertThat(preferences.getImagesPerPage(), is(12)); + assertThat(preferences.getCharactersPerPost(), is(150)); + assertThat(preferences.getPostCutOffLength(), is(300)); + assertThat(preferences.isRequireFullAccess(), is(true)); + assertThat(preferences.getPositiveTrust(), is(50)); + assertThat(preferences.getNegativeTrust(), is(-50)); + assertThat(preferences.getTrustComment(), is("Trusted")); + assertThat(preferences.isFcpInterfaceActive(), is(true)); + assertThat(preferences.getFcpFullAccessRequired(), is(WRITING)); + } + + @Test + public void configurationIsLoadedCorrectlyWithCutOffLengthMinusOne() { + setupConfiguration(); + setupIntValue("PostCutOffLength", -1); + preferencesLoader.loadFrom(configuration); + assertThat(preferences.getPostCutOffLength(), not(is(-1))); + } + +} diff --git a/src/test/java/net/pterodactylus/sone/core/PreferencesTest.java b/src/test/java/net/pterodactylus/sone/core/PreferencesTest.java new file mode 100644 index 0000000..a19fe2e --- /dev/null +++ b/src/test/java/net/pterodactylus/sone/core/PreferencesTest.java @@ -0,0 +1,311 @@ +package net.pterodactylus.sone.core; + +import static net.pterodactylus.sone.fcp.FcpInterface.FullAccessRequired.ALWAYS; +import static net.pterodactylus.sone.fcp.FcpInterface.FullAccessRequired.NO; +import static net.pterodactylus.sone.fcp.FcpInterface.FullAccessRequired.WRITING; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +import net.pterodactylus.sone.core.event.InsertionDelayChangedEvent; +import net.pterodactylus.sone.fcp.FcpInterface.FullAccessRequired; +import net.pterodactylus.sone.fcp.event.FcpInterfaceActivatedEvent; +import net.pterodactylus.sone.fcp.event.FcpInterfaceDeactivatedEvent; +import net.pterodactylus.sone.fcp.event.FullAccessRequiredChanged; + +import com.google.common.eventbus.EventBus; +import org.junit.After; +import org.junit.Test; +import org.mockito.ArgumentCaptor; + +/** + * Unit test for {@link Preferences}. + * + * @author David ‘Bombe’ Roden + */ +public class PreferencesTest { + + private final EventBus eventBus = mock(EventBus.class); + private final Preferences preferences = new Preferences(eventBus); + + @After + public void tearDown() { + verifyNoMoreInteractions(eventBus); + } + + @Test + public void preferencesRetainInsertionDelay() { + preferences.setInsertionDelay(15); + assertThat(preferences.getInsertionDelay(), is(15)); + verify(eventBus).post(any(InsertionDelayChangedEvent.class)); + } + + @Test(expected = IllegalArgumentException.class) + public void invalidInsertionDelayIsRejected() { + preferences.setInsertionDelay(-15); + } + + @Test + public void preferencesReturnDefaultValueWhenInsertionDelayIsSetToNull() { + preferences.setInsertionDelay(null); + assertThat(preferences.getInsertionDelay(), is(60)); + verify(eventBus).post(any(InsertionDelayChangedEvent.class)); + } + + @Test + public void preferencesStartWithInsertionDelayDefaultValue() { + assertThat(preferences.getInsertionDelay(), is(60)); + } + + @Test + public void preferencesRetainPostsPerPage() { + preferences.setPostsPerPage(15); + assertThat(preferences.getPostsPerPage(), is(15)); + } + + @Test(expected = IllegalArgumentException.class) + public void invalidPostsPerPageIsRejected() { + preferences.setPostsPerPage(-15); + } + + @Test + public void preferencesReturnDefaultValueWhenPostsPerPageIsSetToNull() { + preferences.setPostsPerPage(null); + assertThat(preferences.getPostsPerPage(), is(10)); + } + + @Test + public void preferencesStartWithPostsPerPageDefaultValue() { + assertThat(preferences.getPostsPerPage(), is(10)); + } + + @Test + public void preferencesRetainImagesPerPage() { + preferences.setImagesPerPage(15); + assertThat(preferences.getImagesPerPage(), is(15)); + } + + @Test(expected = IllegalArgumentException.class) + public void invalidImagesPerPageIsRejected() { + preferences.setImagesPerPage(-15); + } + + @Test + public void preferencesReturnDefaultValueWhenImagesPerPageIsSetToNull() { + preferences.setImagesPerPage(null); + assertThat(preferences.getImagesPerPage(), is(9)); + } + + @Test + public void preferencesStartWithImagesPerPageDefaultValue() { + assertThat(preferences.getImagesPerPage(), is(9)); + } + + @Test + public void preferencesRetainCharactersPerPost() { + preferences.setCharactersPerPost(150); + assertThat(preferences.getCharactersPerPost(), is(150)); + } + + @Test(expected = IllegalArgumentException.class) + public void invalidCharactersPerPostIsRejected() { + preferences.setCharactersPerPost(-15); + } + + @Test + public void preferencesReturnDefaultValueWhenCharactersPerPostIsSetToNull() { + preferences.setCharactersPerPost(null); + assertThat(preferences.getCharactersPerPost(), is(400)); + } + + @Test + public void preferencesStartWithCharactersPerPostDefaultValue() { + assertThat(preferences.getCharactersPerPost(), is(400)); + } + + @Test + public void preferencesRetainPostCutOffLength() { + preferences.setPostCutOffLength(150); + assertThat(preferences.getPostCutOffLength(), is(150)); + } + + @Test(expected = IllegalArgumentException.class) + public void invalidPostCutOffLengthIsRejected() { + preferences.setPostCutOffLength(-15); + } + + @Test(expected = IllegalArgumentException.class) + public void cutOffLengthOfMinusOneIsNotAllowed() { + preferences.setPostCutOffLength(-1); + } + + @Test + public void preferencesReturnDefaultValueWhenPostCutOffLengthIsSetToNull() { + preferences.setPostCutOffLength(null); + assertThat(preferences.getPostCutOffLength(), is(200)); + } + + @Test + public void preferencesStartWithPostCutOffLengthDefaultValue() { + assertThat(preferences.getPostCutOffLength(), is(200)); + } + + @Test + public void preferencesRetainRequireFullAccessOfTrue() { + preferences.setRequireFullAccess(true); + assertThat(preferences.isRequireFullAccess(), is(true)); + } + + @Test + public void preferencesRetainRequireFullAccessOfFalse() { + preferences.setRequireFullAccess(false); + assertThat(preferences.isRequireFullAccess(), is(false)); + } + + @Test + public void preferencesReturnDefaultValueWhenRequireFullAccessIsSetToNull() { + preferences.setRequireFullAccess(null); + assertThat(preferences.isRequireFullAccess(), is(false)); + } + + @Test + public void preferencesStartWithRequireFullAccessDefaultValue() { + assertThat(preferences.isRequireFullAccess(), is(false)); + } + + @Test + public void preferencesRetainPositiveTrust() { + preferences.setPositiveTrust(15); + assertThat(preferences.getPositiveTrust(), is(15)); + } + + @Test(expected = IllegalArgumentException.class) + public void invalidPositiveTrustIsRejected() { + preferences.setPositiveTrust(-15); + } + + @Test + public void preferencesReturnDefaultValueWhenPositiveTrustIsSetToNull() { + preferences.setPositiveTrust(null); + assertThat(preferences.getPositiveTrust(), is(75)); + } + + @Test + public void preferencesStartWithPositiveTrustDefaultValue() { + assertThat(preferences.getPositiveTrust(), is(75)); + } + + @Test + public void preferencesRetainNegativeTrust() { + preferences.setNegativeTrust(-15); + assertThat(preferences.getNegativeTrust(), is(-15)); + } + + @Test(expected = IllegalArgumentException.class) + public void invalidNegativeTrustIsRejected() { + preferences.setNegativeTrust(150); + } + + @Test + public void preferencesReturnDefaultValueWhenNegativeTrustIsSetToNull() { + preferences.setNegativeTrust(null); + assertThat(preferences.getNegativeTrust(), is(-25)); + } + + @Test + public void preferencesStartWithNegativeTrustDefaultValue() { + assertThat(preferences.getNegativeTrust(), is(-25)); + } + + @Test + public void preferencesRetainTrustComment() { + preferences.setTrustComment("Trust"); + assertThat(preferences.getTrustComment(), is("Trust")); + } + + @Test + public void preferencesReturnDefaultValueWhenTrustCommentIsSetToNull() { + preferences.setTrustComment(null); + assertThat(preferences.getTrustComment(), + is("Set from Sone Web Interface")); + } + + @Test + public void preferencesStartWithTrustCommentDefaultValue() { + assertThat(preferences.getTrustComment(), + is("Set from Sone Web Interface")); + } + + @Test + public void preferencesRetainFcpInterfaceActiveOfTrue() { + preferences.setFcpInterfaceActive(true); + assertThat(preferences.isFcpInterfaceActive(), is(true)); + verify(eventBus).post(any(FcpInterfaceActivatedEvent.class)); + } + + @Test + public void preferencesRetainFcpInterfaceActiveOfFalse() { + preferences.setFcpInterfaceActive(false); + assertThat(preferences.isFcpInterfaceActive(), is(false)); + verify(eventBus).post(any(FcpInterfaceDeactivatedEvent.class)); + } + + @Test + public void preferencesReturnDefaultValueWhenFcpInterfaceActiveIsSetToNull() { + preferences.setFcpInterfaceActive(null); + assertThat(preferences.isFcpInterfaceActive(), is(false)); + verify(eventBus).post(any(FcpInterfaceDeactivatedEvent.class)); + } + + @Test + public void preferencesStartWithFcpInterfaceActiveDefaultValue() { + assertThat(preferences.isFcpInterfaceActive(), is(false)); + } + + @Test + public void preferencesRetainFcpFullAccessRequiredOfNo() { + preferences.setFcpFullAccessRequired(NO); + assertThat(preferences.getFcpFullAccessRequired(), is(NO)); + verifyFullAccessRequiredChangedEvent(NO); + } + + private void verifyFullAccessRequiredChangedEvent( + FullAccessRequired fullAccessRequired) { + ArgumentCaptor fullAccessRequiredCaptor = + ArgumentCaptor.forClass(FullAccessRequiredChanged.class); + verify(eventBus).post(fullAccessRequiredCaptor.capture()); + assertThat( + fullAccessRequiredCaptor.getValue().getFullAccessRequired(), + is(fullAccessRequired)); + } + + @Test + public void preferencesRetainFcpFullAccessRequiredOfWriting() { + preferences.setFcpFullAccessRequired(WRITING); + assertThat(preferences.getFcpFullAccessRequired(), is(WRITING)); + verifyFullAccessRequiredChangedEvent(WRITING); + } + + @Test + public void preferencesRetainFcpFullAccessRequiredOfAlways() { + preferences.setFcpFullAccessRequired(ALWAYS); + assertThat(preferences.getFcpFullAccessRequired(), is(ALWAYS)); + verifyFullAccessRequiredChangedEvent(ALWAYS); + } + + @Test + public void preferencesReturnDefaultValueWhenFcpFullAccessRequiredIsSetToNull() { + preferences.setFcpFullAccessRequired(null); + assertThat(preferences.getFcpFullAccessRequired(), is(ALWAYS)); + verifyFullAccessRequiredChangedEvent(ALWAYS); + } + + @Test + public void preferencesStartWithFcpFullAccessRequiredDefaultValue() { + assertThat(preferences.getFcpFullAccessRequired(), is(ALWAYS)); + } + +} diff --git a/src/test/java/net/pterodactylus/sone/core/SoneChangeDetectorTest.java b/src/test/java/net/pterodactylus/sone/core/SoneChangeDetectorTest.java new file mode 100644 index 0000000..f070eff --- /dev/null +++ b/src/test/java/net/pterodactylus/sone/core/SoneChangeDetectorTest.java @@ -0,0 +1,108 @@ +package net.pterodactylus.sone.core; + +import static java.util.Arrays.asList; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.HashSet; + +import net.pterodactylus.sone.core.SoneChangeDetector.PostProcessor; +import net.pterodactylus.sone.core.SoneChangeDetector.PostReplyProcessor; +import net.pterodactylus.sone.data.Post; +import net.pterodactylus.sone.data.PostReply; +import net.pterodactylus.sone.data.Sone; + +import org.junit.Before; +import org.junit.Test; + +/** + * Unit test for {@link SoneChangeDetector}. + * + * @author David ‘Bombe’ Roden + */ +public class SoneChangeDetectorTest { + + private final Sone oldSone = mock(Sone.class); + private final Sone newSone = mock(Sone.class); + private final SoneChangeDetector soneChangeDetector = + new SoneChangeDetector(oldSone); + private final Post oldPost = mock(Post.class); + private final Post removedPost = mock(Post.class); + private final Post newPost = mock(Post.class); + private final PostProcessor newPostProcessor = mock(PostProcessor.class); + private final PostProcessor removedPostProcessor = + mock(PostProcessor.class); + private final PostReply oldPostReply = mock(PostReply.class); + private final PostReply removedPostReply = mock(PostReply.class); + private final PostReply newPostReply = mock(PostReply.class); + private final PostReplyProcessor newPostReplyProcessor = + mock(PostReplyProcessor.class); + private final PostReplyProcessor removedPostReplyProcessor = + mock(PostReplyProcessor.class); + + @Before + public void setupPosts() { + when(oldSone.getPosts()).thenReturn(asList(oldPost, removedPost)); + when(newSone.getPosts()).thenReturn(asList(oldPost, newPost)); + } + + @Before + public void setupPostProcessors() { + soneChangeDetector.onNewPosts(newPostProcessor); + soneChangeDetector.onRemovedPosts(removedPostProcessor); + } + + @Before + public void setupPostReplies() { + when(oldSone.getReplies()).thenReturn( + new HashSet( + asList(oldPostReply, removedPostReply))); + when(newSone.getReplies()).thenReturn( + new HashSet(asList(oldPostReply, newPostReply))); + } + + @Before + public void setupPostReplyProcessors() { + soneChangeDetector.onNewPostReplies(newPostReplyProcessor); + soneChangeDetector.onRemovedPostReplies(removedPostReplyProcessor); + } + + @Test + public void changeDetectorDetectsChanges() { + soneChangeDetector.detectChanges(newSone); + + verify(newPostProcessor).processPost(newPost); + verify(newPostProcessor, never()).processPost(oldPost); + verify(newPostProcessor, never()).processPost(removedPost); + verify(removedPostProcessor).processPost(removedPost); + verify(removedPostProcessor, never()).processPost(oldPost); + verify(removedPostProcessor, never()).processPost(newPost); + + verify(newPostReplyProcessor).processPostReply(newPostReply); + verify(newPostReplyProcessor, never()).processPostReply(oldPostReply); + verify(newPostReplyProcessor, never()).processPostReply( + removedPostReply); + verify(removedPostReplyProcessor).processPostReply(removedPostReply); + verify(removedPostReplyProcessor, never()).processPostReply( + oldPostReply); + verify(removedPostReplyProcessor, never()).processPostReply( + newPostReply); + } + + @Test + public void changeDetectorDoesNotNotifyAnyProcessorIfProcessorsUnset() { + soneChangeDetector.onNewPosts(null); + soneChangeDetector.onRemovedPosts(null); + soneChangeDetector.onNewPostReplies(null); + soneChangeDetector.onRemovedPostReplies(null); + soneChangeDetector.detectChanges(newSone); + verify(newPostProcessor, never()).processPost(any(Post.class)); + verify(removedPostProcessor, never()).processPost(any(Post.class)); + verify(newPostReplyProcessor, never()).processPostReply(any(PostReply.class)); + verify(removedPostReplyProcessor, never()).processPostReply(any(PostReply.class)); + } + +} diff --git a/src/test/java/net/pterodactylus/sone/core/SoneDownloaderTest.java b/src/test/java/net/pterodactylus/sone/core/SoneDownloaderTest.java new file mode 100644 index 0000000..33c8ead --- /dev/null +++ b/src/test/java/net/pterodactylus/sone/core/SoneDownloaderTest.java @@ -0,0 +1,196 @@ +package net.pterodactylus.sone.core; + +import static freenet.keys.InsertableClientSSK.createRandom; +import static java.lang.System.currentTimeMillis; +import static java.util.concurrent.TimeUnit.DAYS; +import static net.pterodactylus.sone.data.Sone.SoneStatus.downloading; +import static net.pterodactylus.sone.data.Sone.SoneStatus.idle; +import static net.pterodactylus.sone.data.Sone.SoneStatus.unknown; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.mockito.ArgumentCaptor.forClass; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.io.InputStream; + +import net.pterodactylus.sone.core.FreenetInterface.Fetched; +import net.pterodactylus.sone.data.Sone; +import net.pterodactylus.sone.data.Sone.SoneStatus; +import net.pterodactylus.sone.freenet.wot.Identity; + +import freenet.client.ClientMetadata; +import freenet.client.FetchResult; +import freenet.client.async.USKCallback; +import freenet.crypt.DummyRandomSource; +import freenet.keys.FreenetURI; +import freenet.keys.InsertableClientSSK; +import freenet.support.api.Bucket; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +/** + * Unit test for {@link SoneDownloaderImpl} and its subclasses. + * + * @author David ‘Bombe’ Roden + */ +public class SoneDownloaderTest { + + private final Core core = mock(Core.class); + private final FreenetInterface freenetInterface = mock(FreenetInterface.class); + private final SoneParser soneParser = mock(SoneParser.class); + private final SoneDownloaderImpl soneDownloader = new SoneDownloaderImpl(core, freenetInterface, soneParser); + private FreenetURI requestUri = mock(FreenetURI.class); + private Sone sone = mock(Sone.class); + + @Before + public void setupSone() { + Sone sone = SoneDownloaderTest.this.sone; + Identity identity = mock(Identity.class); + InsertableClientSSK clientSSK = createRandom(new DummyRandomSource(), "WoT"); + when(identity.getRequestUri()).thenReturn(clientSSK.getURI().toString()); + when(identity.getId()).thenReturn("identity"); + when(sone.getId()).thenReturn("identity"); + when(sone.getIdentity()).thenReturn(identity); + requestUri = clientSSK.getURI().setKeyType("USK").setDocName("Sone"); + when(sone.getRequestUri()).thenAnswer(new Answer() { + @Override + public FreenetURI answer(InvocationOnMock invocation) + throws Throwable { + return requestUri; + } + }); + when(sone.getTime()).thenReturn(currentTimeMillis() - DAYS.toMillis(1)); + } + + private void setupSoneAsUnknown() { + when(sone.getTime()).thenReturn(0L); + } + + @Test + public void addingASoneWillRegisterItsKey() { + soneDownloader.addSone(sone); + verify(freenetInterface).registerActiveUsk(eq(sone.getRequestUri()), any( + USKCallback.class)); + verify(freenetInterface, never()).unregisterUsk(sone); + } + + @Test + public void addingASoneTwiceWillAlsoDeregisterItsKey() { + soneDownloader.addSone(sone); + soneDownloader.addSone(sone); + verify(freenetInterface, times(2)).registerActiveUsk(eq( + sone.getRequestUri()), any(USKCallback.class)); + verify(freenetInterface).unregisterUsk(sone); + } + + + @Test + public void stoppingTheSoneDownloaderUnregistersTheSone() { + soneDownloader.addSone(sone); + soneDownloader.stop(); + verify(freenetInterface).unregisterUsk(sone); + } + + @Test + public void notBeingAbleToFetchAnUnknownSoneDoesNotUpdateCore() { + FreenetURI finalRequestUri = requestUri.sskForUSK() + .setMetaString(new String[] { "sone.xml" }); + setupSoneAsUnknown(); + soneDownloader.fetchSoneAction(sone).run(); + verify(freenetInterface).fetchUri(finalRequestUri); + verifyThatSoneStatusWasChangedToDownloadingAndBackTo(unknown); + verify(core, never()).updateSone(any(Sone.class)); + } + + private void verifyThatSoneStatusWasChangedToDownloadingAndBackTo(SoneStatus soneStatus) { + ArgumentCaptor soneStatuses = forClass(SoneStatus.class); + verify(sone, times(2)).setStatus(soneStatuses.capture()); + assertThat(soneStatuses.getAllValues().get(0), is(downloading)); + assertThat(soneStatuses.getAllValues().get(1), is(soneStatus)); + } + + @Test + public void notBeingAbleToFetchAKnownSoneDoesNotUpdateCore() { + FreenetURI finalRequestUri = requestUri.sskForUSK() + .setMetaString(new String[] { "sone.xml" }); + soneDownloader.fetchSoneAction(sone).run(); + verify(freenetInterface).fetchUri(finalRequestUri); + verifyThatSoneStatusWasChangedToDownloadingAndBackTo(idle); + verify(core, never()).updateSone(any(Sone.class)); + } + + @Test(expected = NullPointerException.class) + public void exceptionWhileFetchingAnUnknownSoneDoesNotUpdateCore() { + FreenetURI finalRequestUri = requestUri.sskForUSK() + .setMetaString(new String[] { "sone.xml" }); + setupSoneAsUnknown(); + when(freenetInterface.fetchUri(finalRequestUri)).thenThrow(NullPointerException.class); + try { + soneDownloader.fetchSoneAction(sone).run(); + } finally { + verify(freenetInterface).fetchUri(finalRequestUri); + verifyThatSoneStatusWasChangedToDownloadingAndBackTo(unknown); + verify(core, never()).updateSone(any(Sone.class)); + } + } + + @Test(expected = NullPointerException.class) + public void exceptionWhileFetchingAKnownSoneDoesNotUpdateCore() { + FreenetURI finalRequestUri = requestUri.sskForUSK() + .setMetaString(new String[] { "sone.xml" }); + when(freenetInterface.fetchUri(finalRequestUri)).thenThrow( NullPointerException.class); + try { + soneDownloader.fetchSoneAction(sone).run(); + } finally { + verify(freenetInterface).fetchUri(finalRequestUri); + verifyThatSoneStatusWasChangedToDownloadingAndBackTo(idle); + verify(core, never()).updateSone(any(Sone.class)); + } + } + + @Test + public void fetchingSoneWithInvalidXmlWillNotUpdateTheCore() throws IOException { + final Fetched fetchResult = createFetchResult(requestUri, getClass().getResourceAsStream("sone-parser-not-xml.xml")); + when(freenetInterface.fetchUri(requestUri)).thenReturn(fetchResult); + soneDownloader.fetchSoneAction(sone).run(); + verify(core, never()).updateSone(any(Sone.class)); + } + + @Test + public void exceptionWhileFetchingSoneWillNotUpdateTheCore() throws IOException { + final Fetched fetchResult = createFetchResult(requestUri, getClass().getResourceAsStream("sone-parser-no-payload.xml")); + when(core.soneBuilder()).thenReturn(null); + when(freenetInterface.fetchUri(requestUri)).thenReturn(fetchResult); + soneDownloader.fetchSoneAction(sone).run(); + verify(core, never()).updateSone(any(Sone.class)); + } + + @Test + public void onlyFetchingASoneWillNotUpdateTheCore() throws IOException { + final Fetched fetchResult = createFetchResult(requestUri, getClass().getResourceAsStream("sone-parser-no-payload.xml")); + when(freenetInterface.fetchUri(requestUri)).thenReturn(fetchResult); + soneDownloader.fetchSone(sone, sone.getRequestUri(), true); + verify(core, never()).updateSone(any(Sone.class)); + verifyThatSoneStatusWasChangedToDownloadingAndBackTo(idle); + } + + private Fetched createFetchResult(FreenetURI uri, InputStream inputStream) throws IOException { + ClientMetadata clientMetadata = new ClientMetadata("application/xml"); + Bucket bucket = mock(Bucket.class); + when(bucket.getInputStream()).thenReturn(inputStream); + FetchResult fetchResult = new FetchResult(clientMetadata, bucket); + return new Fetched(uri, fetchResult); + } + +} diff --git a/src/test/java/net/pterodactylus/sone/core/SoneInserterTest.java b/src/test/java/net/pterodactylus/sone/core/SoneInserterTest.java new file mode 100644 index 0000000..552746e --- /dev/null +++ b/src/test/java/net/pterodactylus/sone/core/SoneInserterTest.java @@ -0,0 +1,289 @@ +package net.pterodactylus.sone.core; + +import static com.google.common.base.Optional.of; +import static com.google.common.io.ByteStreams.toByteArray; +import static com.google.common.util.concurrent.MoreExecutors.sameThreadExecutor; +import static java.lang.System.currentTimeMillis; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.argThat; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import net.pterodactylus.sone.core.SoneInserter.ManifestCreator; +import net.pterodactylus.sone.core.event.InsertionDelayChangedEvent; +import net.pterodactylus.sone.core.event.SoneEvent; +import net.pterodactylus.sone.core.event.SoneInsertAbortedEvent; +import net.pterodactylus.sone.core.event.SoneInsertedEvent; +import net.pterodactylus.sone.core.event.SoneInsertingEvent; +import net.pterodactylus.sone.data.Album; +import net.pterodactylus.sone.data.Sone; +import net.pterodactylus.sone.main.SonePlugin; + +import freenet.keys.FreenetURI; +import freenet.support.api.ManifestElement; + +import com.google.common.base.Charsets; +import com.google.common.base.Optional; +import com.google.common.eventbus.AsyncEventBus; +import com.google.common.eventbus.EventBus; +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +/** + * Unit test for {@link SoneInserter} and its subclasses. + * + * @author David ‘Bombe’ Roden + */ +public class SoneInserterTest { + + private final Core core = mock(Core.class); + private final EventBus eventBus = mock(EventBus.class); + private final FreenetInterface freenetInterface = mock(FreenetInterface.class); + + @Before + public void setupCore() { + UpdateChecker updateChecker = mock(UpdateChecker.class); + when(core.getUpdateChecker()).thenReturn(updateChecker); + when(core.getSone(anyString())).thenReturn(Optional.absent()); + } + + @Test + public void insertionDelayIsForwardedToSoneInserter() { + EventBus eventBus = new AsyncEventBus(sameThreadExecutor()); + eventBus.register(new SoneInserter(core, eventBus, freenetInterface, "SoneId")); + eventBus.post(new InsertionDelayChangedEvent(15)); + assertThat(SoneInserter.getInsertionDelay().get(), is(15)); + } + + private Sone createSone(FreenetURI insertUri, String fingerprint) { + Sone sone = mock(Sone.class); + when(sone.getInsertUri()).thenReturn(insertUri); + when(sone.getFingerprint()).thenReturn(fingerprint); + when(sone.getRootAlbum()).thenReturn(mock(Album.class)); + when(core.getSone(anyString())).thenReturn(of(sone)); + return sone; + } + + @Test + public void isModifiedIsTrueIfModificationDetectorSaysSo() { + SoneModificationDetector soneModificationDetector = mock(SoneModificationDetector.class); + when(soneModificationDetector.isModified()).thenReturn(true); + SoneInserter soneInserter = new SoneInserter(core, eventBus, freenetInterface, "SoneId", soneModificationDetector, 1); + assertThat(soneInserter.isModified(), is(true)); + } + + @Test + public void isModifiedIsFalseIfModificationDetectorSaysSo() { + SoneModificationDetector soneModificationDetector = mock(SoneModificationDetector.class); + SoneInserter soneInserter = new SoneInserter(core, eventBus, freenetInterface, "SoneId", soneModificationDetector, 1); + assertThat(soneInserter.isModified(), is(false)); + } + + @Test + public void lastFingerprintIsStoredCorrectly() { + SoneInserter soneInserter = new SoneInserter(core, eventBus, freenetInterface, "SoneId"); + soneInserter.setLastInsertFingerprint("last-fingerprint"); + assertThat(soneInserter.getLastInsertFingerprint(), is("last-fingerprint")); + } + + @Test + public void soneInserterStopsWhenItShould() { + SoneInserter soneInserter = new SoneInserter(core, eventBus, freenetInterface, "SoneId"); + soneInserter.stop(); + soneInserter.serviceRun(); + } + + @Test + public void soneInserterInsertsASoneIfItIsEligible() throws SoneException { + FreenetURI insertUri = mock(FreenetURI.class); + final FreenetURI finalUri = mock(FreenetURI.class); + String fingerprint = "fingerprint"; + Sone sone = createSone(insertUri, fingerprint); + SoneModificationDetector soneModificationDetector = mock(SoneModificationDetector.class); + when(soneModificationDetector.isEligibleForInsert()).thenReturn(true); + when(freenetInterface.insertDirectory(eq(insertUri), any(HashMap.class), eq("index.html"))).thenReturn(finalUri); + final SoneInserter soneInserter = new SoneInserter(core, eventBus, freenetInterface, "SoneId", soneModificationDetector, 1); + doAnswer(new Answer() { + @Override + public Void answer(InvocationOnMock invocation) throws Throwable { + soneInserter.stop(); + return null; + } + }).when(core).touchConfiguration(); + soneInserter.serviceRun(); + ArgumentCaptor soneEvents = ArgumentCaptor.forClass(SoneEvent.class); + verify(freenetInterface).insertDirectory(eq(insertUri), any(HashMap.class), eq("index.html")); + verify(eventBus, times(2)).post(soneEvents.capture()); + assertThat(soneEvents.getAllValues().get(0), instanceOf(SoneInsertingEvent.class)); + assertThat(soneEvents.getAllValues().get(0).sone(), is(sone)); + assertThat(soneEvents.getAllValues().get(1), instanceOf(SoneInsertedEvent.class)); + assertThat(soneEvents.getAllValues().get(1).sone(), is(sone)); + } + + @Test + public void soneInserterBailsOutIfItIsStoppedWhileInserting() throws SoneException { + FreenetURI insertUri = mock(FreenetURI.class); + final FreenetURI finalUri = mock(FreenetURI.class); + String fingerprint = "fingerprint"; + Sone sone = createSone(insertUri, fingerprint); + SoneModificationDetector soneModificationDetector = mock(SoneModificationDetector.class); + when(soneModificationDetector.isEligibleForInsert()).thenReturn(true); + final SoneInserter soneInserter = new SoneInserter(core, eventBus, freenetInterface, "SoneId", soneModificationDetector, 1); + when(freenetInterface.insertDirectory(eq(insertUri), any(HashMap.class), eq("index.html"))).thenAnswer(new Answer() { + @Override + public FreenetURI answer(InvocationOnMock invocation) throws Throwable { + soneInserter.stop(); + return finalUri; + } + }); + soneInserter.serviceRun(); + ArgumentCaptor soneEvents = ArgumentCaptor.forClass(SoneEvent.class); + verify(freenetInterface).insertDirectory(eq(insertUri), any(HashMap.class), eq("index.html")); + verify(eventBus, times(2)).post(soneEvents.capture()); + assertThat(soneEvents.getAllValues().get(0), instanceOf(SoneInsertingEvent.class)); + assertThat(soneEvents.getAllValues().get(0).sone(), is(sone)); + assertThat(soneEvents.getAllValues().get(1), instanceOf(SoneInsertedEvent.class)); + assertThat(soneEvents.getAllValues().get(1).sone(), is(sone)); + verify(core, never()).touchConfiguration(); + } + + @Test + public void soneInserterDoesNotInsertSoneIfItIsNotEligible() throws SoneException { + FreenetURI insertUri = mock(FreenetURI.class); + String fingerprint = "fingerprint"; + Sone sone = createSone(insertUri, fingerprint); + SoneModificationDetector soneModificationDetector = mock(SoneModificationDetector.class); + final SoneInserter soneInserter = new SoneInserter(core, eventBus, freenetInterface, "SoneId", soneModificationDetector, 1); + new Thread(new Runnable() { + @Override + public void run() { + try { + Thread.sleep(500); + } catch (InterruptedException ie1) { + throw new RuntimeException(ie1); + } + soneInserter.stop(); + } + }).start(); + soneInserter.serviceRun(); + verify(freenetInterface, never()).insertDirectory(eq(insertUri), any(HashMap.class), eq("index.html")); + verify(eventBus, never()).post(argThat(org.hamcrest.Matchers.any(SoneEvent.class))); + } + + @Test + public void soneInserterPostsAbortedEventIfAnExceptionOccurs() throws SoneException { + FreenetURI insertUri = mock(FreenetURI.class); + String fingerprint = "fingerprint"; + Sone sone = createSone(insertUri, fingerprint); + SoneModificationDetector soneModificationDetector = mock(SoneModificationDetector.class); + when(soneModificationDetector.isEligibleForInsert()).thenReturn(true); + final SoneInserter soneInserter = new SoneInserter(core, eventBus, freenetInterface, "SoneId", soneModificationDetector, 1); + final SoneException soneException = new SoneException(new Exception()); + when(freenetInterface.insertDirectory(eq(insertUri), any(HashMap.class), eq("index.html"))).thenAnswer(new Answer() { + @Override + public FreenetURI answer(InvocationOnMock invocation) throws Throwable { + soneInserter.stop(); + throw soneException; + } + }); + soneInserter.serviceRun(); + ArgumentCaptor soneEvents = ArgumentCaptor.forClass(SoneEvent.class); + verify(freenetInterface).insertDirectory(eq(insertUri), any(HashMap.class), eq("index.html")); + verify(eventBus, times(2)).post(soneEvents.capture()); + assertThat(soneEvents.getAllValues().get(0), instanceOf(SoneInsertingEvent.class)); + assertThat(soneEvents.getAllValues().get(0).sone(), is(sone)); + assertThat(soneEvents.getAllValues().get(1), instanceOf(SoneInsertAbortedEvent.class)); + assertThat(soneEvents.getAllValues().get(1).sone(), is(sone)); + verify(core, never()).touchConfiguration(); + } + + @Test + public void soneInserterExitsIfSoneIsUnknown() { + SoneModificationDetector soneModificationDetector = + mock(SoneModificationDetector.class); + SoneInserter soneInserter = + new SoneInserter(core, eventBus, freenetInterface, "SoneId", + soneModificationDetector, 1); + when(soneModificationDetector.isEligibleForInsert()).thenReturn(true); + when(core.getSone("SoneId")).thenReturn(Optional.absent()); + soneInserter.serviceRun(); + } + + @Test + public void soneInserterCatchesExceptionAndContinues() { + SoneModificationDetector soneModificationDetector = + mock(SoneModificationDetector.class); + final SoneInserter soneInserter = + new SoneInserter(core, eventBus, freenetInterface, "SoneId", + soneModificationDetector, 1); + Answer> stopInserterAndThrowException = + new Answer>() { + @Override + public Optional answer( + InvocationOnMock invocation) { + soneInserter.stop(); + throw new NullPointerException(); + } + }; + when(soneModificationDetector.isEligibleForInsert()).thenAnswer( + stopInserterAndThrowException); + soneInserter.serviceRun(); + } + + @Test + public void templateIsRenderedCorrectlyForManifestElement() + throws IOException { + Map soneProperties = new HashMap(); + soneProperties.put("id", "SoneId"); + ManifestCreator manifestCreator = new ManifestCreator(core, soneProperties); + long now = currentTimeMillis(); + when(core.getStartupTime()).thenReturn(now); + ManifestElement manifestElement = manifestCreator.createManifestElement("test.txt", "plain/text; charset=utf-8", "sone-inserter-manifest.txt"); + assertThat(manifestElement.getName(), is("test.txt")); + assertThat(manifestElement.getMimeTypeOverride(), is("plain/text; charset=utf-8")); + String templateContent = new String(toByteArray(manifestElement.getData().getInputStream()), Charsets.UTF_8); + assertThat(templateContent, containsString("Sone Version: " + SonePlugin.VERSION.toString() + "\n")); + assertThat(templateContent, containsString("Core Startup: " + now + "\n")); + assertThat(templateContent, containsString("Sone ID: " + "SoneId" + "\n")); + } + + @Test + public void invalidTemplateReturnsANullManifestElement() { + Map soneProperties = new HashMap(); + ManifestCreator manifestCreator = new ManifestCreator(core, soneProperties); + assertThat(manifestCreator.createManifestElement("test.txt", + "plain/text; charset=utf-8", + "sone-inserter-invalid-manifest.txt"), + nullValue()); + } + + @Test + public void errorWhileRenderingTemplateReturnsANullManifestElement() { + Map soneProperties = new HashMap(); + ManifestCreator manifestCreator = new ManifestCreator(core, soneProperties); + when(core.toString()).thenThrow(NullPointerException.class); + assertThat(manifestCreator.createManifestElement("test.txt", + "plain/text; charset=utf-8", + "sone-inserter-faulty-manifest.txt"), + nullValue()); + } + +} diff --git a/src/test/java/net/pterodactylus/sone/core/SoneModificationDetectorTest.java b/src/test/java/net/pterodactylus/sone/core/SoneModificationDetectorTest.java new file mode 100644 index 0000000..d70cc5c --- /dev/null +++ b/src/test/java/net/pterodactylus/sone/core/SoneModificationDetectorTest.java @@ -0,0 +1,155 @@ +package net.pterodactylus.sone.core; + +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.concurrent.atomic.AtomicInteger; + +import net.pterodactylus.sone.core.SoneModificationDetector.LockableFingerprintProvider; +import net.pterodactylus.sone.data.Sone; + +import com.google.common.base.Ticker; +import org.junit.Before; +import org.junit.Test; + +/** + * Unit test for {@link SoneModificationDetector}. + * + * @author David ‘Bombe’ Roden + */ +public class SoneModificationDetectorTest { + + private final Ticker ticker = mock(Ticker.class); + private final AtomicInteger insertionDelay = new AtomicInteger(60); + private final SoneModificationDetector soneModificationDetector; + private final LockableFingerprintProvider lockableFingerprintProvider = mock(LockableFingerprintProvider.class); + + public SoneModificationDetectorTest() { + when(lockableFingerprintProvider.getFingerprint()).thenReturn("original"); + when(lockableFingerprintProvider.isLocked()).thenReturn(false); + soneModificationDetector = new SoneModificationDetector(ticker, lockableFingerprintProvider, insertionDelay); + } + + private void modifySone() { + modifySone(""); + } + + private void modifySone(String uniqueValue) { + when(lockableFingerprintProvider.getFingerprint()).thenReturn("modified" + uniqueValue); + } + + private void passTime(int seconds) { + when(ticker.read()).thenReturn(SECONDS.toNanos(seconds)); + } + + private void lockSone() { + when(lockableFingerprintProvider.isLocked()).thenReturn(true); + } + + private void unlockSone() { + when(lockableFingerprintProvider.isLocked()).thenReturn(false); + } + + @Before + public void setupOriginalFingerprint() { + soneModificationDetector.setFingerprint("original"); + } + + @Test + public void normalConstructorCanBeCalled() { + new SoneModificationDetector(lockableFingerprintProvider, insertionDelay); + } + + @Test + public void sonesStartOutAsNotEligible() { + assertThat(soneModificationDetector.isModified(), is(false)); + assertThat(soneModificationDetector.isEligibleForInsert(), is(false)); + } + + @Test + public void originalFingerprintIsRetained() { + assertThat(soneModificationDetector.getOriginalFingerprint(), is("original")); + } + + @Test + public void modifiedSoneIsEligibleAfter60Seconds() { + modifySone(); + assertThat(soneModificationDetector.isModified(), is(true)); + assertThat(soneModificationDetector.isEligibleForInsert(), is(false)); + passTime(100); + assertThat(soneModificationDetector.isModified(), is(true)); + assertThat(soneModificationDetector.isEligibleForInsert(), is(true)); + } + + @Test + public void modifiedAndRemodifiedSoneIsEligibleAfter90Seconds() { + modifySone(); + assertThat(soneModificationDetector.isModified(), is(true)); + assertThat(soneModificationDetector.isEligibleForInsert(), is(false)); + passTime(30); + modifySone("2"); + assertThat(soneModificationDetector.isModified(), is(true)); + assertThat(soneModificationDetector.isEligibleForInsert(), is(false)); + passTime(61); + assertThat(soneModificationDetector.isModified(), is(true)); + assertThat(soneModificationDetector.isEligibleForInsert(), is(false)); + passTime(91); + assertThat(soneModificationDetector.isModified(), is(true)); + assertThat(soneModificationDetector.isEligibleForInsert(), is(true)); + } + + @Test + public void modifiedSoneIsNotEligibleAfter30Seconds() { + modifySone(); + passTime(30); + assertThat(soneModificationDetector.isEligibleForInsert(), is(false)); + } + + @Test + public void lockedAndModifiedSoneIsNotEligibleAfter60Seconds() { + lockSone(); + assertThat(soneModificationDetector.isEligibleForInsert(), is(false)); + modifySone(); + assertThat(soneModificationDetector.isEligibleForInsert(), is(false)); + passTime(100); + assertThat(soneModificationDetector.isEligibleForInsert(), is(false)); + } + + @Test + public void lockingAndUnlockingASoneRestartsTheWaitPeriod() { + modifySone(); + lockSone(); + passTime(30); + assertThat(soneModificationDetector.isEligibleForInsert(), is(false)); + unlockSone(); + assertThat(soneModificationDetector.isEligibleForInsert(), is(false)); + passTime(60); + assertThat(soneModificationDetector.isEligibleForInsert(), is(false)); + passTime(90); + assertThat(soneModificationDetector.isEligibleForInsert(), is(true)); + } + + @Test + public void settingFingerprintWillResetTheEligibility() { + modifySone(); + assertThat(soneModificationDetector.isEligibleForInsert(), is(false)); + passTime(100); + assertThat(soneModificationDetector.isEligibleForInsert(), is(true)); + soneModificationDetector.setFingerprint("modified"); + assertThat(soneModificationDetector.isEligibleForInsert(), is(false)); + } + + @Test + public void changingInsertionDelayWillInfluenceEligibility() { + modifySone(); + assertThat(soneModificationDetector.isEligibleForInsert(), is(false)); + passTime(100); + assertThat(soneModificationDetector.isEligibleForInsert(), is(true)); + insertionDelay.set(120); + assertThat(soneModificationDetector.isEligibleForInsert(), is(false)); + } + +} diff --git a/src/test/java/net/pterodactylus/sone/core/SoneParserTest.java b/src/test/java/net/pterodactylus/sone/core/SoneParserTest.java new file mode 100644 index 0000000..dc19195 --- /dev/null +++ b/src/test/java/net/pterodactylus/sone/core/SoneParserTest.java @@ -0,0 +1,832 @@ +package net.pterodactylus.sone.core; + +import static com.google.common.base.Optional.of; +import static freenet.keys.InsertableClientSSK.createRandom; +import static java.lang.System.currentTimeMillis; +import static java.util.UUID.randomUUID; +import static java.util.concurrent.TimeUnit.DAYS; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyLong; +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.InputStream; +import java.net.MalformedURLException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import net.pterodactylus.sone.data.Album; +import net.pterodactylus.sone.data.Album.Modifier; +import net.pterodactylus.sone.data.Client; +import net.pterodactylus.sone.data.Image; +import net.pterodactylus.sone.data.Post; +import net.pterodactylus.sone.data.PostReply; +import net.pterodactylus.sone.data.Profile; +import net.pterodactylus.sone.data.Sone; +import net.pterodactylus.sone.database.AlbumBuilder; +import net.pterodactylus.sone.database.ImageBuilder; +import net.pterodactylus.sone.database.PostBuilder; +import net.pterodactylus.sone.database.PostReplyBuilder; +import net.pterodactylus.sone.database.SoneBuilder; +import net.pterodactylus.sone.database.memory.MemorySoneBuilder; +import net.pterodactylus.sone.freenet.wot.Identity; +import net.pterodactylus.sone.freenet.wot.OwnIdentity; + +import freenet.crypt.DummyRandomSource; +import freenet.keys.FreenetURI; +import freenet.keys.InsertableClientSSK; + +import com.google.common.base.Optional; +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.ListMultimap; +import org.junit.Before; +import org.junit.Test; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +/** + * Unit test for {@link SoneParser}. + * + * @author David ‘Bombe’ Roden + */ +public class SoneParserTest { + + private final Core core = mock(Core.class); + private final SoneParser soneParser = new SoneParser(core); + private final Sone sone = mock(Sone.class); + private FreenetURI requestUri = mock(FreenetURI.class); + private final PostBuilder postBuilder = mock(PostBuilder.class); + private final List createdPosts = new ArrayList(); + private Post post = mock(Post.class); + private final PostReplyBuilder postReplyBuilder = mock(PostReplyBuilder.class); + private final Set createdPostReplies = new HashSet(); + private PostReply postReply = mock(PostReply.class); + private final AlbumBuilder albumBuilder = mock(AlbumBuilder.class); + private final ListMultimap + nestedAlbums = ArrayListMultimap.create(); + private final ListMultimap albumImages = ArrayListMultimap.create(); + private Album album = mock(Album.class); + private final Map albums = new HashMap(); + private final ImageBuilder imageBuilder = mock(ImageBuilder.class); + private Image image = mock(Image.class); + private final Map images = new HashMap(); + + @Before + public void setupSone() { + setupSone(this.sone, Identity.class); + } + + private void setupSone(Sone sone, Class identityClass) { + Identity identity = mock(identityClass); + InsertableClientSSK clientSSK = + createRandom(new DummyRandomSource(), "WoT"); + when(identity.getRequestUri()).thenReturn(clientSSK.getURI().toString()); + when(identity.getId()).thenReturn("identity"); + when(sone.getId()).thenReturn("identity"); + when(sone.getIdentity()).thenReturn(identity); + requestUri = clientSSK.getURI().setKeyType("USK").setDocName("Sone"); + when(sone.getRequestUri()).thenAnswer(new Answer() { + @Override + public FreenetURI answer(InvocationOnMock invocation) + throws Throwable { + return requestUri; + } + }); + when(sone.getTime()) + .thenReturn(currentTimeMillis() - DAYS.toMillis(1)); + } + + @Before + public void setupSoneBuilder() { + when(core.soneBuilder()).thenAnswer(new Answer() { + @Override + public SoneBuilder answer(InvocationOnMock invocation) { + return new MemorySoneBuilder(null); + } + }); + } + + @Before + public void setupPost() { + when(post.getRecipientId()).thenReturn(Optional.absent()); + } + + @Before + public void setupPostBuilder() { + when(postBuilder.withId(anyString())).thenAnswer(new Answer() { + @Override + public PostBuilder answer(InvocationOnMock invocation) throws Throwable { + when(post.getId()).thenReturn((String) invocation.getArguments()[0]); + return postBuilder; + } + }); + when(postBuilder.from(anyString())).thenAnswer(new Answer() { + @Override + public PostBuilder answer(InvocationOnMock invocation) throws Throwable { + final Sone sone = mock(Sone.class); + when(sone.getId()).thenReturn((String) invocation.getArguments()[0]); + when(post.getSone()).thenReturn(sone); + return postBuilder; + } + }); + when(postBuilder.withTime(anyLong())).thenAnswer(new Answer() { + @Override + public PostBuilder answer(InvocationOnMock invocation) throws Throwable { + when(post.getTime()).thenReturn((Long) invocation.getArguments()[0]); + return postBuilder; + } + }); + when(postBuilder.withText(anyString())).thenAnswer(new Answer() { + @Override + public PostBuilder answer(InvocationOnMock invocation) throws Throwable { + when(post.getText()).thenReturn((String) invocation.getArguments()[0]); + return postBuilder; + } + }); + when(postBuilder.to(anyString())).thenAnswer(new Answer() { + @Override + public PostBuilder answer(InvocationOnMock invocation) throws Throwable { + when(post.getRecipientId()).thenReturn(of((String) invocation.getArguments()[0])); + return postBuilder; + } + }); + when(postBuilder.build()).thenAnswer(new Answer() { + @Override + public Post answer(InvocationOnMock invocation) throws Throwable { + Post post = SoneParserTest.this.post; + SoneParserTest.this.post = mock(Post.class); + setupPost(); + createdPosts.add(post); + return post; + } + }); + when(core.postBuilder()).thenReturn(postBuilder); + } + + @Before + public void setupPostReplyBuilder() { + when(postReplyBuilder.withId(anyString())).thenAnswer(new Answer() { + @Override + public PostReplyBuilder answer(InvocationOnMock invocation) throws Throwable { + when(postReply.getId()).thenReturn((String) invocation.getArguments()[0]); + return postReplyBuilder; + } + }); + when(postReplyBuilder.from(anyString())).thenAnswer( + new Answer() { + @Override + public PostReplyBuilder answer( + InvocationOnMock invocation) throws Throwable { + Sone sone = when(mock(Sone.class).getId()).thenReturn( + (String) invocation.getArguments()[0]) + .getMock(); + when(postReply.getSone()).thenReturn(sone); + return postReplyBuilder; + } + }); + when(postReplyBuilder.to(anyString())).thenAnswer( + new Answer() { + @Override + public PostReplyBuilder answer( + InvocationOnMock invocation) throws Throwable { + when(postReply.getPostId()).thenReturn( + (String) invocation.getArguments()[0]); + Post post = when(mock(Post.class).getId()).thenReturn( + (String) invocation.getArguments()[0]) + .getMock(); + when(postReply.getPost()).thenReturn(of(post)); + return postReplyBuilder; + } + }); + when(postReplyBuilder.withTime(anyLong())).thenAnswer( + new Answer() { + @Override + public PostReplyBuilder answer( + InvocationOnMock invocation) throws Throwable { + when(postReply.getTime()).thenReturn( + (Long) invocation.getArguments()[0]); + return postReplyBuilder; + } + }); + when(postReplyBuilder.withText(anyString())).thenAnswer(new Answer() { + @Override + public PostReplyBuilder answer(InvocationOnMock invocation) throws Throwable { + when(postReply.getText()).thenReturn((String) invocation.getArguments()[0]); + return postReplyBuilder; + } + }); + when(postReplyBuilder.build()).thenAnswer(new Answer() { + @Override + public PostReply answer(InvocationOnMock invocation) throws Throwable { + PostReply postReply = SoneParserTest.this.postReply; + createdPostReplies.add(postReply); + SoneParserTest.this.postReply = mock(PostReply.class); + return postReply; + } + }); + when(core.postReplyBuilder()).thenReturn(postReplyBuilder); + } + + @Before + public void setupAlbum() { + final Album album = SoneParserTest.this.album; + when(album.getAlbumImage()).thenReturn(mock(Image.class)); + doAnswer(new Answer() { + @Override + public Void answer(InvocationOnMock invocation) { + nestedAlbums.put(album, (Album) invocation.getArguments()[0]); + return null; + } + }).when(album).addAlbum(any(Album.class)); + doAnswer(new Answer() { + @Override + public Void answer(InvocationOnMock invocation) { + albumImages.put(album, (Image) invocation.getArguments()[0]); + return null; + } + }).when(album).addImage(any(Image.class)); + when(album.getAlbums()).thenAnswer(new Answer>() { + @Override + public List answer(InvocationOnMock invocation) { + return nestedAlbums.get(album); + } + }); + when(album.getImages()).thenAnswer(new Answer>() { + @Override + public List answer(InvocationOnMock invocation) { + return albumImages.get(album); + } + }); + final Modifier albumModifier = new Modifier() { + private String title = album.getTitle(); + private String description = album.getDescription(); + private String imageId = album.getAlbumImage().getId(); + + @Override + public Modifier setTitle(String title) { + this.title = title; + return this; + } + + @Override + public Modifier setDescription(String description) { + this.description = description; + return this; + } + + @Override + public Modifier setAlbumImage(String imageId) { + this.imageId = imageId; + return this; + } + + @Override + public Album update() throws IllegalStateException { + when(album.getTitle()).thenReturn(title); + when(album.getDescription()).thenReturn(description); + Image image = mock(Image.class); + when(image.getId()).thenReturn(imageId); + when(album.getAlbumImage()).thenReturn(image); + return album; + } + }; + when(album.modify()).thenReturn(albumModifier); + } + + @Before + public void setupAlbumBuilder() { + when(albumBuilder.withId(anyString())).thenAnswer(new Answer() { + @Override + public AlbumBuilder answer(InvocationOnMock invocation) { + when(album.getId()).thenReturn((String) invocation.getArguments()[0]); + return albumBuilder; + } + }); + when(albumBuilder.randomId()).thenAnswer(new Answer() { + @Override + public AlbumBuilder answer(InvocationOnMock invocation) { + when(album.getId()).thenReturn(randomUUID().toString()); + return albumBuilder; + } + }); + when(albumBuilder.by(any(Sone.class))).thenAnswer(new Answer() { + @Override + public AlbumBuilder answer(InvocationOnMock invocation) { + when(album.getSone()).thenReturn((Sone) invocation.getArguments()[0]); + return albumBuilder; + } + }); + when(albumBuilder.build()).thenAnswer(new Answer() { + @Override + public Album answer(InvocationOnMock invocation) { + Album album = SoneParserTest.this.album; + albums.put(album.getId(), album); + SoneParserTest.this.album = mock(Album.class); + setupAlbum(); + return album; + } + }); + when(core.albumBuilder()).thenReturn(albumBuilder); + } + + @Before + public void setupAlbums() { + when(core.getAlbum(anyString())).thenAnswer(new Answer() { + @Override + public Album answer(InvocationOnMock invocation) + throws Throwable { + return albums.get(invocation.getArguments()[0]); + } + }); + } + + @Before + public void setupImage() { + final Image image = SoneParserTest.this.image; + Image.Modifier modifier = new Image.Modifier() { + private Sone sone = image.getSone(); + private long creationTime = image.getCreationTime(); + private String key = image.getKey(); + private String title = image.getTitle(); + private String description = image.getDescription(); + private int width = image.getWidth(); + private int height = image.getHeight(); + + @Override + public Image.Modifier setSone(Sone sone) { + this.sone = sone; + return this; + } + + @Override + public Image.Modifier setCreationTime(long creationTime) { + this.creationTime = creationTime; + return this; + } + + @Override + public Image.Modifier setKey(String key) { + this.key = key; + return this; + } + + @Override + public Image.Modifier setTitle(String title) { + this.title = title; + return this; + } + + @Override + public Image.Modifier setDescription(String description) { + this.description = description; + return this; + } + + @Override + public Image.Modifier setWidth(int width) { + this.width = width; + return this; + } + + @Override + public Image.Modifier setHeight(int height) { + this.height = height; + return this; + } + + @Override + public Image update() throws IllegalStateException { + when(image.getSone()).thenReturn(sone); + when(image.getCreationTime()).thenReturn(creationTime); + when(image.getKey()).thenReturn(key); + when(image.getTitle()).thenReturn(title); + when(image.getDescription()).thenReturn(description); + when(image.getWidth()).thenReturn(width); + when(image.getHeight()).thenReturn(height); + return image; + } + }; + when(image.getSone()).thenReturn(sone); + when(image.modify()).thenReturn(modifier); + } + + @Before + public void setupImageBuilder() { + when(imageBuilder.randomId()).thenAnswer(new Answer() { + @Override + public ImageBuilder answer(InvocationOnMock invocation) { + when(image.getId()).thenReturn(randomUUID().toString()); + return imageBuilder; + } + }); + when(imageBuilder.withId(anyString())).thenAnswer(new Answer() { + @Override + public ImageBuilder answer(InvocationOnMock invocation) { + when(image.getId()).thenReturn( + (String) invocation.getArguments()[0]); + return imageBuilder; + } + }); + when(imageBuilder.build()).thenAnswer(new Answer() { + @Override + public Image answer(InvocationOnMock invocation) { + Image image = SoneParserTest.this.image; + images.put(image.getId(), image); + SoneParserTest.this.image = mock(Image.class); + setupImage(); + return image; + } + }); + when(core.imageBuilder()).thenReturn(imageBuilder); + } + + @Before + public void setupImages() { + when(core.getImage(anyString())).thenAnswer(new Answer() { + @Override + public Image answer(InvocationOnMock invocation) + throws Throwable { + return images.get(invocation.getArguments()[0]); + } + }); + } + @Test + public void parsingASoneFailsWhenDocumentIsNotXml() throws SoneException { + InputStream inputStream = getClass().getResourceAsStream("sone-parser-not-xml.xml"); + assertThat(soneParser.parseSone(sone, inputStream), nullValue()); + } + + @Test + public void parsingASoneFailsWhenDocumentHasNegativeProtocolVersion() throws SoneException { + InputStream inputStream = getClass().getResourceAsStream("sone-parser-negative-protocol-version.xml"); + assertThat(soneParser.parseSone(sone, inputStream), nullValue()); + } + + @Test + public void parsingASoneFailsWhenProtocolVersionIsTooLarge() throws SoneException { + InputStream inputStream = getClass().getResourceAsStream("sone-parser-too-large-protocol-version.xml"); + assertThat(soneParser.parseSone(sone, inputStream), nullValue()); + } + + @Test + public void parsingASoneFailsWhenThereIsNoTime() throws SoneException { + InputStream inputStream = getClass().getResourceAsStream("sone-parser-no-time.xml"); + assertThat(soneParser.parseSone(sone, inputStream), nullValue()); + } + + @Test + public void parsingASoneFailsWhenTimeIsNotNumeric() throws SoneException { + InputStream inputStream = getClass().getResourceAsStream("sone-parser-time-not-numeric.xml"); + assertThat(soneParser.parseSone(sone, inputStream), nullValue()); + } + + @Test + public void parsingASoneFailsWhenProfileIsMissing() throws SoneException { + InputStream inputStream = getClass().getResourceAsStream("sone-parser-no-profile.xml"); + assertThat(soneParser.parseSone(sone, inputStream), nullValue()); + } + + @Test + public void parsingASoneFailsWhenProfileFieldIsMissingAFieldName() throws SoneException { + InputStream inputStream = getClass().getResourceAsStream("sone-parser-profile-missing-field-name.xml"); + assertThat(soneParser.parseSone(sone, inputStream), nullValue()); + } + + @Test + public void parsingASoneFailsWhenProfileFieldNameIsEmpty() throws SoneException { + InputStream inputStream = getClass().getResourceAsStream("sone-parser-profile-empty-field-name.xml"); + assertThat(soneParser.parseSone(sone, inputStream), nullValue()); + } + + @Test + public void parsingASoneFailsWhenProfileFieldNameIsNotUnique() throws SoneException { + InputStream inputStream = getClass().getResourceAsStream("sone-parser-profile-duplicate-field-name.xml"); + assertThat(soneParser.parseSone(sone, inputStream), nullValue()); + } + + @Test + public void parsingASoneSucceedsWithoutPayload() throws SoneException { + InputStream inputStream = getClass().getResourceAsStream("sone-parser-no-payload.xml"); + assertThat(soneParser.parseSone(sone, inputStream).getTime(), is( + 1407197508000L)); + } + + @Test + public void parsingALocalSoneSucceedsWithoutPayload() throws SoneException { + InputStream inputStream = getClass().getResourceAsStream("sone-parser-no-payload.xml"); + Sone localSone = mock(Sone.class); + setupSone(localSone, OwnIdentity.class); + when(localSone.isLocal()).thenReturn(true); + Sone parsedSone = soneParser.parseSone(localSone, inputStream); + assertThat(parsedSone.getTime(), is(1407197508000L)); + assertThat(parsedSone.isLocal(), is(true)); + } + + @Test + public void parsingASoneSucceedsWithoutProtocolVersion() throws SoneException { + InputStream inputStream = getClass().getResourceAsStream("sone-parser-missing-protocol-version.xml"); + assertThat(soneParser.parseSone(sone, inputStream), not( + nullValue())); + } + + @Test + public void parsingASoneFailsWithMissingClientName() throws SoneException { + InputStream inputStream = getClass().getResourceAsStream("sone-parser-missing-client-name.xml"); + assertThat(soneParser.parseSone(sone, inputStream), nullValue()); + } + + @Test + public void parsingASoneFailsWithMissingClientVersion() throws SoneException { + InputStream inputStream = getClass().getResourceAsStream("sone-parser-missing-client-version.xml"); + assertThat(soneParser.parseSone(sone, inputStream), nullValue()); + } + + @Test + public void parsingASoneSucceedsWithClientInfo() throws SoneException { + InputStream inputStream = getClass().getResourceAsStream("sone-parser-with-client-info.xml"); + assertThat(soneParser.parseSone(sone, inputStream).getClient(), is(new Client("some-client", "some-version"))); + } + + @Test + public void parsingASoneSucceedsWithProfile() throws SoneException, + MalformedURLException { + InputStream inputStream = getClass().getResourceAsStream("sone-parser-with-profile.xml"); + final Profile profile = soneParser.parseSone(sone, inputStream).getProfile(); + assertThat(profile.getFirstName(), is("first")); + assertThat(profile.getMiddleName(), is("middle")); + assertThat(profile.getLastName(), is("last")); + assertThat(profile.getBirthDay(), is(18)); + assertThat(profile.getBirthMonth(), is(12)); + assertThat(profile.getBirthYear(), is(1976)); + } + + @Test + public void parsingASoneSucceedsWithoutProfileFields() throws SoneException, MalformedURLException { + InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-fields.xml"); + assertThat(soneParser.parseSone(sone, inputStream), notNullValue()); + } + + @Test + public void parsingASoneFailsWithoutPostId() throws SoneException, MalformedURLException { + InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-post-id.xml"); + assertThat(soneParser.parseSone(sone, inputStream), nullValue()); + } + + @Test + public void parsingASoneFailsWithoutPostTime() throws SoneException, MalformedURLException { + InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-post-time.xml"); + assertThat(soneParser.parseSone(sone, inputStream), nullValue()); + } + + @Test + public void parsingASoneFailsWithoutPostText() throws SoneException, MalformedURLException { + InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-post-text.xml"); + assertThat(soneParser.parseSone(sone, inputStream), nullValue()); + } + + @Test + public void parsingASoneFailsWithInvalidPostTime() throws SoneException, MalformedURLException { + InputStream inputStream = getClass().getResourceAsStream("sone-parser-with-invalid-post-time.xml"); + assertThat(soneParser.parseSone(sone, inputStream), nullValue()); + } + + @Test + public void parsingASoneSucceedsWithValidPostTime() throws SoneException, MalformedURLException { + InputStream inputStream = getClass().getResourceAsStream("sone-parser-with-valid-post-time.xml"); + final List posts = soneParser.parseSone(sone, inputStream).getPosts(); + assertThat(posts, is(createdPosts)); + assertThat(posts.get(0).getSone().getId(), is(sone.getId())); + assertThat(posts.get(0).getId(), is("post-id")); + assertThat(posts.get(0).getTime(), is(1407197508000L)); + assertThat(posts.get(0).getRecipientId(), is(Optional.absent())); + assertThat(posts.get(0).getText(), is("text")); + } + + @Test + public void parsingASoneSucceedsWithRecipient() throws SoneException, MalformedURLException { + InputStream inputStream = getClass().getResourceAsStream("sone-parser-with-recipient.xml"); + final List posts = soneParser.parseSone(sone, inputStream).getPosts(); + assertThat(posts, is(createdPosts)); + assertThat(posts.get(0).getSone().getId(), is(sone.getId())); + assertThat(posts.get(0).getId(), is("post-id")); + assertThat(posts.get(0).getTime(), is(1407197508000L)); + assertThat(posts.get(0).getRecipientId(), is(of( + "1234567890123456789012345678901234567890123"))); + assertThat(posts.get(0).getText(), is("text")); + } + + @Test + public void parsingASoneSucceedsWithInvalidRecipient() throws SoneException, MalformedURLException { + InputStream inputStream = getClass().getResourceAsStream("sone-parser-with-invalid-recipient.xml"); + final List posts = soneParser.parseSone(sone, inputStream).getPosts(); + assertThat(posts, is(createdPosts)); + assertThat(posts.get(0).getSone().getId(), is(sone.getId())); + assertThat(posts.get(0).getId(), is("post-id")); + assertThat(posts.get(0).getTime(), is(1407197508000L)); + assertThat(posts.get(0).getRecipientId(), is(Optional.absent())); + assertThat(posts.get(0).getText(), is("text")); + } + + @Test + public void parsingASoneFailsWithoutPostReplyId() throws SoneException, MalformedURLException { + InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-post-reply-id.xml"); + assertThat(soneParser.parseSone(sone, inputStream), nullValue()); + } + + @Test + public void parsingASoneFailsWithoutPostReplyPostId() throws SoneException, MalformedURLException { + InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-post-reply-post-id.xml"); + assertThat(soneParser.parseSone(sone, inputStream), nullValue()); + } + + @Test + public void parsingASoneFailsWithoutPostReplyTime() throws SoneException, MalformedURLException { + InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-post-reply-time.xml"); + assertThat(soneParser.parseSone(sone, inputStream), nullValue()); + } + + @Test + public void parsingASoneFailsWithoutPostReplyText() throws SoneException, MalformedURLException { + InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-post-reply-text.xml"); + assertThat(soneParser.parseSone(sone, inputStream), nullValue()); + } + + @Test + public void parsingASoneFailsWithInvalidPostReplyTime() throws SoneException, MalformedURLException { + InputStream inputStream = getClass().getResourceAsStream("sone-parser-with-invalid-post-reply-time.xml"); + assertThat(soneParser.parseSone(sone, inputStream), nullValue()); + } + + @Test + public void parsingASoneSucceedsWithValidPostReplyTime() throws SoneException, MalformedURLException { + InputStream inputStream = getClass().getResourceAsStream("sone-parser-with-valid-post-reply-time.xml"); + final Set postReplies = soneParser.parseSone(sone, inputStream).getReplies(); + assertThat(postReplies, is(createdPostReplies)); + PostReply postReply = createdPostReplies.iterator().next(); + assertThat(postReply.getId(), is("reply-id")); + assertThat(postReply.getPostId(), is("post-id")); + assertThat(postReply.getPost().get().getId(), is("post-id")); + assertThat(postReply.getSone().getId(), is("identity")); + assertThat(postReply.getTime(), is(1407197508000L)); + assertThat(postReply.getText(), is("reply-text")); + } + + @Test + public void parsingASoneSucceedsWithoutLikedPostIds() throws SoneException, MalformedURLException { + InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-liked-post-ids.xml"); + assertThat(soneParser.parseSone(sone, inputStream), not( + nullValue())); + } + + @Test + public void parsingASoneSucceedsWithLikedPostIds() throws SoneException, MalformedURLException { + InputStream inputStream = getClass().getResourceAsStream("sone-parser-with-liked-post-ids.xml"); + assertThat(soneParser.parseSone(sone, inputStream).getLikedPostIds(), is( + (Set) ImmutableSet.of("liked-post-id"))); + } + + @Test + public void parsingASoneSucceedsWithoutLikedPostReplyIds() throws SoneException, MalformedURLException { + InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-liked-post-reply-ids.xml"); + assertThat(soneParser.parseSone(sone, inputStream), not( + nullValue())); + } + + @Test + public void parsingASoneSucceedsWithLikedPostReplyIds() throws SoneException, MalformedURLException { + InputStream inputStream = getClass().getResourceAsStream("sone-parser-with-liked-post-reply-ids.xml"); + assertThat(soneParser.parseSone(sone, inputStream).getLikedReplyIds(), is( + (Set) ImmutableSet.of("liked-post-reply-id"))); + } + + @Test + public void parsingASoneSucceedsWithoutAlbums() throws SoneException, MalformedURLException { + InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-albums.xml"); + assertThat(soneParser.parseSone(sone, inputStream), not( + nullValue())); + } + + @Test + public void parsingASoneFailsWithoutAlbumId() throws SoneException, MalformedURLException { + InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-album-id.xml"); + assertThat(soneParser.parseSone(sone, inputStream), nullValue()); + } + + @Test + public void parsingASoneFailsWithoutAlbumTitle() throws SoneException, MalformedURLException { + InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-album-title.xml"); + assertThat(soneParser.parseSone(sone, inputStream), nullValue()); + } + + @Test + public void parsingASoneSucceedsWithNestedAlbums() throws SoneException, MalformedURLException { + InputStream inputStream = getClass().getResourceAsStream("sone-parser-with-multiple-albums.xml"); + final Sone parsedSone = soneParser.parseSone(sone, inputStream); + assertThat(parsedSone, not(nullValue())); + assertThat(parsedSone.getRootAlbum().getAlbums(), hasSize(1)); + Album album = parsedSone.getRootAlbum().getAlbums().get(0); + assertThat(album.getId(), is("album-id-1")); + assertThat(album.getTitle(), is("album-title")); + assertThat(album.getDescription(), is("album-description")); + assertThat(album.getAlbums(), hasSize(1)); + Album nestedAlbum = album.getAlbums().get(0); + assertThat(nestedAlbum.getId(), is("album-id-2")); + assertThat(nestedAlbum.getTitle(), is("album-title-2")); + assertThat(nestedAlbum.getDescription(), is("album-description-2")); + assertThat(nestedAlbum.getAlbums(), hasSize(0)); + } + + @Test + public void parsingASoneFailsWithInvalidParentAlbumId() throws SoneException, MalformedURLException { + InputStream inputStream = getClass().getResourceAsStream("sone-parser-with-invalid-parent-album-id.xml"); + assertThat(soneParser.parseSone(sone, inputStream), nullValue()); + } + + @Test + public void parsingASoneSucceedsWithoutImages() throws SoneException, MalformedURLException { + InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-images.xml"); + assertThat(soneParser.parseSone(sone, inputStream), not( + nullValue())); + } + + @Test + public void parsingASoneFailsWithoutImageId() throws SoneException, MalformedURLException { + InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-image-id.xml"); + assertThat(soneParser.parseSone(sone, inputStream), nullValue()); + } + + @Test + public void parsingASoneFailsWithoutImageTime() throws SoneException, MalformedURLException { + InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-image-time.xml"); + assertThat(soneParser.parseSone(sone, inputStream), nullValue()); + } + + @Test + public void parsingASoneFailsWithoutImageKey() throws SoneException, MalformedURLException { + InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-image-key.xml"); + assertThat(soneParser.parseSone(sone, inputStream), nullValue()); + } + + @Test + public void parsingASoneFailsWithoutImageTitle() throws SoneException, MalformedURLException { + InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-image-title.xml"); + assertThat(soneParser.parseSone(sone, inputStream), nullValue()); + } + + @Test + public void parsingASoneFailsWithoutImageWidth() throws SoneException, MalformedURLException { + InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-image-width.xml"); + assertThat(soneParser.parseSone(sone, inputStream), nullValue()); + } + + @Test + public void parsingASoneFailsWithoutImageHeight() throws SoneException, MalformedURLException { + InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-image-height.xml"); + assertThat(soneParser.parseSone(sone, inputStream), nullValue()); + } + + @Test + public void parsingASoneFailsWithInvalidImageWidth() throws SoneException, MalformedURLException { + InputStream inputStream = getClass().getResourceAsStream("sone-parser-with-invalid-image-width.xml"); + assertThat(soneParser.parseSone(sone, inputStream), nullValue()); + } + + @Test + public void parsingASoneFailsWithInvalidImageHeight() throws SoneException, MalformedURLException { + InputStream inputStream = getClass().getResourceAsStream("sone-parser-with-invalid-image-height.xml"); + assertThat(soneParser.parseSone(sone, inputStream), nullValue()); + } + + @Test + public void parsingASoneSucceedsWithImage() throws SoneException, MalformedURLException { + InputStream inputStream = getClass().getResourceAsStream("sone-parser-with-image.xml"); + final Sone sone = soneParser.parseSone(this.sone, inputStream); + assertThat(sone, not(nullValue())); + assertThat(sone.getRootAlbum().getAlbums(), hasSize(1)); + assertThat(sone.getRootAlbum().getAlbums().get(0).getImages(), hasSize(1)); + Image image = sone.getRootAlbum().getAlbums().get(0).getImages().get(0); + assertThat(image.getId(), is("image-id")); + assertThat(image.getCreationTime(), is(1407197508000L)); + assertThat(image.getKey(), is("KSK@GPLv3.txt")); + assertThat(image.getTitle(), is("image-title")); + assertThat(image.getDescription(), is("image-description")); + assertThat(image.getWidth(), is(1920)); + assertThat(image.getHeight(), is(1080)); + assertThat(sone.getProfile().getAvatar(), is("image-id")); + } + + +} diff --git a/src/test/java/net/pterodactylus/sone/core/SoneRescuerTest.java b/src/test/java/net/pterodactylus/sone/core/SoneRescuerTest.java new file mode 100644 index 0000000..ede6f13 --- /dev/null +++ b/src/test/java/net/pterodactylus/sone/core/SoneRescuerTest.java @@ -0,0 +1,136 @@ +package net.pterodactylus.sone.core; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import net.pterodactylus.sone.data.Sone; + +import freenet.keys.FreenetURI; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +/** + * Unit test for {@link SoneRescuer}. + * + * @author David ‘Bombe’ Roden + */ +public class SoneRescuerTest { + + private static final long CURRENT_EDITION = 12L; + private static final long SOME_OTHER_EDITION = 15L; + private final Core core = mock(Core.class); + private final SoneDownloader soneDownloader = mock(SoneDownloader.class); + private final Sone sone = mock(Sone.class); + private SoneRescuer soneRescuer; + + @Before + public void setupSone() { + FreenetURI soneUri = mock(FreenetURI.class); + when(soneUri.getEdition()).thenReturn(CURRENT_EDITION); + when(sone.getRequestUri()).thenReturn(soneUri); + } + + @Before + public void setupSoneRescuer() { + soneRescuer = new SoneRescuer(core, soneDownloader, sone); + } + + @Test + public void newSoneRescuerIsNotFetchingAnything() { + assertThat(soneRescuer.isFetching(), is(false)); + } + + @Test + public void newSoneRescuerStartsAtCurrentEditionOfSone() { + assertThat(soneRescuer.getCurrentEdition(), is(CURRENT_EDITION)); + } + + @Test + public void newSoneRescuerHasANextEditionToGet() { + assertThat(soneRescuer.hasNextEdition(), is(true)); + } + + @Test + public void soneRescuerDoesNotHaveANextEditionIfCurrentEditionIsZero() { + when(sone.getRequestUri().getEdition()).thenReturn(0L); + soneRescuer = new SoneRescuer(core, soneDownloader, sone); + assertThat(soneRescuer.hasNextEdition(), is(false)); + } + + @Test + public void nextEditionIsOneSmallerThanTheCurrentEdition() { + assertThat(soneRescuer.getNextEdition(), is(CURRENT_EDITION - 1)); + } + + @Test + public void currentEditionCanBeSet() { + soneRescuer.setEdition(SOME_OTHER_EDITION); + assertThat(soneRescuer.getCurrentEdition(), is(SOME_OTHER_EDITION)); + } + + @Test + public void lastFetchOfANewSoneRescuerWasSuccessful() { + assertThat(soneRescuer.isLastFetchSuccessful(), is(true)); + } + + @Test + public void mainLoopStopsWhenItShould() { + soneRescuer.stop(); + soneRescuer.serviceRun(); + } + + @Test + public void successfulInsert() { + final Sone fetchedSone = mock(Sone.class); + returnUriOnInsert(fetchedSone); + soneRescuer.startNextFetch(); + soneRescuer.serviceRun(); + verify(core).lockSone(eq(sone)); + verify(core).updateSone(eq(fetchedSone), eq(true)); + assertThat(soneRescuer.isLastFetchSuccessful(), is(true)); + assertThat(soneRescuer.isFetching(), is(false)); + } + + @Test + public void nonSuccessfulInsertIsRecognized() { + returnUriOnInsert(null); + soneRescuer.startNextFetch(); + soneRescuer.serviceRun(); + verify(core).lockSone(eq(sone)); + verify(core, never()).updateSone(any(Sone.class), eq(true)); + assertThat(soneRescuer.isLastFetchSuccessful(), is(false)); + assertThat(soneRescuer.isFetching(), is(false)); + } + + private void returnUriOnInsert(final Sone fetchedSone) { + FreenetURI keyWithMetaStrings = setupFreenetUri(); + doAnswer(new Answer() { + @Override + public Sone answer(InvocationOnMock invocation) throws Throwable { + soneRescuer.stop(); + return fetchedSone; + } + }).when(soneDownloader).fetchSone(eq(sone), eq(keyWithMetaStrings), eq(true)); + } + + private FreenetURI setupFreenetUri() { + FreenetURI sskKey = mock(FreenetURI.class); + FreenetURI keyWithDocName = mock(FreenetURI.class); + FreenetURI keyWithMetaStrings = mock(FreenetURI.class); + when(keyWithDocName.setMetaString(eq(new String[] { "sone.xml" }))).thenReturn(keyWithMetaStrings); + when(sskKey.setDocName(eq("Sone-" + CURRENT_EDITION))).thenReturn(keyWithDocName); + when(sone.getRequestUri().setKeyType(eq("SSK"))).thenReturn(sskKey); + return keyWithMetaStrings; + } + +} diff --git a/src/test/java/net/pterodactylus/sone/core/SoneUriTest.java b/src/test/java/net/pterodactylus/sone/core/SoneUriTest.java new file mode 100644 index 0000000..879da4a --- /dev/null +++ b/src/test/java/net/pterodactylus/sone/core/SoneUriTest.java @@ -0,0 +1,38 @@ +package net.pterodactylus.sone.core; + +import static freenet.keys.InsertableClientSSK.createRandom; +import static net.pterodactylus.sone.core.SoneUri.create; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; + +import freenet.crypt.DummyRandomSource; +import freenet.keys.FreenetURI; + +import org.junit.Test; + +/** + * Unit test for {@link SoneUri}. + * + * @author David ‘Bombe’ Roden + */ +public class SoneUriTest { + + @Test + public void callConstructorForIncreasedTestCoverage() { + new SoneUri(); + } + + @Test + public void returnedUriHasCorrectDocNameAndMetaStrings() { + FreenetURI uri = createRandom(new DummyRandomSource(), "test-0").getURI().uskForSSK(); + assertThat(create(uri.toString()).getDocName(), is("Sone")); + assertThat(create(uri.toString()).getAllMetaStrings(), is(new String[0])); + } + + @Test + public void malformedUriReturnsNull() { + assertThat(create("not a key"), nullValue()); + } + +} diff --git a/src/test/java/net/pterodactylus/sone/core/UpdateCheckerTest.java b/src/test/java/net/pterodactylus/sone/core/UpdateCheckerTest.java new file mode 100644 index 0000000..a5e3b2a --- /dev/null +++ b/src/test/java/net/pterodactylus/sone/core/UpdateCheckerTest.java @@ -0,0 +1,233 @@ +package net.pterodactylus.sone.core; + +import static java.lang.Long.MAX_VALUE; +import static net.pterodactylus.sone.main.SonePlugin.VERSION; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; +import static org.mockito.ArgumentCaptor.forClass; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.argThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.io.InputStream; + +import net.pterodactylus.sone.core.FreenetInterface.Callback; +import net.pterodactylus.sone.core.FreenetInterface.Fetched; +import net.pterodactylus.sone.core.event.UpdateFoundEvent; +import net.pterodactylus.util.version.Version; + +import freenet.client.ClientMetadata; +import freenet.client.FetchResult; +import freenet.keys.FreenetURI; +import freenet.support.api.Bucket; +import freenet.support.io.ArrayBucket; + +import com.google.common.eventbus.EventBus; +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +/** + * Unit test for {@link UpdateChecker}. + * + * @author David ‘Bombe’ Roden + */ +public class UpdateCheckerTest { + + private final EventBus eventBus = mock(EventBus.class); + private final FreenetInterface freenetInterface = mock(FreenetInterface.class); + private final UpdateChecker updateChecker = new UpdateChecker(eventBus, freenetInterface); + + @Before + public void startUpdateChecker() { + updateChecker.start(); + } + + @Test + public void newUpdateCheckerDoesNotHaveALatestVersion() { + assertThat(updateChecker.hasLatestVersion(), is(false)); + assertThat(updateChecker.getLatestVersion(), is(VERSION)); + } + + @Test + public void startingAnUpdateCheckerRegisterAUsk() { + verify(freenetInterface).registerUsk(any(FreenetURI.class), any(Callback.class)); + } + + @Test + public void stoppingAnUpdateCheckerUnregistersAUsk() { + updateChecker.stop(); + verify(freenetInterface).unregisterUsk(any(FreenetURI.class)); + } + + @Test + public void callbackDoesNotDownloadIfNewEditionIsNotFound() { + setupCallbackWithEdition(MAX_VALUE, false, false); + verify(freenetInterface, never()).fetchUri(any(FreenetURI.class)); + verify(eventBus, never()).post(argThat(instanceOf(UpdateFoundEvent.class))); + } + + private void setupCallbackWithEdition(long edition, boolean newKnownGood, boolean newSlot) { + ArgumentCaptor uri = forClass(FreenetURI.class); + ArgumentCaptor callback = forClass(Callback.class); + verify(freenetInterface).registerUsk(uri.capture(), callback.capture()); + callback.getValue().editionFound(uri.getValue(), edition, newKnownGood, newSlot); + } + + @Test + public void callbackStartsIfNewEditionIsFound() { + setupFetchResult(createFutureFetchResult()); + setupCallbackWithEdition(MAX_VALUE, true, false); + verifyAFreenetUriIsFetched(); + ArgumentCaptor updateFoundEvent = forClass(UpdateFoundEvent.class); + verify(eventBus, times(1)).post(updateFoundEvent.capture()); + assertThat(updateFoundEvent.getValue().version(), is(new Version(99, 0, 0))); + assertThat(updateFoundEvent.getValue().releaseTime(), is(11865368297000L)); + assertThat(updateChecker.getLatestVersion(), is(new Version(99, 0, 0))); + assertThat(updateChecker.getLatestVersionDate(), is(11865368297000L)); + assertThat(updateChecker.hasLatestVersion(), is(true)); + } + + private FetchResult createFutureFetchResult() { + ClientMetadata clientMetadata = new ClientMetadata("application/xml"); + Bucket fetched = new ArrayBucket(("# MapConfigurationBackendVersion=1\n" + + "CurrentVersion/Version: 99.0.0\n" + + "CurrentVersion/ReleaseTime: 11865368297000").getBytes()); + return new FetchResult(clientMetadata, fetched); + } + + @Test + public void callbackDoesNotStartIfNoNewEditionIsFound() { + setupFetchResult(createPastFetchResult()); + setupCallbackWithEdition(updateChecker.getLatestEdition(), true, false); + verifyAFreenetUriIsFetched(); + verifyNoUpdateFoundEventIsFired(); + } + + private void setupFetchResult(final FetchResult pastFetchResult) { + when(freenetInterface.fetchUri(any(FreenetURI.class))).thenAnswer(new Answer() { + @Override + public Fetched answer(InvocationOnMock invocation) throws Throwable { + FreenetURI freenetUri = (FreenetURI) invocation.getArguments()[0]; + return new Fetched(freenetUri, pastFetchResult); + } + }); + } + + private FetchResult createPastFetchResult() { + ClientMetadata clientMetadata = new ClientMetadata("application/xml"); + Bucket fetched = new ArrayBucket(("# MapConfigurationBackendVersion=1\n" + + "CurrentVersion/Version: 0.2\n" + + "CurrentVersion/ReleaseTime: 1289417883000").getBytes()); + return new FetchResult(clientMetadata, fetched); + } + + @Test + public void invalidUpdateFileDoesNotStartCallback() { + setupFetchResult(createInvalidFetchResult()); + setupCallbackWithEdition(MAX_VALUE, true, false); + verifyAFreenetUriIsFetched(); + verifyNoUpdateFoundEventIsFired(); + } + + private FetchResult createInvalidFetchResult() { + ClientMetadata clientMetadata = new ClientMetadata("text/plain"); + Bucket fetched = new ArrayBucket("Some other data.".getBytes()); + return new FetchResult(clientMetadata, fetched); + } + + @Test + public void nonExistingPropertiesWillNotCauseUpdateToBeFound() { + setupCallbackWithEdition(MAX_VALUE, true, false); + verifyAFreenetUriIsFetched(); + verifyNoUpdateFoundEventIsFired(); + } + + private void verifyNoUpdateFoundEventIsFired() { + verify(eventBus, never()).post(any(UpdateFoundEvent.class)); + } + + private void verifyAFreenetUriIsFetched() { + verify(freenetInterface).fetchUri(any(FreenetURI.class)); + } + + @Test + public void brokenBucketDoesNotCauseUpdateToBeFound() { + setupFetchResult(createBrokenBucketFetchResult()); + setupCallbackWithEdition(MAX_VALUE, true, false); + verifyAFreenetUriIsFetched(); + verifyNoUpdateFoundEventIsFired(); + } + + private FetchResult createBrokenBucketFetchResult() { + ClientMetadata clientMetadata = new ClientMetadata("text/plain"); + Bucket fetched = new ArrayBucket("Some other data.".getBytes()) { + @Override + public InputStream getInputStream() { + try { + return when(mock(InputStream.class).read()).thenThrow(IOException.class).getMock(); + } catch (IOException ioe1) { + /* won’t throw here. */ + return null; + } + } + }; + return new FetchResult(clientMetadata, fetched); + } + + @Test + public void invalidTimeDoesNotCauseAnUpdateToBeFound() { + setupFetchResult(createInvalidTimeFetchResult()); + setupCallbackWithEdition(MAX_VALUE, true, false); + verifyAFreenetUriIsFetched(); + verifyNoUpdateFoundEventIsFired(); + } + + private FetchResult createInvalidTimeFetchResult() { + ClientMetadata clientMetadata = new ClientMetadata("application/xml"); + Bucket fetched = new ArrayBucket(("# MapConfigurationBackendVersion=1\n" + + "CurrentVersion/Version: 0.2\n" + + "CurrentVersion/ReleaseTime: invalid").getBytes()); + return new FetchResult(clientMetadata, fetched); + } + + @Test + public void invalidPropertiesDoesNotCauseAnUpdateToBeFound() { + setupFetchResult(createMissingTimeFetchResult()); + setupCallbackWithEdition(MAX_VALUE, true, false); + verifyAFreenetUriIsFetched(); + verifyNoUpdateFoundEventIsFired(); + } + + private FetchResult createMissingTimeFetchResult() { + ClientMetadata clientMetadata = new ClientMetadata("application/xml"); + Bucket fetched = new ArrayBucket(("# MapConfigurationBackendVersion=1\n" + + "CurrentVersion/Version: 0.2\n").getBytes()); + return new FetchResult(clientMetadata, fetched); + } + + @Test + public void invalidVersionDoesNotCauseAnUpdateToBeFound() { + setupFetchResult(createInvalidVersionFetchResult()); + setupCallbackWithEdition(MAX_VALUE, true, false); + verifyAFreenetUriIsFetched(); + verifyNoUpdateFoundEventIsFired(); + } + + private FetchResult createInvalidVersionFetchResult() { + ClientMetadata clientMetadata = new ClientMetadata("application/xml"); + Bucket fetched = new ArrayBucket(("# MapConfigurationBackendVersion=1\n" + + "CurrentVersion/Version: foo\n" + + "CurrentVersion/ReleaseTime: 1289417883000").getBytes()); + return new FetchResult(clientMetadata, fetched); + } + +} diff --git a/src/test/java/net/pterodactylus/sone/core/WebOfTrustUpdaterTest.java b/src/test/java/net/pterodactylus/sone/core/WebOfTrustUpdaterTest.java new file mode 100644 index 0000000..aa810ee --- /dev/null +++ b/src/test/java/net/pterodactylus/sone/core/WebOfTrustUpdaterTest.java @@ -0,0 +1,453 @@ +package net.pterodactylus.sone.core; + +import static java.lang.Thread.sleep; +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.concurrent.CountDownLatch; + +import net.pterodactylus.sone.core.WebOfTrustUpdaterImpl.AddContextJob; +import net.pterodactylus.sone.core.WebOfTrustUpdaterImpl.RemoveContextJob; +import net.pterodactylus.sone.core.WebOfTrustUpdaterImpl.SetPropertyJob; +import net.pterodactylus.sone.core.WebOfTrustUpdaterImpl.SetTrustJob; +import net.pterodactylus.sone.core.WebOfTrustUpdaterImpl.WebOfTrustContextUpdateJob; +import net.pterodactylus.sone.core.WebOfTrustUpdaterImpl.WebOfTrustUpdateJob; +import net.pterodactylus.sone.freenet.plugin.PluginException; +import net.pterodactylus.sone.freenet.wot.Identity; +import net.pterodactylus.sone.freenet.wot.OwnIdentity; +import net.pterodactylus.sone.freenet.wot.Trust; +import net.pterodactylus.sone.freenet.wot.WebOfTrustConnector; +import net.pterodactylus.sone.freenet.wot.WebOfTrustException; + +import org.junit.Test; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +/** + * Unit test for {@link WebOfTrustUpdaterImpl} and its subclasses. + * + * @author David ‘Bombe’ Roden + */ +public class WebOfTrustUpdaterTest { + + private static final String CONTEXT = "test-context"; + private static final Integer SCORE = 50; + private static final Integer OTHER_SCORE = 25; + private static final String TRUST_COMMENT = "set in a test"; + private static final String PROPERTY_NAME = "test-property"; + private final WebOfTrustConnector webOfTrustConnector = mock(WebOfTrustConnector.class); + private final WebOfTrustUpdaterImpl webOfTrustUpdater = new WebOfTrustUpdaterImpl(webOfTrustConnector); + private final OwnIdentity ownIdentity = when(mock(OwnIdentity.class).getId()).thenReturn("own-identity-id").getMock(); + private final WebOfTrustUpdateJob successfulWebOfTrustUpdateJob = createWebOfTrustUpdateJob(true); + private final WebOfTrustUpdateJob failingWebOfTrustUpdateJob = createWebOfTrustUpdateJob(false); + private final WebOfTrustContextUpdateJob contextUpdateJob = webOfTrustUpdater.new WebOfTrustContextUpdateJob(ownIdentity, CONTEXT); + private final AddContextJob addContextJob = webOfTrustUpdater.new AddContextJob(ownIdentity, CONTEXT); + private final RemoveContextJob removeContextJob = webOfTrustUpdater.new RemoveContextJob(ownIdentity, CONTEXT); + private final Identity trustee = when(mock(Identity.class).getId()).thenReturn("trustee-id").getMock(); + + private WebOfTrustUpdateJob createWebOfTrustUpdateJob(final boolean success) { + return webOfTrustUpdater.new WebOfTrustUpdateJob() { + @Override + public void run() { + super.run(); + try { + sleep(100); + } catch (InterruptedException ie1) { + throw new RuntimeException(ie1); + } + finish(success); + } + }; + } + + @Test + public void webOfTrustUpdateJobWaitsUntilFinishedHasBeenCalledAndReturnsSuccess() throws InterruptedException { + new Thread(successfulWebOfTrustUpdateJob).start(); + assertThat(successfulWebOfTrustUpdateJob.waitForCompletion(), is(true)); + } + + @Test + public void webOfTrustUpdateJobWaitsUntilFinishedHasBeenCalledAndReturnsFailure() throws InterruptedException { + new Thread(failingWebOfTrustUpdateJob).start(); + assertThat(failingWebOfTrustUpdateJob.waitForCompletion(), is(false)); + } + + @Test + public void webOfTrustContextUpdateJobsAreEqualIfTheirClassOwnIdentityAndContextAreEqual() { + WebOfTrustContextUpdateJob secondContextUpdateJob = webOfTrustUpdater.new WebOfTrustContextUpdateJob(ownIdentity, CONTEXT); + assertThat(contextUpdateJob.equals(secondContextUpdateJob), is(true)); + assertThat(secondContextUpdateJob.equals(contextUpdateJob), is(true)); + assertThat(contextUpdateJob.hashCode(), is(secondContextUpdateJob.hashCode())); + } + + @Test + public void webOfTrustContextUpdatesJobsAreNotEqualIfTheirClassDiffers() { + assertThat(contextUpdateJob.equals(addContextJob), is(false)); + } + + @Test + public void webOfTrustContextUpdateJobToStringContainsIdentityAndContext() { + assertThat(contextUpdateJob.toString(), containsString(ownIdentity.toString())); + assertThat(contextUpdateJob.toString(), containsString(CONTEXT)); + } + + @Test + public void webOfTrustContextUpdateJobsAreNotEqualIfTheIdentitiesDiffer() { + OwnIdentity ownIdentity = mock(OwnIdentity.class); + WebOfTrustContextUpdateJob secondContextUpdateJob = webOfTrustUpdater.new WebOfTrustContextUpdateJob(ownIdentity, CONTEXT); + assertThat(contextUpdateJob.equals(secondContextUpdateJob), is(false)); + assertThat(secondContextUpdateJob.equals(contextUpdateJob), is(false)); + } + + @Test + public void webOfTrustContextUpdateJobsAreNotEqualIfTheirContextsDiffer() { + WebOfTrustContextUpdateJob secondContextUpdateJob = webOfTrustUpdater.new WebOfTrustContextUpdateJob(ownIdentity, CONTEXT + CONTEXT); + assertThat(contextUpdateJob.equals(secondContextUpdateJob), is(false)); + assertThat(secondContextUpdateJob.equals(contextUpdateJob), is(false)); + } + + @Test + public void webOfTrustContextUpdateJobsAreNotEqualToNull() { + assertThat(contextUpdateJob.equals(null), is(false)); + } + + @Test + public void addContextJobAddsTheContext() throws PluginException { + addContextJob.run(); + verify(webOfTrustConnector).addContext(eq(ownIdentity), eq(CONTEXT)); + verify(ownIdentity).addContext(eq(CONTEXT)); + assertThat(addContextJob.waitForCompletion(), is(true)); + } + + @Test + public void exceptionWhileAddingAContextIsExposed() throws PluginException { + doThrow(PluginException.class).when(webOfTrustConnector).addContext(eq(ownIdentity), eq(CONTEXT)); + addContextJob.run(); + verify(webOfTrustConnector).addContext(eq(ownIdentity), eq(CONTEXT)); + verify(ownIdentity, never()).addContext(eq(CONTEXT)); + assertThat(addContextJob.waitForCompletion(), is(false)); + } + + @Test + public void removeContextJobRemovesTheContext() throws PluginException { + removeContextJob.run(); + verify(webOfTrustConnector).removeContext(eq(ownIdentity), eq(CONTEXT)); + verify(ownIdentity).removeContext(eq(CONTEXT)); + assertThat(removeContextJob.waitForCompletion(), is(true)); + } + + @Test + public void exceptionWhileRemovingAContextIsExposed() throws PluginException { + doThrow(PluginException.class).when(webOfTrustConnector).removeContext(eq(ownIdentity), eq(CONTEXT)); + removeContextJob.run(); + verify(webOfTrustConnector).removeContext(eq(ownIdentity), eq(CONTEXT)); + verify(ownIdentity, never()).removeContext(eq(CONTEXT)); + assertThat(removeContextJob.waitForCompletion(), is(false)); + } + + @Test + public void settingAPropertySetsTheProperty() throws PluginException { + String propertyName = "property-name"; + String propertyValue = "property-value"; + SetPropertyJob setPropertyJob = webOfTrustUpdater.new SetPropertyJob(ownIdentity, propertyName, propertyValue); + setPropertyJob.run(); + verify(webOfTrustConnector).setProperty(eq(ownIdentity), eq(propertyName), eq(propertyValue)); + verify(ownIdentity).setProperty(eq(propertyName), eq(propertyValue)); + assertThat(setPropertyJob.waitForCompletion(), is(true)); + } + + @Test + public void settingAPropertyToNullRemovesTheProperty() throws PluginException { + String propertyName = "property-name"; + SetPropertyJob setPropertyJob = webOfTrustUpdater.new SetPropertyJob(ownIdentity, propertyName, null); + setPropertyJob.run(); + verify(webOfTrustConnector).removeProperty(eq(ownIdentity), eq(propertyName)); + verify(ownIdentity).removeProperty(eq(propertyName)); + assertThat(setPropertyJob.waitForCompletion(), is(true)); + } + + @Test + public void pluginExceptionWhileSettingAPropertyIsHandled() throws PluginException { + String propertyName = "property-name"; + String propertyValue = "property-value"; + doThrow(PluginException.class).when(webOfTrustConnector).setProperty(eq(ownIdentity), eq(propertyName), eq(propertyValue)); + SetPropertyJob setPropertyJob = webOfTrustUpdater.new SetPropertyJob(ownIdentity, propertyName, propertyValue); + setPropertyJob.run(); + verify(webOfTrustConnector).setProperty(eq(ownIdentity), eq(propertyName), eq(propertyValue)); + verify(ownIdentity, never()).setProperty(eq(propertyName), eq(propertyValue)); + assertThat(setPropertyJob.waitForCompletion(), is(false)); + } + + @Test + public void setPropertyJobsWithSameClassPropertyAndValueAreEqual() { + String propertyName = "property-name"; + String propertyValue = "property-value"; + SetPropertyJob firstSetPropertyJob = webOfTrustUpdater.new SetPropertyJob(ownIdentity, propertyName, propertyValue); + SetPropertyJob secondSetPropertyJob = webOfTrustUpdater.new SetPropertyJob(ownIdentity, propertyName, propertyValue); + assertThat(firstSetPropertyJob, is(secondSetPropertyJob)); + assertThat(secondSetPropertyJob, is(firstSetPropertyJob)); + assertThat(firstSetPropertyJob.hashCode(), is(secondSetPropertyJob.hashCode())); + } + + @Test + public void setPropertyJobsWithDifferentClassesAreNotEqual() { + String propertyName = "property-name"; + String propertyValue = "property-value"; + SetPropertyJob firstSetPropertyJob = webOfTrustUpdater.new SetPropertyJob(ownIdentity, propertyName, propertyValue); + SetPropertyJob secondSetPropertyJob = webOfTrustUpdater.new SetPropertyJob(ownIdentity, propertyName, propertyValue) { + }; + assertThat(firstSetPropertyJob, not(is(secondSetPropertyJob))); + } + + @Test + public void nullIsNotASetProjectJobEither() { + String propertyName = "property-name"; + String propertyValue = "property-value"; + SetPropertyJob setPropertyJob = webOfTrustUpdater.new SetPropertyJob(ownIdentity, propertyName, propertyValue); + assertThat(setPropertyJob, not(is((Object) null))); + } + + @Test + public void setPropertyJobsWithDifferentPropertiesAreNotEqual() { + String propertyName = "property-name"; + String propertyValue = "property-value"; + SetPropertyJob firstSetPropertyJob = webOfTrustUpdater.new SetPropertyJob(ownIdentity, propertyName, propertyValue); + SetPropertyJob secondSetPropertyJob = webOfTrustUpdater.new SetPropertyJob(ownIdentity, propertyName + "2", propertyValue); + assertThat(firstSetPropertyJob, not(is(secondSetPropertyJob))); + } + + @Test + public void setPropertyJobsWithDifferentOwnIdentitiesAreNotEqual() { + OwnIdentity otherOwnIdentity = mock(OwnIdentity.class); + String propertyName = "property-name"; + String propertyValue = "property-value"; + SetPropertyJob firstSetPropertyJob = webOfTrustUpdater.new SetPropertyJob(ownIdentity, propertyName, propertyValue); + SetPropertyJob secondSetPropertyJob = webOfTrustUpdater.new SetPropertyJob(otherOwnIdentity, propertyName, propertyValue); + assertThat(firstSetPropertyJob, not(is(secondSetPropertyJob))); + } + + @Test + public void setTrustJobSetsTrust() throws PluginException { + SetTrustJob setTrustJob = webOfTrustUpdater.new SetTrustJob(ownIdentity, trustee, SCORE, TRUST_COMMENT); + setTrustJob.run(); + verify(webOfTrustConnector).setTrust(eq(ownIdentity), eq(trustee), eq(SCORE), eq(TRUST_COMMENT)); + verify(trustee).setTrust(eq(ownIdentity), eq(new Trust(SCORE, null, 0))); + assertThat(setTrustJob.waitForCompletion(), is(true)); + } + + @Test + public void settingNullTrustRemovesTrust() throws WebOfTrustException { + SetTrustJob setTrustJob = webOfTrustUpdater.new SetTrustJob(ownIdentity, trustee, null, TRUST_COMMENT); + setTrustJob.run(); + verify(webOfTrustConnector).removeTrust(eq(ownIdentity), eq(trustee)); + verify(trustee).removeTrust(eq(ownIdentity)); + assertThat(setTrustJob.waitForCompletion(), is(true)); + } + + @Test + public void exceptionWhileSettingTrustIsCaught() throws PluginException { + doThrow(PluginException.class).when(webOfTrustConnector).setTrust(eq(ownIdentity), eq(trustee), eq(SCORE), eq(TRUST_COMMENT)); + SetTrustJob setTrustJob = webOfTrustUpdater.new SetTrustJob(ownIdentity, trustee, SCORE, TRUST_COMMENT); + setTrustJob.run(); + verify(webOfTrustConnector).setTrust(eq(ownIdentity), eq(trustee), eq(SCORE), eq(TRUST_COMMENT)); + verify(trustee, never()).setTrust(eq(ownIdentity), eq(new Trust(SCORE, null, 0))); + assertThat(setTrustJob.waitForCompletion(), is(false)); + } + + @Test + public void setTrustJobsWithDifferentClassesAreNotEqual() { + SetTrustJob firstSetTrustJob = webOfTrustUpdater.new SetTrustJob(ownIdentity, trustee, SCORE, TRUST_COMMENT); + SetTrustJob secondSetTrustJob = webOfTrustUpdater.new SetTrustJob(ownIdentity, trustee, SCORE, TRUST_COMMENT) { + }; + assertThat(firstSetTrustJob, not(is(secondSetTrustJob))); + assertThat(secondSetTrustJob, not(is(firstSetTrustJob))); + } + + @Test + public void setTrustJobsWithDifferentTrustersAreNotEqual() { + SetTrustJob firstSetTrustJob = webOfTrustUpdater.new SetTrustJob(ownIdentity, trustee, SCORE, TRUST_COMMENT); + SetTrustJob secondSetTrustJob = webOfTrustUpdater.new SetTrustJob(mock(OwnIdentity.class), trustee, SCORE, TRUST_COMMENT); + assertThat(firstSetTrustJob, not(is(secondSetTrustJob))); + assertThat(secondSetTrustJob, not(is(firstSetTrustJob))); + } + + @Test + public void setTrustJobsWithDifferentTrusteesAreNotEqual() { + SetTrustJob firstSetTrustJob = webOfTrustUpdater.new SetTrustJob(ownIdentity, trustee, SCORE, TRUST_COMMENT); + SetTrustJob secondSetTrustJob = webOfTrustUpdater.new SetTrustJob(ownIdentity, mock(Identity.class), SCORE, TRUST_COMMENT); + assertThat(firstSetTrustJob, not(is(secondSetTrustJob))); + assertThat(secondSetTrustJob, not(is(firstSetTrustJob))); + } + + @Test + public void setTrustJobsWithDifferentScoreAreEqual() { + SetTrustJob firstSetTrustJob = webOfTrustUpdater.new SetTrustJob(ownIdentity, trustee, SCORE, TRUST_COMMENT); + SetTrustJob secondSetTrustJob = webOfTrustUpdater.new SetTrustJob(ownIdentity, trustee, OTHER_SCORE, TRUST_COMMENT); + assertThat(firstSetTrustJob, is(secondSetTrustJob)); + assertThat(secondSetTrustJob, is(firstSetTrustJob)); + assertThat(firstSetTrustJob.hashCode(), is(secondSetTrustJob.hashCode())); + } + + @Test + public void setTrustJobDoesNotEqualNull() { + SetTrustJob setTrustJob = webOfTrustUpdater.new SetTrustJob(ownIdentity, trustee, SCORE, TRUST_COMMENT); + assertThat(setTrustJob, not(is((Object) null))); + } + + @Test + public void toStringOfSetTrustJobContainsIdsOfTrusterAndTrustee() { + SetTrustJob setTrustJob = webOfTrustUpdater.new SetTrustJob(ownIdentity, trustee, SCORE, TRUST_COMMENT); + assertThat(setTrustJob.toString(), containsString(ownIdentity.getId())); + assertThat(setTrustJob.toString(), containsString(trustee.getId())); + } + + @Test + public void webOfTrustUpdaterStopsWhenItShould() { + webOfTrustUpdater.stop(); + webOfTrustUpdater.serviceRun(); + } + + @Test + public void webOfTrustUpdaterStopsAfterItWasStarted() { + webOfTrustUpdater.start(); + webOfTrustUpdater.stop(); + } + + @Test + public void removePropertyRemovesProperty() throws InterruptedException, PluginException { + final CountDownLatch wotCallTriggered = new CountDownLatch(1); + doAnswer(new Answer() { + @Override + public Void answer(InvocationOnMock invocation) throws Throwable { + wotCallTriggered.countDown(); + return null; + } + }).when(webOfTrustConnector).removeProperty(eq(ownIdentity), eq(PROPERTY_NAME)); + webOfTrustUpdater.removeProperty(ownIdentity, PROPERTY_NAME); + webOfTrustUpdater.start(); + assertThat(wotCallTriggered.await(1, SECONDS), is(true)); + } + + @Test + public void multipleCallsToSetPropertyAreCollapsed() throws InterruptedException, PluginException { + final CountDownLatch wotCallTriggered = new CountDownLatch(1); + doAnswer(new Answer() { + @Override + public Void answer(InvocationOnMock invocation) throws Throwable { + wotCallTriggered.countDown(); + return null; + } + }).when(webOfTrustConnector).removeProperty(eq(ownIdentity), eq(PROPERTY_NAME)); + webOfTrustUpdater.removeProperty(ownIdentity, PROPERTY_NAME); + webOfTrustUpdater.removeProperty(ownIdentity, PROPERTY_NAME); + webOfTrustUpdater.start(); + assertThat(wotCallTriggered.await(1, SECONDS), is(true)); + verify(webOfTrustConnector).removeProperty(eq(ownIdentity), eq(PROPERTY_NAME)); + } + + @Test + public void addContextWaitWaitsForTheContextToBeAdded() { + webOfTrustUpdater.start(); + assertThat(webOfTrustUpdater.addContextWait(ownIdentity, CONTEXT), is(true)); + verify(ownIdentity).addContext(eq(CONTEXT)); + } + + @Test + public void removeContextRemovesAContext() throws InterruptedException, PluginException { + webOfTrustUpdater.start(); + final CountDownLatch removeContextTrigger = new CountDownLatch(1); + doAnswer(new Answer() { + @Override + public Void answer(InvocationOnMock invocation) throws Throwable { + removeContextTrigger.countDown(); + return null; + } + }).when(ownIdentity).removeContext(eq(CONTEXT)); + webOfTrustUpdater.removeContext(ownIdentity, CONTEXT); + removeContextTrigger.await(1, SECONDS); + verify(webOfTrustConnector).removeContext(eq(ownIdentity), eq(CONTEXT)); + verify(ownIdentity).removeContext(eq(CONTEXT)); + } + + @Test + public void removeContextRequestsAreCoalesced() throws InterruptedException, PluginException { + final CountDownLatch contextRemovedTrigger = new CountDownLatch(1); + doAnswer(new Answer() { + @Override + public Void answer(InvocationOnMock invocation) throws Throwable { + contextRemovedTrigger.countDown(); + return null; + } + }).when(ownIdentity).removeContext(eq(CONTEXT)); + for (int i = 1; i <= 2; i++) { + /* this is so fucking volatile. */ + if (i > 1) { + sleep(200); + } + new Thread(new Runnable() { + public void run() { + webOfTrustUpdater.removeContext(ownIdentity, CONTEXT); + } + }).start(); + } + webOfTrustUpdater.start(); + assertThat(contextRemovedTrigger.await(1, SECONDS), is(true)); + verify(webOfTrustConnector).removeContext(eq(ownIdentity), eq(CONTEXT)); + verify(ownIdentity).removeContext(eq(CONTEXT)); + } + + @Test + public void setTrustSetsTrust() throws InterruptedException, PluginException { + final CountDownLatch trustSetTrigger = new CountDownLatch(1); + doAnswer(new Answer() { + @Override + public Void answer(InvocationOnMock invocation) throws Throwable { + trustSetTrigger.countDown(); + return null; + } + }).when(trustee).setTrust(eq(ownIdentity), eq(new Trust(SCORE, null, 0))); + webOfTrustUpdater.start(); + webOfTrustUpdater.setTrust(ownIdentity, trustee, SCORE, TRUST_COMMENT); + assertThat(trustSetTrigger.await(1, SECONDS), is(true)); + verify(trustee).setTrust(eq(ownIdentity), eq(new Trust(SCORE, null, 0))); + verify(webOfTrustConnector).setTrust(eq(ownIdentity), eq(trustee), eq(SCORE), eq(TRUST_COMMENT)); + } + + @Test + public void setTrustRequestsAreCoalesced() throws InterruptedException, PluginException { + final CountDownLatch trustSetTrigger = new CountDownLatch(1); + doAnswer(new Answer() { + @Override + public Void answer(InvocationOnMock invocation) throws Throwable { + trustSetTrigger.countDown(); + return null; + } + }).when(trustee).setTrust(eq(ownIdentity), eq(new Trust(SCORE, null, 0))); + for (int i = 1; i <= 2; i++) { + /* this is so fucking volatile. */ + if (i > 1) { + sleep(200); + } + new Thread(new Runnable() { + public void run() { + webOfTrustUpdater.setTrust(ownIdentity, trustee, SCORE, TRUST_COMMENT); + } + }).start(); + } + webOfTrustUpdater.start(); + assertThat(trustSetTrigger.await(1, SECONDS), is(true)); + verify(trustee).setTrust(eq(ownIdentity), eq(new Trust(SCORE, null, 0))); + verify(webOfTrustConnector).setTrust(eq(ownIdentity), eq(trustee), eq(SCORE), eq(TRUST_COMMENT)); + } + +} diff --git a/src/test/java/net/pterodactylus/sone/data/ProfileTest.java b/src/test/java/net/pterodactylus/sone/data/ProfileTest.java new file mode 100644 index 0000000..b06e2f4 --- /dev/null +++ b/src/test/java/net/pterodactylus/sone/data/ProfileTest.java @@ -0,0 +1,26 @@ +package net.pterodactylus.sone.data; + +import net.pterodactylus.sone.data.Profile.Field; + +import org.hamcrest.MatcherAssert; +import org.hamcrest.Matchers; +import org.junit.Test; +import org.mockito.Mockito; + +/** + * Unit test for {@link Profile}. + * + * @author David ‘Bombe’ Roden + */ +public class ProfileTest { + + private final Sone sone = Mockito.mock(Sone.class); + private final Profile profile = new Profile(sone); + + @Test + public void newFieldsAreInitializedWithAnEmptyString() { + Field newField = profile.addField("testField"); + MatcherAssert.assertThat(newField.getValue(), Matchers.is("")); + } + +} diff --git a/src/test/java/net/pterodactylus/sone/data/impl/AbstractSoneBuilderTest.java b/src/test/java/net/pterodactylus/sone/data/impl/AbstractSoneBuilderTest.java new file mode 100644 index 0000000..b2d86dd --- /dev/null +++ b/src/test/java/net/pterodactylus/sone/data/impl/AbstractSoneBuilderTest.java @@ -0,0 +1,54 @@ +package net.pterodactylus.sone.data.impl; + +import static org.mockito.Mockito.mock; + +import net.pterodactylus.sone.data.Sone; +import net.pterodactylus.sone.freenet.wot.Identity; +import net.pterodactylus.sone.freenet.wot.OwnIdentity; + +import org.junit.Test; + +/** + * Unit test for {@link AbstractSoneBuilder}. + * + * @author David ‘Bombe’ Roden + */ +public class AbstractSoneBuilderTest { + + private final AbstractSoneBuilder soneBuilder = new AbstractSoneBuilder() { + @Override + public Sone build() throws IllegalStateException { + validate(); + return null; + } + }; + + @Test + public void localSoneIsValidated() { + Identity ownIdentity = mock(OwnIdentity.class); + soneBuilder.local().from(ownIdentity).build(); + } + + @Test(expected = IllegalStateException.class) + public void localSoneIsNotValidatedIfIdentityIsNotAnOwnIdentity() { + Identity identity = mock(Identity.class); + soneBuilder.local().from(identity).build(); + } + + @Test(expected = IllegalStateException.class) + public void localSoneIsNotValidatedIfIdentityIsNull() { + soneBuilder.local().build(); + } + + @Test + public void removeSoneIsValidate() { + Identity identity = mock(Identity.class); + soneBuilder.from(identity).build(); + } + + @Test(expected = IllegalStateException.class) + public void remoteSoneIsNotValidatedIfIdentityIsNull() { + soneBuilder.build(); + } + +} diff --git a/src/test/java/net/pterodactylus/sone/data/impl/ImageImplTest.java b/src/test/java/net/pterodactylus/sone/data/impl/ImageImplTest.java new file mode 100644 index 0000000..b78f0f6 --- /dev/null +++ b/src/test/java/net/pterodactylus/sone/data/impl/ImageImplTest.java @@ -0,0 +1,22 @@ +package net.pterodactylus.sone.data.impl; + +import net.pterodactylus.sone.data.Image; +import net.pterodactylus.sone.data.Image.Modifier.ImageTitleMustNotBeEmpty; + +import org.junit.Test; + +/** + * Unit test for {@link ImageImpl}. + * + * @author David ‘Bombe’ Roden + */ +public class ImageImplTest { + + private final Image image = new ImageImpl(); + + @Test(expected = ImageTitleMustNotBeEmpty.class) + public void modifierDoesNotAllowTitleDoBeEmpty() { + image.modify().setTitle("").update(); + } + +} diff --git a/src/test/java/net/pterodactylus/sone/database/memory/ConfigurationLoaderTest.java b/src/test/java/net/pterodactylus/sone/database/memory/ConfigurationLoaderTest.java new file mode 100644 index 0000000..47e26b7 --- /dev/null +++ b/src/test/java/net/pterodactylus/sone/database/memory/ConfigurationLoaderTest.java @@ -0,0 +1,84 @@ +package net.pterodactylus.sone.database.memory; + +import static java.util.Arrays.asList; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.nullValue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.HashSet; +import java.util.Set; + +import net.pterodactylus.sone.TestValue; +import net.pterodactylus.util.config.Configuration; +import net.pterodactylus.util.config.ConfigurationException; +import net.pterodactylus.util.config.Value; + +import org.junit.Test; + +/** + * Unit test for {@link ConfigurationLoader}. + * + * @author David ‘Bombe’ Roden + */ +public class ConfigurationLoaderTest { + + private final Configuration configuration = mock(Configuration.class); + private final ConfigurationLoader configurationLoader = + new ConfigurationLoader(configuration); + + @Test + public void loaderCanLoadKnownPosts() { + when(configuration.getStringValue("KnownPosts/0/ID")) + .thenReturn(TestValue.from("Post2")); + when(configuration.getStringValue("KnownPosts/1/ID")) + .thenReturn(TestValue.from("Post1")); + when(configuration.getStringValue("KnownPosts/2/ID")) + .thenReturn(TestValue.from(null)); + Set knownPosts = configurationLoader.loadKnownPosts(); + assertThat(knownPosts, containsInAnyOrder("Post1", "Post2")); + } + + @Test + public void loaderCanLoadKnownPostReplies() { + when(configuration.getStringValue("KnownReplies/0/ID")) + .thenReturn(TestValue.from("PostReply2")); + when(configuration.getStringValue("KnownReplies/1/ID")) + .thenReturn(TestValue.from("PostReply1")); + when(configuration.getStringValue("KnownReplies/2/ID")) + .thenReturn(TestValue.from(null)); + Set knownPosts = configurationLoader.loadKnownPostReplies(); + assertThat(knownPosts, + containsInAnyOrder("PostReply1", "PostReply2")); + } + + @Test + public void loaderCanLoadBookmarkedPosts() { + when(configuration.getStringValue("Bookmarks/Post/0/ID")) + .thenReturn(TestValue.from("Post2")); + when(configuration.getStringValue("Bookmarks/Post/1/ID")) + .thenReturn(TestValue.from("Post1")); + when(configuration.getStringValue("Bookmarks/Post/2/ID")) + .thenReturn(TestValue.from(null)); + Set knownPosts = configurationLoader.loadBookmarkedPosts(); + assertThat(knownPosts, containsInAnyOrder("Post1", "Post2")); + } + + @Test + public void loaderCanSaveBookmarkedPosts() throws ConfigurationException { + final Value post1 = TestValue.from(null); + final Value post2 = TestValue.from(null); + final Value post3 = TestValue.from(null); + when(configuration.getStringValue("Bookmarks/Post/0/ID")).thenReturn(post1); + when(configuration.getStringValue("Bookmarks/Post/1/ID")).thenReturn(post2); + when(configuration.getStringValue("Bookmarks/Post/2/ID")).thenReturn(post3); + HashSet originalPosts = new HashSet(asList("Post1", "Post2")); + configurationLoader.saveBookmarkedPosts(originalPosts); + HashSet extractedPosts = + new HashSet(asList(post1.getValue(), post2.getValue())); + assertThat(extractedPosts, containsInAnyOrder("Post1", "Post2")); + assertThat(post3.getValue(), nullValue()); + } + +} diff --git a/src/test/java/net/pterodactylus/sone/database/memory/MemoryBookmarkDatabaseTest.java b/src/test/java/net/pterodactylus/sone/database/memory/MemoryBookmarkDatabaseTest.java new file mode 100644 index 0000000..06c5b96 --- /dev/null +++ b/src/test/java/net/pterodactylus/sone/database/memory/MemoryBookmarkDatabaseTest.java @@ -0,0 +1,142 @@ +package net.pterodactylus.sone.database.memory; + +import static com.google.common.base.Optional.fromNullable; +import static net.pterodactylus.sone.Matchers.isPostWithId; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.is; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import net.pterodactylus.sone.data.Post; + +import com.google.common.base.Optional; +import org.junit.Before; +import org.junit.Test; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +/** + * Unit test for {@link MemoryBookmarkDatabase}. + * + * @author David ‘Bombe’ Roden + */ +public class MemoryBookmarkDatabaseTest { + + private final MemoryDatabase memoryDatabase = mock(MemoryDatabase.class); + private final ConfigurationLoader configurationLoader = + mock(ConfigurationLoader.class); + private final MemoryBookmarkDatabase bookmarkDatabase = + new MemoryBookmarkDatabase(memoryDatabase, configurationLoader); + private final Map posts = new HashMap(); + + @Before + public void setupMemoryDatabase() { + when(memoryDatabase.getPost(anyString())).thenAnswer( + new Answer>() { + @Override + public Optional answer( + InvocationOnMock invocation) { + return fromNullable( + posts.get(invocation.getArguments()[0])); + } + }); + } + + @Before + public void setupPosts() { + createAndRegisterPost("PostId1"); + createAndRegisterPost("PostId2"); + } + + private Post createAndRegisterPost(String postId) { + Post post = createPost(postId); + posts.put(postId, post); + return post; + } + + private Post createPost(String postId) { + Post post = mock(Post.class); + when(post.getId()).thenReturn(postId); + return post; + } + + @Test + public void bookmarkDatabaseRetainsBookmarkedPosts() { + Set allPosts = new HashSet(posts.values()); + for (Post post : allPosts) { + bookmarkDatabase.bookmarkPost(post); + } + assertThat(bookmarkDatabase.getBookmarkedPosts(), is(allPosts)); + for (Post post : allPosts) { + assertThat(bookmarkDatabase.isPostBookmarked(post), is(true)); + } + } + + @Test + public void bookmarkingAPostSavesTheDatabase() { + for (Post post : posts.values()) { + bookmarkDatabase.bookmarkPost(post); + } + verify(configurationLoader, times(posts.size())) + .saveBookmarkedPosts(any(Set.class)); + } + + @Test + public void unbookmarkingAPostSavesTheDatabase() { + for (Post post : posts.values()) { + bookmarkDatabase.bookmarkPost(post); + bookmarkDatabase.unbookmarkPost(post); + } + verify(configurationLoader, times(posts.size() * 2)) + .saveBookmarkedPosts(any(Set.class)); + } + + @Test + public void removingABookmarkRemovesTheCorrectBookmark() { + Set allPosts = new HashSet(posts.values()); + for (Post post : allPosts) { + bookmarkDatabase.bookmarkPost(post); + } + Post randomPost = posts.values().iterator().next(); + bookmarkDatabase.unbookmarkPost(randomPost); + allPosts.remove(randomPost); + assertThat(bookmarkDatabase.getBookmarkedPosts(), is(allPosts)); + for (Post post : posts.values()) { + assertThat(bookmarkDatabase.isPostBookmarked(post), + is(!post.equals(randomPost))); + } + } + + @Test + public void startingTheDatabaseLoadsBookmarkedPosts() { + bookmarkDatabase.start(); + verify(configurationLoader).loadBookmarkedPosts(); + } + + @Test + public void stoppingTheDatabaseSavesTheBookmarkedPosts() { + bookmarkDatabase.stop(); + verify(configurationLoader).saveBookmarkedPosts(any(Set.class)); + } + + @Test + public void bookmarkedPostsIncludeNotYetLoadedPosts() { + bookmarkDatabase.bookmarkPost(posts.get("PostId1")); + bookmarkDatabase.bookmarkPost(createPost("PostId3")); + final Set bookmarkedPosts = + bookmarkDatabase.getBookmarkedPosts(); + assertThat(bookmarkedPosts, + contains(isPostWithId("PostId1"), isPostWithId("PostId3"))); + } + +} diff --git a/src/test/java/net/pterodactylus/sone/database/memory/MemoryDatabaseTest.java b/src/test/java/net/pterodactylus/sone/database/memory/MemoryDatabaseTest.java index 056a06d..cc5babb 100644 --- a/src/test/java/net/pterodactylus/sone/database/memory/MemoryDatabaseTest.java +++ b/src/test/java/net/pterodactylus/sone/database/memory/MemoryDatabaseTest.java @@ -18,14 +18,51 @@ package net.pterodactylus.sone.database.memory; import static com.google.common.base.Optional.of; +import static java.util.Arrays.asList; +import static java.util.UUID.randomUUID; +import static net.pterodactylus.sone.Matchers.isAlbum; +import static net.pterodactylus.sone.Matchers.isImage; +import static net.pterodactylus.sone.Matchers.isPost; +import static net.pterodactylus.sone.Matchers.isPostReply; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.emptyIterable; +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import net.pterodactylus.sone.TestAlbumBuilder; +import net.pterodactylus.sone.TestImageBuilder; +import net.pterodactylus.sone.TestPostBuilder; +import net.pterodactylus.sone.TestPostReplyBuilder; +import net.pterodactylus.sone.TestValue; import net.pterodactylus.sone.data.Album; -import net.pterodactylus.sone.data.AlbumImpl; +import net.pterodactylus.sone.data.impl.AlbumImpl; +import net.pterodactylus.sone.data.Image; +import net.pterodactylus.sone.data.Post; +import net.pterodactylus.sone.data.PostReply; +import net.pterodactylus.sone.data.Sone; +import net.pterodactylus.util.config.Configuration; +import net.pterodactylus.util.config.Value; import com.google.common.base.Optional; +import org.junit.Before; import org.junit.Test; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; /** * Tests for {@link MemoryDatabase}. @@ -34,11 +71,204 @@ import org.junit.Test; */ public class MemoryDatabaseTest { - private final MemoryDatabase memoryDatabase = new MemoryDatabase(null, null); + private static final String SONE_ID = "sone"; + private static final String RECIPIENT_ID = "recipient"; + private final Configuration configuration = mock(Configuration.class); + private final MemoryDatabase memoryDatabase = new MemoryDatabase(null, configuration); + private final Sone sone = mock(Sone.class); + + @Before + public void setupSone() { + when(sone.getId()).thenReturn(SONE_ID); + } + + @Test + public void storedSoneIsMadeAvailable() { + Post firstPost = new TestPostBuilder().withId("post1") + .from(SONE_ID) + .withTime(1000L) + .withText("post1") + .build(); + Post secondPost = new TestPostBuilder().withId("post2") + .from(SONE_ID) + .withTime(2000L) + .withText("post2") + .to(RECIPIENT_ID) + .build(); + List posts = asList(firstPost, secondPost); + when(sone.getPosts()).thenReturn(posts); + PostReply firstPostFirstReply = + new TestPostReplyBuilder().withId("reply1") + .from(SONE_ID) + .to(firstPost.getId()) + .withTime(3000L) + .withText("reply1") + .build(); + PostReply firstPostSecondReply = + new TestPostReplyBuilder().withId("reply3") + .from(RECIPIENT_ID) + .to(firstPost.getId()) + .withTime(5000L) + .withText("reply3") + .build(); + PostReply secondPostReply = + new TestPostReplyBuilder().withId("reply2") + .from(SONE_ID) + .to(secondPost.getId()) + .withTime(4000L) + .withText("reply2") + .build(); + Set postReplies = new HashSet( + asList(firstPostFirstReply, firstPostSecondReply, + secondPostReply)); + when(sone.getReplies()).thenReturn(postReplies); + Album firstAlbum = new TestAlbumBuilder().withId("album1") + .by(sone) + .build() + .modify() + .setTitle("album1") + .setDescription("album-description1") + .update(); + Album secondAlbum = new TestAlbumBuilder().withId("album2").by( + sone).build().modify().setTitle("album2").setDescription( + "album-description2").setAlbumImage("image1").update(); + Album thirdAlbum = new TestAlbumBuilder().withId("album3").by( + sone).build().modify().setTitle("album3").setDescription( + "album-description3").update(); + firstAlbum.addAlbum(thirdAlbum); + Album rootAlbum = mock(Album.class); + when(rootAlbum.getAlbums()).thenReturn( + asList(firstAlbum, secondAlbum)); + when(sone.getRootAlbum()).thenReturn(rootAlbum); + Image firstImage = new TestImageBuilder().withId("image1") + .build() + .modify() + .setSone(sone) + .setCreationTime(1000L) + .setKey("KSK@image1") + .setTitle("image1") + .setDescription("image-description1") + .setWidth(16) + .setHeight(9) + .update(); + Image secondImage = new TestImageBuilder().withId("image2") + .build() + .modify() + .setSone(sone) + .setCreationTime(2000L) + .setKey("KSK@image2") + .setTitle("image2") + .setDescription("image-description2") + .setWidth(32) + .setHeight(18) + .update(); + Image thirdImage = new TestImageBuilder().withId("image3") + .build() + .modify() + .setSone(sone) + .setCreationTime(3000L) + .setKey("KSK@image3") + .setTitle("image3") + .setDescription("image-description3") + .setWidth(48) + .setHeight(27) + .update(); + firstAlbum.addImage(firstImage); + firstAlbum.addImage(thirdImage); + secondAlbum.addImage(secondImage); + memoryDatabase.storeSone(sone); + assertThat(memoryDatabase.getPost("post1").get(), + isPost(firstPost.getId(), 1000L, "post1", + Optional.absent())); + assertThat(memoryDatabase.getPost("post2").get(), + isPost(secondPost.getId(), 2000L, "post2", of(RECIPIENT_ID))); + assertThat(memoryDatabase.getPost("post3").isPresent(), is(false)); + assertThat(memoryDatabase.getPostReply("reply1").get(), + isPostReply("reply1", "post1", 3000L, "reply1")); + assertThat(memoryDatabase.getPostReply("reply2").get(), + isPostReply("reply2", "post2", 4000L, "reply2")); + assertThat(memoryDatabase.getPostReply("reply3").get(), + isPostReply("reply3", "post1", 5000L, "reply3")); + assertThat(memoryDatabase.getPostReply("reply4").isPresent(), + is(false)); + assertThat(memoryDatabase.getAlbum("album1").get(), + isAlbum("album1", null, "album1", "album-description1", + null)); + assertThat(memoryDatabase.getAlbum("album2").get(), + isAlbum("album2", null, "album2", "album-description2", + "image1")); + assertThat(memoryDatabase.getAlbum("album3").get(), + isAlbum("album3", "album1", "album3", "album-description3", + null)); + assertThat(memoryDatabase.getAlbum("album4").isPresent(), is(false)); + assertThat(memoryDatabase.getImage("image1").get(), + isImage("image1", 1000L, "KSK@image1", "image1", + "image-description1", 16, 9)); + assertThat(memoryDatabase.getImage("image2").get(), + isImage("image2", 2000L, "KSK@image2", "image2", + "image-description2", 32, 18)); + assertThat(memoryDatabase.getImage("image3").get(), + isImage("image3", 3000L, "KSK@image3", "image3", + "image-description3", 48, 27)); + assertThat(memoryDatabase.getImage("image4").isPresent(), is(false)); + } + + @Test + public void storedAndRemovedSoneIsNotAvailable() { + storedSoneIsMadeAvailable(); + memoryDatabase.removeSone(sone); + assertThat(memoryDatabase.getSones(), empty()); + } + + @Test + public void postRecipientsAreDetectedCorrectly() { + Post postWithRecipient = createPost(of(RECIPIENT_ID)); + memoryDatabase.storePost(postWithRecipient); + Post postWithoutRecipient = createPost(Optional.absent()); + memoryDatabase.storePost(postWithoutRecipient); + assertThat(memoryDatabase.getDirectedPosts(RECIPIENT_ID), + contains(postWithRecipient)); + } + + private Post createPost(Optional recipient) { + Post postWithRecipient = mock(Post.class); + when(postWithRecipient.getId()).thenReturn(randomUUID().toString()); + when(postWithRecipient.getSone()).thenReturn(sone); + when(postWithRecipient.getRecipientId()).thenReturn(recipient); + return postWithRecipient; + } + + @Test + public void postRepliesAreManagedCorrectly() { + Post firstPost = createPost(Optional.absent()); + PostReply firstPostFirstReply = createPostReply(firstPost, 1000L); + Post secondPost = createPost(Optional.absent()); + PostReply secondPostFirstReply = createPostReply(secondPost, 1000L); + PostReply secondPostSecondReply = createPostReply(secondPost, 2000L); + memoryDatabase.storePost(firstPost); + memoryDatabase.storePost(secondPost); + memoryDatabase.storePostReply(firstPostFirstReply); + memoryDatabase.storePostReply(secondPostFirstReply); + memoryDatabase.storePostReply(secondPostSecondReply); + assertThat(memoryDatabase.getReplies(firstPost.getId()), + contains(firstPostFirstReply)); + assertThat(memoryDatabase.getReplies(secondPost.getId()), + contains(secondPostFirstReply, secondPostSecondReply)); + } + + private PostReply createPostReply(Post post, long time) { + PostReply postReply = mock(PostReply.class); + when(postReply.getId()).thenReturn(randomUUID().toString()); + when(postReply.getTime()).thenReturn(time); + when(postReply.getPost()).thenReturn(of(post)); + final String postId = post.getId(); + when(postReply.getPostId()).thenReturn(postId); + return postReply; + } @Test public void testBasicAlbumFunctionality() { - Album newAlbum = new AlbumImpl(); + Album newAlbum = new AlbumImpl(mock(Sone.class)); assertThat(memoryDatabase.getAlbum(newAlbum.getId()), is(Optional.absent())); memoryDatabase.storeAlbum(newAlbum); assertThat(memoryDatabase.getAlbum(newAlbum.getId()), is(of(newAlbum))); @@ -46,4 +276,114 @@ public class MemoryDatabaseTest { assertThat(memoryDatabase.getAlbum(newAlbum.getId()), is(Optional.absent())); } + private void initializeFriends() { + when(configuration.getStringValue("Sone/" + SONE_ID + "/Friends/0/ID")).thenReturn( + TestValue.from("Friend1")); + when(configuration.getStringValue("Sone/" + SONE_ID + "/Friends/1/ID")).thenReturn( + TestValue.from("Friend2")); + when(configuration.getStringValue("Sone/" + SONE_ID + "/Friends/2/ID")).thenReturn( + TestValue.from(null)); + } + + @Test + public void friendsAreReturnedCorrectly() { + initializeFriends(); + when(sone.isLocal()).thenReturn(true); + Collection friends = memoryDatabase.getFriends(sone); + assertThat(friends, containsInAnyOrder("Friend1", "Friend2")); + } + + @Test + public void friendsAreOnlyLoadedOnceFromConfiguration() { + friendsAreReturnedCorrectly(); + memoryDatabase.getFriends(sone); + verify(configuration).getStringValue("Sone/" + SONE_ID + "/Friends/0/ID"); + } + + @Test + public void friendsAreOnlyReturnedForLocalSones() { + Collection friends = memoryDatabase.getFriends(sone); + assertThat(friends, emptyIterable()); + verify(configuration, never()).getStringValue("Sone/" + SONE_ID + "/Friends/0/ID"); + } + + @Test + public void checkingForAFriendReturnsTrue() { + initializeFriends(); + when(sone.isLocal()).thenReturn(true); + assertThat(memoryDatabase.isFriend(sone, "Friend1"), is(true)); + } + + @Test + public void checkingForAFriendThatIsNotAFriendReturnsFalse() { + initializeFriends(); + when(sone.isLocal()).thenReturn(true); + assertThat(memoryDatabase.isFriend(sone, "FriendX"), is(false)); + } + + @Test + public void checkingForAFriendOfRemoteSoneReturnsFalse() { + initializeFriends(); + assertThat(memoryDatabase.isFriend(sone, "Friend1"), is(false)); + } + + private Map> prepareConfigurationValues() { + final Map> configurationValues = new HashMap>(); + when(configuration.getStringValue(anyString())).thenAnswer(new Answer>() { + @Override + public Value answer(InvocationOnMock invocation) throws Throwable { + Value stringValue = TestValue.from(null); + configurationValues.put((String) invocation.getArguments()[0], stringValue); + return stringValue; + } + }); + return configurationValues; + } + + @Test + public void friendIsAddedCorrectlyToLocalSone() { + Map> configurationValues = prepareConfigurationValues(); + when(sone.isLocal()).thenReturn(true); + memoryDatabase.addFriend(sone, "Friend1"); + assertThat(configurationValues.get("Sone/" + SONE_ID + "/Friends/0/ID"), + is(TestValue.from("Friend1"))); + assertThat(configurationValues.get("Sone/" + SONE_ID + "/Friends/1/ID"), + is(TestValue.from(null))); + } + + @Test + public void friendIsNotAddedToRemoteSone() { + memoryDatabase.addFriend(sone, "Friend1"); + verify(configuration, never()).getStringValue(anyString()); + } + + @Test + public void configurationIsWrittenOnceIfFriendIsAddedTwice() { + prepareConfigurationValues(); + when(sone.isLocal()).thenReturn(true); + memoryDatabase.addFriend(sone, "Friend1"); + memoryDatabase.addFriend(sone, "Friend1"); + verify(configuration, times(3)).getStringValue(anyString()); + } + + @Test + public void friendIsRemovedCorrectlyFromLocalSone() { + Map> configurationValues = prepareConfigurationValues(); + when(sone.isLocal()).thenReturn(true); + memoryDatabase.addFriend(sone, "Friend1"); + memoryDatabase.removeFriend(sone, "Friend1"); + assertThat(configurationValues.get("Sone/" + SONE_ID + "/Friends/0/ID"), + is(TestValue.from(null))); + assertThat(configurationValues.get("Sone/" + SONE_ID + "/Friends/1/ID"), + is(TestValue.from(null))); + } + + @Test + public void configurationIsNotWrittenWhenANonFriendIsRemoved() { + prepareConfigurationValues(); + when(sone.isLocal()).thenReturn(true); + memoryDatabase.removeFriend(sone, "Friend1"); + verify(configuration).getStringValue(anyString()); + } + } diff --git a/src/test/java/net/pterodactylus/sone/fcp/FcpInterfaceTest.java b/src/test/java/net/pterodactylus/sone/fcp/FcpInterfaceTest.java new file mode 100644 index 0000000..2312907 --- /dev/null +++ b/src/test/java/net/pterodactylus/sone/fcp/FcpInterfaceTest.java @@ -0,0 +1,57 @@ +package net.pterodactylus.sone.fcp; + +import static net.pterodactylus.sone.fcp.FcpInterface.FullAccessRequired.ALWAYS; +import static net.pterodactylus.sone.fcp.FcpInterface.FullAccessRequired.NO; +import static net.pterodactylus.sone.fcp.FcpInterface.FullAccessRequired.WRITING; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +import net.pterodactylus.sone.fcp.event.FcpInterfaceActivatedEvent; +import net.pterodactylus.sone.fcp.event.FcpInterfaceDeactivatedEvent; +import net.pterodactylus.sone.fcp.event.FullAccessRequiredChanged; + +import org.junit.Test; + +/** + * Unit test for {@link FcpInterface} and its subclasses. + * + * @author David ‘Bombe’ Roden + */ +public class FcpInterfaceTest { + + private final FcpInterface fcpInterface = new FcpInterface(null); + + @Test + public void fcpInterfaceCanBeActivated() { + fcpInterface.fcpInterfaceActivated(new FcpInterfaceActivatedEvent()); + assertThat(fcpInterface.isActive(), is(true)); + } + + @Test + public void fcpInterfaceCanBeDeactivated() { + fcpInterface.fcpInterfaceDeactivated(new FcpInterfaceDeactivatedEvent()); + assertThat(fcpInterface.isActive(), is(false)); + } + + @Test + public void setFullAccessRequiredCanSetAccessToNo() { + fcpInterface.fullAccessRequiredChanged( + new FullAccessRequiredChanged(NO)); + assertThat(fcpInterface.getFullAccessRequired(), is(NO)); + } + + @Test + public void setFullAccessRequiredCanSetAccessToWriting() { + fcpInterface.fullAccessRequiredChanged( + new FullAccessRequiredChanged(WRITING)); + assertThat(fcpInterface.getFullAccessRequired(), is(WRITING)); + } + + @Test + public void setFullAccessRequiredCanSetAccessToAlways() { + fcpInterface.fullAccessRequiredChanged( + new FullAccessRequiredChanged(ALWAYS)); + assertThat(fcpInterface.getFullAccessRequired(), is(ALWAYS)); + } + +} diff --git a/src/test/java/net/pterodactylus/sone/fcp/LockSoneCommandTest.java b/src/test/java/net/pterodactylus/sone/fcp/LockSoneCommandTest.java index ae1993a..2306931 100644 --- a/src/test/java/net/pterodactylus/sone/fcp/LockSoneCommandTest.java +++ b/src/test/java/net/pterodactylus/sone/fcp/LockSoneCommandTest.java @@ -20,7 +20,6 @@ package net.pterodactylus.sone.fcp; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.notNullValue; import static org.hamcrest.MatcherAssert.assertThat; -import static org.mockito.Matchers.anyBoolean; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; @@ -51,7 +50,7 @@ public class LockSoneCommandTest { when(localSone.isLocal()).thenReturn(true); Core core = mock(Core.class); when(core.getSone(eq("LocalSone"))).thenReturn(Optional.of(localSone)); - when(core.getLocalSone(eq("LocalSone"), anyBoolean())).thenReturn(localSone); + when(core.getLocalSone(eq("LocalSone"))).thenReturn(localSone); SimpleFieldSet fields = new SimpleFieldSetBuilder().put("Sone", "LocalSone").get(); LockSoneCommand lockSoneCommand = new LockSoneCommand(core); diff --git a/src/test/java/net/pterodactylus/sone/fcp/UnlockSoneCommandTest.java b/src/test/java/net/pterodactylus/sone/fcp/UnlockSoneCommandTest.java index b966b19..ca615ee 100644 --- a/src/test/java/net/pterodactylus/sone/fcp/UnlockSoneCommandTest.java +++ b/src/test/java/net/pterodactylus/sone/fcp/UnlockSoneCommandTest.java @@ -20,7 +20,6 @@ package net.pterodactylus.sone.fcp; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.notNullValue; import static org.hamcrest.MatcherAssert.assertThat; -import static org.mockito.Matchers.anyBoolean; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; @@ -51,7 +50,7 @@ public class UnlockSoneCommandTest { when(localSone.isLocal()).thenReturn(true); Core core = mock(Core.class); when(core.getSone(eq("LocalSone"))).thenReturn(Optional.of(localSone)); - when(core.getLocalSone(eq("LocalSone"), anyBoolean())).thenReturn(localSone); + when(core.getLocalSone(eq("LocalSone"))).thenReturn(localSone); SimpleFieldSet fields = new SimpleFieldSetBuilder().put("Sone", "LocalSone").get(); UnlockSoneCommand unlockSoneCommand = new UnlockSoneCommand(core); diff --git a/src/test/java/net/pterodactylus/sone/freenet/KeyTest.java b/src/test/java/net/pterodactylus/sone/freenet/KeyTest.java new file mode 100644 index 0000000..8fff7bd --- /dev/null +++ b/src/test/java/net/pterodactylus/sone/freenet/KeyTest.java @@ -0,0 +1,69 @@ +package net.pterodactylus.sone.freenet; + +import static freenet.support.Base64.encode; +import static net.pterodactylus.sone.freenet.Key.from; +import static net.pterodactylus.sone.freenet.Key.routingKey; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +import java.net.MalformedURLException; + +import freenet.keys.FreenetURI; + +import org.junit.Test; + +/** + * Unit test for {@link Key}. + * + * @author David ‘Bombe’ Roden + */ +public class KeyTest { + + private final FreenetURI uri; + private final Key key; + + public KeyTest() throws MalformedURLException { + uri = new FreenetURI( + "SSK@NfUYvxDwU9vqb2mh-qdT~DYJ6U0XNbxMGGoLe0aCHJs,Miglsgix0VR56ZiPl4NgjnUd~UdrnHqIvXJ3KKHmxmI,AQACAAE/some-site-12/foo/bar.html"); + key = from(uri); + } + + @Test + public void keyCanBeCreatedFromFreenetUri() throws MalformedURLException { + assertThat(key.getRoutingKey(), + is("NfUYvxDwU9vqb2mh-qdT~DYJ6U0XNbxMGGoLe0aCHJs")); + assertThat(key.getCryptoKey(), + is("Miglsgix0VR56ZiPl4NgjnUd~UdrnHqIvXJ3KKHmxmI")); + assertThat(key.getExtra(), is("AQACAAE")); + } + + @Test + public void keyCanBeConvertedToUsk() throws MalformedURLException { + FreenetURI uskUri = key.toUsk("other-site", 15, "some", "path.html"); + assertThat(uskUri.toString(), + is("USK@NfUYvxDwU9vqb2mh-qdT~DYJ6U0XNbxMGGoLe0aCHJs,Miglsgix0VR56ZiPl4NgjnUd~UdrnHqIvXJ3KKHmxmI,AQACAAE/other-site/15/some/path.html")); + } + + @Test + public void keyCanBeConvertedToSskWithoutEdition() + throws MalformedURLException { + FreenetURI uskUri = key.toSsk("other-site", "some", "path.html"); + assertThat(uskUri.toString(), + is("SSK@NfUYvxDwU9vqb2mh-qdT~DYJ6U0XNbxMGGoLe0aCHJs,Miglsgix0VR56ZiPl4NgjnUd~UdrnHqIvXJ3KKHmxmI,AQACAAE/other-site/some/path.html")); + } + + @Test + public void keyCanBeConvertedToSskWithEdition() + throws MalformedURLException { + FreenetURI uskUri = key.toSsk("other-site", 15, "some", "path.html"); + assertThat(uskUri.toString(), + is("SSK@NfUYvxDwU9vqb2mh-qdT~DYJ6U0XNbxMGGoLe0aCHJs,Miglsgix0VR56ZiPl4NgjnUd~UdrnHqIvXJ3KKHmxmI,AQACAAE/other-site-15/some/path.html")); + } + + @Test + public void routingKeyIsExtractCorrectly() { + assertThat(routingKey(uri), + is("NfUYvxDwU9vqb2mh-qdT~DYJ6U0XNbxMGGoLe0aCHJs")); + } + +} diff --git a/src/test/java/net/pterodactylus/sone/freenet/wot/DefaultIdentityTest.java b/src/test/java/net/pterodactylus/sone/freenet/wot/DefaultIdentityTest.java new file mode 100644 index 0000000..165d5fd --- /dev/null +++ b/src/test/java/net/pterodactylus/sone/freenet/wot/DefaultIdentityTest.java @@ -0,0 +1,152 @@ +/* + * Sone - DefaultIdentityTest.java - Copyright © 2013 David Roden + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.pterodactylus.sone.freenet.wot; + +import static com.google.common.collect.ImmutableMap.of; +import static java.util.Arrays.asList; +import static net.pterodactylus.sone.Matchers.matchesRegex; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.hasEntry; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.nullValue; +import static org.hamcrest.collection.IsIterableContainingInOrder.contains; +import static org.mockito.Mockito.mock; + +import java.util.Collections; + +import org.junit.Test; + +/** + * Unit test for {@link DefaultIdentity}. + * + * @author David ‘Bombe’ Roden + */ +public class DefaultIdentityTest { + + protected final DefaultIdentity identity = createIdentity(); + + protected DefaultIdentity createIdentity() { + return new DefaultIdentity("Id", "Nickname", "RequestURI"); + } + + @Test + public void identityCanBeCreated() { + assertThat(identity.getId(), is("Id")); + assertThat(identity.getNickname(), is("Nickname")); + assertThat(identity.getRequestUri(), is("RequestURI")); + assertThat(identity.getContexts(), empty()); + assertThat(identity.getProperties(), is(Collections.emptyMap())); + } + + @Test + public void contextsAreAddedCorrectly() { + identity.addContext("Test"); + assertThat(identity.getContexts(), contains("Test")); + assertThat(identity.hasContext("Test"), is(true)); + } + + @Test + public void contextsAreRemovedCorrectly() { + identity.addContext("Test"); + identity.removeContext("Test"); + assertThat(identity.getContexts(), empty()); + assertThat(identity.hasContext("Test"), is(false)); + } + + @Test + public void contextsAreSetCorrectlyInBulk() { + identity.addContext("Test"); + identity.setContexts(asList("Test1", "Test2")); + assertThat(identity.getContexts(), containsInAnyOrder("Test1", "Test2")); + assertThat(identity.hasContext("Test"), is(false)); + assertThat(identity.hasContext("Test1"), is(true)); + assertThat(identity.hasContext("Test2"), is(true)); + } + + @Test + public void propertiesAreAddedCorrectly() { + identity.setProperty("Key", "Value"); + assertThat(identity.getProperties().size(), is(1)); + assertThat(identity.getProperties(), hasEntry("Key", "Value")); + assertThat(identity.getProperty("Key"), is("Value")); + } + + @Test + public void propertiesAreRemovedCorrectly() { + identity.setProperty("Key", "Value"); + identity.removeProperty("Key"); + assertThat(identity.getProperties(), is(Collections.emptyMap())); + assertThat(identity.getProperty("Key"), nullValue()); + } + + @Test + public void propertiesAreSetCorrectlyInBulk() { + identity.setProperty("Key", "Value"); + identity.setProperties(of("Key1", "Value1", "Key2", "Value2")); + assertThat(identity.getProperties().size(), is(2)); + assertThat(identity.getProperty("Key"), nullValue()); + assertThat(identity.getProperty("Key1"), is("Value1")); + assertThat(identity.getProperty("Key2"), is("Value2")); + } + + @Test + public void trustRelationshipsAreAddedCorrectly() { + OwnIdentity ownIdentity = mock(OwnIdentity.class); + Trust trust = mock(Trust.class); + identity.setTrust(ownIdentity, trust); + assertThat(identity.getTrust(ownIdentity), is(trust)); + } + + @Test + public void trustRelationshipsAreRemovedCorrectly() { + OwnIdentity ownIdentity = mock(OwnIdentity.class); + Trust trust = mock(Trust.class); + identity.setTrust(ownIdentity, trust); + identity.removeTrust(ownIdentity); + assertThat(identity.getTrust(ownIdentity), nullValue()); + } + + @Test + public void identitiesWithTheSameIdAreEqual() { + DefaultIdentity identity2 = new DefaultIdentity("Id", "Nickname2", "RequestURI2"); + assertThat(identity2, is(identity)); + assertThat(identity, is(identity2)); + } + + @Test + public void twoEqualIdentitiesHaveTheSameHashCode() { + DefaultIdentity identity2 = new DefaultIdentity("Id", "Nickname2", "RequestURI2"); + assertThat(identity.hashCode(), is(identity2.hashCode())); + } + + @Test + public void nullDoesNotMatchAnIdentity() { + assertThat(identity, not(is((Object) null))); + } + + @Test + public void toStringContainsIdAndNickname() { + String identityString = identity.toString(); + assertThat(identityString, matchesRegex(".*\\bId\\b.*")); + assertThat(identityString, matchesRegex(".*\\bNickname\\b.*")); + } + +} diff --git a/src/test/java/net/pterodactylus/sone/freenet/wot/DefaultOwnIdentityTest.java b/src/test/java/net/pterodactylus/sone/freenet/wot/DefaultOwnIdentityTest.java new file mode 100644 index 0000000..4e2728b --- /dev/null +++ b/src/test/java/net/pterodactylus/sone/freenet/wot/DefaultOwnIdentityTest.java @@ -0,0 +1,42 @@ +/* + * Sone - DefaultOwnIdentityTest.java - Copyright © 2013 David Roden + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.pterodactylus.sone.freenet.wot; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +import org.junit.Test; + +/** + * Unit test for {@link DefaultOwnIdentity}. + * + * @author David ‘Bombe’ Roden + */ +public class DefaultOwnIdentityTest extends DefaultIdentityTest { + + @Override + protected DefaultIdentity createIdentity() { + return new DefaultOwnIdentity("Id", "Nickname", "RequestURI", "InsertURI"); + } + + @Test + public void ownIdentityCanBeCreated() { + assertThat(((OwnIdentity) identity).getInsertUri(), is("InsertURI")); + } + +} diff --git a/src/test/java/net/pterodactylus/sone/freenet/wot/Identities.java b/src/test/java/net/pterodactylus/sone/freenet/wot/Identities.java new file mode 100644 index 0000000..fe3625e --- /dev/null +++ b/src/test/java/net/pterodactylus/sone/freenet/wot/Identities.java @@ -0,0 +1,47 @@ +/* + * Sone - Identities.java - Copyright © 2013 David Roden + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.pterodactylus.sone.freenet.wot; + +import java.util.Collection; +import java.util.Map; + +/** + * Creates {@link Identity}s and {@link OwnIdentity}s. + * + * @author David ‘Bombe’ Roden + */ +public class Identities { + + public static OwnIdentity createOwnIdentity(String id, Collection contexts, Map properties) { + DefaultOwnIdentity ownIdentity = new DefaultOwnIdentity(id, "Nickname" + id, "Request" + id, "Insert" + id); + setContextsAndPropertiesOnIdentity(ownIdentity, contexts, properties); + return ownIdentity; + } + + public static Identity createIdentity(String id, Collection contexts, Map properties) { + DefaultIdentity identity = new DefaultIdentity(id, "Nickname" + id, "Request" + id); + setContextsAndPropertiesOnIdentity(identity, contexts, properties); + return identity; + } + + private static void setContextsAndPropertiesOnIdentity(Identity identity, Collection contexts, Map properties) { + identity.setContexts(contexts); + identity.setProperties(properties); + } + +} diff --git a/src/test/java/net/pterodactylus/sone/freenet/wot/IdentityChangeDetectorTest.java b/src/test/java/net/pterodactylus/sone/freenet/wot/IdentityChangeDetectorTest.java new file mode 100644 index 0000000..b2d4ed1 --- /dev/null +++ b/src/test/java/net/pterodactylus/sone/freenet/wot/IdentityChangeDetectorTest.java @@ -0,0 +1,190 @@ +/* + * Sone - IdentityChangeDetectorTest.java - Copyright © 2013 David Roden + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.pterodactylus.sone.freenet.wot; + +import static com.google.common.collect.ImmutableMap.of; +import static com.google.common.collect.Lists.newArrayList; +import static java.util.Arrays.asList; +import static net.pterodactylus.sone.freenet.wot.Identities.createIdentity; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.empty; + +import java.util.Collection; + +import net.pterodactylus.sone.freenet.wot.IdentityChangeDetector.IdentityProcessor; + +import org.junit.Before; +import org.junit.Test; + +/** + * Unit test for {@link IdentityChangeDetector}. + * + * @author David ‘Bombe’ Roden + */ +public class IdentityChangeDetectorTest { + + private final IdentityChangeDetector identityChangeDetector = new IdentityChangeDetector(createOldIdentities()); + private final Collection newIdentities = newArrayList(); + private final Collection removedIdentities = newArrayList(); + private final Collection changedIdentities = newArrayList(); + private final Collection unchangedIdentities = newArrayList(); + + @Before + public void setup() { + identityChangeDetector.onNewIdentity(new IdentityProcessor() { + @Override + public void processIdentity(Identity identity) { + newIdentities.add(identity); + } + }); + identityChangeDetector.onRemovedIdentity(new IdentityProcessor() { + @Override + public void processIdentity(Identity identity) { + removedIdentities.add(identity); + } + }); + identityChangeDetector.onChangedIdentity(new IdentityProcessor() { + @Override + public void processIdentity(Identity identity) { + changedIdentities.add(identity); + } + }); + identityChangeDetector.onUnchangedIdentity(new IdentityProcessor() { + @Override + public void processIdentity(Identity identity) { + unchangedIdentities.add(identity); + } + }); + } + + @Test + public void noDifferencesAreDetectedWhenSendingTheOldIdentitiesAgain() { + identityChangeDetector.detectChanges(createOldIdentities()); + assertThat(newIdentities, empty()); + assertThat(removedIdentities, empty()); + assertThat(changedIdentities, empty()); + assertThat(unchangedIdentities, containsInAnyOrder(createIdentity1(), createIdentity2(), createIdentity3())); + } + + @Test + public void detectThatAnIdentityWasRemoved() { + identityChangeDetector.detectChanges(asList(createIdentity1(), createIdentity3())); + assertThat(newIdentities, empty()); + assertThat(removedIdentities, containsInAnyOrder(createIdentity2())); + assertThat(changedIdentities, empty()); + assertThat(unchangedIdentities, containsInAnyOrder(createIdentity1(), createIdentity3())); + } + + @Test + public void detectThatAnIdentityWasAdded() { + identityChangeDetector.detectChanges(asList(createIdentity1(), createIdentity2(), createIdentity3(), createIdentity4())); + assertThat(newIdentities, containsInAnyOrder(createIdentity4())); + assertThat(removedIdentities, empty()); + assertThat(changedIdentities, empty()); + assertThat(unchangedIdentities, containsInAnyOrder(createIdentity1(), createIdentity2(), createIdentity3())); + } + + @Test + public void detectThatAContextWasRemoved() { + Identity identity2 = createIdentity2(); + identity2.removeContext("Context C"); + identityChangeDetector.detectChanges(asList(createIdentity1(), identity2, createIdentity3())); + assertThat(newIdentities, empty()); + assertThat(removedIdentities, empty()); + assertThat(changedIdentities, containsInAnyOrder(identity2)); + assertThat(unchangedIdentities, containsInAnyOrder(createIdentity1(), createIdentity3())); + } + + @Test + public void detectThatAContextWasAdded() { + Identity identity2 = createIdentity2(); + identity2.addContext("Context C1"); + identityChangeDetector.detectChanges(asList(createIdentity1(), identity2, createIdentity3())); + assertThat(newIdentities, empty()); + assertThat(removedIdentities, empty()); + assertThat(changedIdentities, containsInAnyOrder(identity2)); + assertThat(unchangedIdentities, containsInAnyOrder(createIdentity1(), createIdentity3())); + } + + @Test + public void detectThatAPropertyWasRemoved() { + Identity identity1 = createIdentity1(); + identity1.removeProperty("Key A"); + identityChangeDetector.detectChanges(asList(identity1, createIdentity2(), createIdentity3())); + assertThat(newIdentities, empty()); + assertThat(removedIdentities, empty()); + assertThat(changedIdentities, containsInAnyOrder(identity1)); + assertThat(unchangedIdentities, containsInAnyOrder(createIdentity2(), createIdentity3())); + } + + @Test + public void detectThatAPropertyWasAdded() { + Identity identity3 = createIdentity3(); + identity3.setProperty("Key A", "Value A"); + identityChangeDetector.detectChanges(asList(createIdentity1(), createIdentity2(), identity3)); + assertThat(newIdentities, empty()); + assertThat(removedIdentities, empty()); + assertThat(changedIdentities, containsInAnyOrder(identity3)); + assertThat(unchangedIdentities, containsInAnyOrder(createIdentity1(), createIdentity2())); + } + + @Test + public void detectThatAPropertyWasChanged() { + Identity identity3 = createIdentity3(); + identity3.setProperty("Key E", "Value F"); + identityChangeDetector.detectChanges(asList(createIdentity1(), createIdentity2(), identity3)); + assertThat(newIdentities, empty()); + assertThat(removedIdentities, empty()); + assertThat(changedIdentities, containsInAnyOrder(identity3)); + assertThat(unchangedIdentities, containsInAnyOrder(createIdentity1(), createIdentity2())); + } + + @Test + public void noRemovedIdentitiesAreDetectedWithoutAnIdentityProcessor() { + identityChangeDetector.onRemovedIdentity(null); + identityChangeDetector.detectChanges(asList(createIdentity1(), createIdentity3())); + } + + @Test + public void noAddedIdentitiesAreDetectedWithoutAnIdentityProcessor() { + identityChangeDetector.onNewIdentity(null); + identityChangeDetector.detectChanges(asList(createIdentity1(), createIdentity2(), createIdentity3(), createIdentity4())); + } + + private static Collection createOldIdentities() { + return asList(createIdentity1(), createIdentity2(), createIdentity3()); + } + + private static Identity createIdentity1() { + return createIdentity("Test1", asList("Context A", "Context B"), of("Key A", "Value A", "Key B", "Value B")); + } + + private static Identity createIdentity2() { + return createIdentity("Test2", asList("Context C", "Context D"), of("Key C", "Value C", "Key D", "Value D")); + } + + private static Identity createIdentity3() { + return createIdentity("Test3", asList("Context E", "Context F"), of("Key E", "Value E", "Key F", "Value F")); + } + + private static Identity createIdentity4() { + return createIdentity("Test4", asList("Context G", "Context H"), of("Key G", "Value G", "Key H", "Value H")); + } + +} diff --git a/src/test/java/net/pterodactylus/sone/freenet/wot/IdentityChangeEventSenderTest.java b/src/test/java/net/pterodactylus/sone/freenet/wot/IdentityChangeEventSenderTest.java new file mode 100644 index 0000000..8e1cf6a --- /dev/null +++ b/src/test/java/net/pterodactylus/sone/freenet/wot/IdentityChangeEventSenderTest.java @@ -0,0 +1,93 @@ +/* + * Sone - IdentityChangeEventSenderTest.java - Copyright © 2013 David Roden + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.pterodactylus.sone.freenet.wot; + +import static com.google.common.collect.ImmutableMap.of; +import static java.util.Arrays.asList; +import static net.pterodactylus.sone.freenet.wot.Identities.createIdentity; +import static net.pterodactylus.sone.freenet.wot.Identities.createOwnIdentity; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import net.pterodactylus.sone.freenet.wot.event.IdentityAddedEvent; +import net.pterodactylus.sone.freenet.wot.event.IdentityRemovedEvent; +import net.pterodactylus.sone.freenet.wot.event.IdentityUpdatedEvent; +import net.pterodactylus.sone.freenet.wot.event.OwnIdentityAddedEvent; +import net.pterodactylus.sone.freenet.wot.event.OwnIdentityRemovedEvent; + +import com.google.common.eventbus.EventBus; +import org.junit.Test; + +/** + * Unit test for {@link IdentityChangeEventSender}. + * + * @author David ‘Bombe’ Roden + */ +public class IdentityChangeEventSenderTest { + + private final EventBus eventBus = mock(EventBus.class); + private final List ownIdentities = asList( + createOwnIdentity("O1", asList("Test"), of("KeyA", "ValueA")), + createOwnIdentity("O2", asList("Test2"), of("KeyB", "ValueB")), + createOwnIdentity("O3", asList("Test3"), of("KeyC", "ValueC")) + ); + private final List identities = asList( + createIdentity("I1", Collections.emptyList(), Collections.emptyMap()), + createIdentity("I2", Collections.emptyList(), Collections.emptyMap()), + createIdentity("I3", Collections.emptyList(), Collections.emptyMap()), + createIdentity("I2", asList("Test"), Collections.emptyMap()) + ); + private final IdentityChangeEventSender identityChangeEventSender = new IdentityChangeEventSender(eventBus, createOldIdentities()); + + @Test + public void addingAnOwnIdentityIsDetectedAndReportedCorrectly() { + Map> newIdentities = createNewIdentities(); + identityChangeEventSender.detectChanges(newIdentities); + verify(eventBus).post(eq(new OwnIdentityRemovedEvent(ownIdentities.get(0)))); + verify(eventBus).post(eq(new IdentityRemovedEvent(ownIdentities.get(0), identities.get(0)))); + verify(eventBus).post(eq(new IdentityRemovedEvent(ownIdentities.get(0), identities.get(1)))); + verify(eventBus).post(eq(new OwnIdentityAddedEvent(ownIdentities.get(2)))); + verify(eventBus).post(eq(new IdentityAddedEvent(ownIdentities.get(2), identities.get(1)))); + verify(eventBus).post(eq(new IdentityAddedEvent(ownIdentities.get(2), identities.get(2)))); + verify(eventBus).post(eq(new IdentityRemovedEvent(ownIdentities.get(1), identities.get(0)))); + verify(eventBus).post(eq(new IdentityAddedEvent(ownIdentities.get(1), identities.get(2)))); + verify(eventBus).post(eq(new IdentityUpdatedEvent(ownIdentities.get(1), identities.get(1)))); + } + + private Map> createNewIdentities() { + Map> oldIdentities = new HashMap>(); + oldIdentities.put(ownIdentities.get(1), asList(identities.get(3), identities.get(2))); + oldIdentities.put(ownIdentities.get(2), asList(identities.get(1), identities.get(2))); + return oldIdentities; + } + + private Map> createOldIdentities() { + Map> oldIdentities = new HashMap>(); + oldIdentities.put(ownIdentities.get(0), asList(identities.get(0), identities.get(1))); + oldIdentities.put(ownIdentities.get(1), asList(identities.get(0), identities.get(1))); + return oldIdentities; + } + +} diff --git a/src/test/java/net/pterodactylus/sone/freenet/wot/IdentityLoaderTest.java b/src/test/java/net/pterodactylus/sone/freenet/wot/IdentityLoaderTest.java new file mode 100644 index 0000000..98e187d --- /dev/null +++ b/src/test/java/net/pterodactylus/sone/freenet/wot/IdentityLoaderTest.java @@ -0,0 +1,156 @@ +/* + * Sone - IdentityLoaderTest.java - Copyright © 2013 David Roden + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.pterodactylus.sone.freenet.wot; + +import static com.google.common.base.Optional.of; +import static com.google.common.collect.Lists.newArrayList; +import static com.google.common.collect.Sets.newHashSet; +import static java.util.Arrays.asList; +import static java.util.Collections.emptySet; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import com.google.common.base.Optional; +import com.google.common.collect.ImmutableMap; +import org.hamcrest.Matchers; +import org.junit.Before; +import org.junit.Test; + +/** + * Unit test for {@link IdentityLoader}. + * + * @author David ‘Bombe’ Roden + */ +public class IdentityLoaderTest { + + private final WebOfTrustConnector webOfTrustConnector = mock(WebOfTrustConnector.class); + private final IdentityLoader identityLoader = new IdentityLoader(webOfTrustConnector, of(new Context("Test"))); + private final IdentityLoader identityLoaderWithoutContext = new IdentityLoader(webOfTrustConnector); + + @Before + public void setup() throws WebOfTrustException { + List ownIdentities = createOwnIdentities(); + when(webOfTrustConnector.loadAllOwnIdentities()).thenReturn(newHashSet(ownIdentities)); + when(webOfTrustConnector.loadTrustedIdentities(eq(ownIdentities.get(0)), any(Optional.class))).thenReturn(createTrustedIdentitiesForFirstOwnIdentity()); + when(webOfTrustConnector.loadTrustedIdentities(eq(ownIdentities.get(1)), any(Optional.class))).thenReturn(createTrustedIdentitiesForSecondOwnIdentity()); + when(webOfTrustConnector.loadTrustedIdentities(eq(ownIdentities.get(2)), any(Optional.class))).thenReturn(createTrustedIdentitiesForThirdOwnIdentity()); + when(webOfTrustConnector.loadTrustedIdentities(eq(ownIdentities.get(3)), any(Optional.class))).thenReturn(createTrustedIdentitiesForFourthOwnIdentity()); + } + + private List createOwnIdentities() { + return newArrayList( + createOwnIdentity("O1", "ON1", "OR1", "OI1", asList("Test", "Test2"), ImmutableMap.of("KeyA", "ValueA", "KeyB", "ValueB")), + createOwnIdentity("O2", "ON2", "OR2", "OI2", asList("Test"), ImmutableMap.of("KeyC", "ValueC")), + createOwnIdentity("O3", "ON3", "OR3", "OI3", asList("Test2"), ImmutableMap.of("KeyE", "ValueE", "KeyD", "ValueD")), + createOwnIdentity("O4", "ON4", "OR$", "OI4", asList("Test"), ImmutableMap.of("KeyA", "ValueA", "KeyD", "ValueD")) + ); + } + + private Set createTrustedIdentitiesForFirstOwnIdentity() { + return newHashSet( + createIdentity("I11", "IN11", "IR11", asList("Test"), ImmutableMap.of("KeyA", "ValueA")) + ); + } + + private Set createTrustedIdentitiesForSecondOwnIdentity() { + return newHashSet( + createIdentity("I21", "IN21", "IR21", asList("Test", "Test2"), ImmutableMap.of("KeyB", "ValueB")) + ); + } + + private Set createTrustedIdentitiesForThirdOwnIdentity() { + return newHashSet( + createIdentity("I31", "IN31", "IR31", asList("Test", "Test3"), ImmutableMap.of("KeyC", "ValueC")) + ); + } + + private Set createTrustedIdentitiesForFourthOwnIdentity() { + return emptySet(); + } + + private OwnIdentity createOwnIdentity(String id, String nickname, String requestUri, String insertUri, List contexts, ImmutableMap properties) { + OwnIdentity ownIdentity = new DefaultOwnIdentity(id, nickname, requestUri, insertUri); + ownIdentity.setContexts(contexts); + ownIdentity.setProperties(properties); + return ownIdentity; + } + + private Identity createIdentity(String id, String nickname, String requestUri, List contexts, ImmutableMap properties) { + Identity identity = new DefaultIdentity(id, nickname, requestUri); + identity.setContexts(contexts); + identity.setProperties(properties); + return identity; + } + + @Test + public void loadingIdentities() throws WebOfTrustException { + List ownIdentities = createOwnIdentities(); + Map> identities = identityLoader.loadIdentities(); + verify(webOfTrustConnector).loadAllOwnIdentities(); + verify(webOfTrustConnector).loadTrustedIdentities(eq(ownIdentities.get(0)), eq(of("Test"))); + verify(webOfTrustConnector).loadTrustedIdentities(eq(ownIdentities.get(1)), eq(of("Test"))); + verify(webOfTrustConnector, never()).loadTrustedIdentities(eq(ownIdentities.get(2)), any(Optional.class)); + verify(webOfTrustConnector).loadTrustedIdentities(eq(ownIdentities.get(3)), eq(of("Test"))); + assertThat(identities.keySet(), hasSize(4)); + assertThat(identities.keySet(), containsInAnyOrder(ownIdentities.get(0), ownIdentities.get(1), ownIdentities.get(2), ownIdentities.get(3))); + verifyIdentitiesForOwnIdentity(identities, ownIdentities.get(0), createTrustedIdentitiesForFirstOwnIdentity()); + verifyIdentitiesForOwnIdentity(identities, ownIdentities.get(1), createTrustedIdentitiesForSecondOwnIdentity()); + verifyIdentitiesForOwnIdentity(identities, ownIdentities.get(2), Collections.emptySet()); + verifyIdentitiesForOwnIdentity(identities, ownIdentities.get(3), createTrustedIdentitiesForFourthOwnIdentity()); + } + + @Test + public void loadingIdentitiesWithoutContext() throws WebOfTrustException { + List ownIdentities = createOwnIdentities(); + Map> identities = identityLoaderWithoutContext.loadIdentities(); + verify(webOfTrustConnector).loadAllOwnIdentities(); + verify(webOfTrustConnector).loadTrustedIdentities(eq(ownIdentities.get(0)), eq(Optional.absent())); + verify(webOfTrustConnector).loadTrustedIdentities(eq(ownIdentities.get(1)), eq(Optional.absent())); + verify(webOfTrustConnector).loadTrustedIdentities(eq(ownIdentities.get(2)), eq(Optional.absent())); + verify(webOfTrustConnector).loadTrustedIdentities(eq(ownIdentities.get(3)), eq(Optional.absent())); + assertThat(identities.keySet(), hasSize(4)); + OwnIdentity firstOwnIdentity = ownIdentities.get(0); + OwnIdentity secondOwnIdentity = ownIdentities.get(1); + OwnIdentity thirdOwnIdentity = ownIdentities.get(2); + OwnIdentity fourthOwnIdentity = ownIdentities.get(3); + assertThat(identities.keySet(), containsInAnyOrder(firstOwnIdentity, secondOwnIdentity, thirdOwnIdentity, fourthOwnIdentity)); + verifyIdentitiesForOwnIdentity(identities, firstOwnIdentity, createTrustedIdentitiesForFirstOwnIdentity()); + verifyIdentitiesForOwnIdentity(identities, secondOwnIdentity, createTrustedIdentitiesForSecondOwnIdentity()); + verifyIdentitiesForOwnIdentity(identities, thirdOwnIdentity, createTrustedIdentitiesForThirdOwnIdentity()); + verifyIdentitiesForOwnIdentity(identities, fourthOwnIdentity, createTrustedIdentitiesForFourthOwnIdentity()); + } + + private void verifyIdentitiesForOwnIdentity(Map> identities, OwnIdentity ownIdentity, Set trustedIdentities) { + assertThat(identities.get(ownIdentity), Matchers.>is(trustedIdentities)); + } + +} diff --git a/src/test/java/net/pterodactylus/sone/freenet/wot/IdentityManagerTest.java b/src/test/java/net/pterodactylus/sone/freenet/wot/IdentityManagerTest.java new file mode 100644 index 0000000..83c696d --- /dev/null +++ b/src/test/java/net/pterodactylus/sone/freenet/wot/IdentityManagerTest.java @@ -0,0 +1,39 @@ +package net.pterodactylus.sone.freenet.wot; + +import static com.google.common.base.Optional.of; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import net.pterodactylus.sone.freenet.plugin.PluginException; + +import com.google.common.eventbus.EventBus; +import org.junit.Test; + +/** + * Unit test for {@link IdentityManagerImpl}. + * + * @author David ‘Bombe’ Roden + */ +public class IdentityManagerTest { + + private final EventBus eventBus = mock(EventBus.class); + private final WebOfTrustConnector webOfTrustConnector = mock(WebOfTrustConnector.class); + private final IdentityManager identityManager = new IdentityManagerImpl(eventBus, webOfTrustConnector, new IdentityLoader(webOfTrustConnector, of(new Context("Test")))); + + @Test + public void identityManagerPingsWotConnector() throws PluginException { + assertThat(identityManager.isConnected(), is(true)); + verify(webOfTrustConnector).ping(); + } + + @Test + public void disconnectedWotConnectorIsRecognized() throws PluginException { + doThrow(PluginException.class).when(webOfTrustConnector).ping(); + assertThat(identityManager.isConnected(), is(false)); + verify(webOfTrustConnector).ping(); + } + +} diff --git a/src/test/java/net/pterodactylus/sone/freenet/wot/event/IdentityEventTest.java b/src/test/java/net/pterodactylus/sone/freenet/wot/event/IdentityEventTest.java new file mode 100644 index 0000000..461e303 --- /dev/null +++ b/src/test/java/net/pterodactylus/sone/freenet/wot/event/IdentityEventTest.java @@ -0,0 +1,54 @@ +package net.pterodactylus.sone.freenet.wot.event; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.mockito.Mockito.mock; + +import net.pterodactylus.sone.freenet.wot.Identity; +import net.pterodactylus.sone.freenet.wot.OwnIdentity; + +import org.junit.Test; + +/** + * Unit test for {@link IdentityEvent}. + * + * @author David ‘Bombe’ Roden + */ +public class IdentityEventTest { + + private final OwnIdentity ownIdentity = mock(OwnIdentity.class); + private final Identity identity = mock(Identity.class); + private final IdentityEvent identityEvent = createIdentityEvent(ownIdentity, identity); + + private IdentityEvent createIdentityEvent(final OwnIdentity ownIdentity, final Identity identity) { + return new IdentityEvent(ownIdentity, identity) { + }; + } + + @Test + public void identityEventRetainsIdentities() { + assertThat(identityEvent.ownIdentity(), is(ownIdentity)); + assertThat(identityEvent.identity(), is(identity)); + } + + @Test + public void eventsWithTheSameIdentityHaveTheSameHashCode() { + IdentityEvent secondIdentityEvent = createIdentityEvent(ownIdentity, identity); + assertThat(identityEvent.hashCode(), is(secondIdentityEvent.hashCode())); + } + + @Test + public void eventsWithTheSameIdentitiesAreEqual() { + IdentityEvent secondIdentityEvent = createIdentityEvent(ownIdentity, identity); + assertThat(identityEvent, is(secondIdentityEvent)); + assertThat(secondIdentityEvent, is(identityEvent)); + } + + @Test + public void nullDoesNotEqualIdentityEvent() { + assertThat(identityEvent, not(is((Object) null))); + } + + +} diff --git a/src/test/java/net/pterodactylus/sone/freenet/wot/event/OwnIdentityEventTest.java b/src/test/java/net/pterodactylus/sone/freenet/wot/event/OwnIdentityEventTest.java new file mode 100644 index 0000000..c4ec43b --- /dev/null +++ b/src/test/java/net/pterodactylus/sone/freenet/wot/event/OwnIdentityEventTest.java @@ -0,0 +1,48 @@ +package net.pterodactylus.sone.freenet.wot.event; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.mockito.Mockito.mock; + +import net.pterodactylus.sone.freenet.wot.OwnIdentity; + +import org.junit.Test; + +/** + * Unit test for {@link OwnIdentityEvent}. + * + * @author David ‘Bombe’ Roden + */ +public class OwnIdentityEventTest { + + private final OwnIdentity ownIdentity = mock(OwnIdentity.class); + private final OwnIdentityEvent ownIdentityEvent = createOwnIdentityEvent(ownIdentity); + + @Test + public void eventRetainsOwnIdentity() { + assertThat(ownIdentityEvent.ownIdentity(), is(ownIdentity)); + } + + protected OwnIdentityEvent createOwnIdentityEvent(final OwnIdentity ownIdentity) { + return new OwnIdentityEvent(ownIdentity) { + }; + } + + @Test + public void twoOwnIdentityEventsWithTheSameIdentityHaveTheSameHashCode() { + OwnIdentityEvent secondOwnIdentityEvent = createOwnIdentityEvent(ownIdentity); + assertThat(secondOwnIdentityEvent.hashCode(), is(ownIdentityEvent.hashCode())); + } + + @Test + public void ownIdentityEventDoesNotMatchNull() { + assertThat(ownIdentityEvent, not(is((Object) null))); + } + + @Test + public void ownIdentityEventDoesNotMatchObjectWithADifferentClass() { + assertThat(ownIdentityEvent, not(is(new Object()))); + } + +} diff --git a/src/test/java/net/pterodactylus/sone/template/AlbumAccessorTest.java b/src/test/java/net/pterodactylus/sone/template/AlbumAccessorTest.java new file mode 100644 index 0000000..afc2a54 --- /dev/null +++ b/src/test/java/net/pterodactylus/sone/template/AlbumAccessorTest.java @@ -0,0 +1,93 @@ +package net.pterodactylus.sone.template; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.is; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.List; + +import net.pterodactylus.sone.TestUtil; +import net.pterodactylus.sone.data.Album; +import net.pterodactylus.sone.data.Profile; +import net.pterodactylus.sone.data.Sone; + +import org.hamcrest.Description; +import org.hamcrest.Matcher; +import org.hamcrest.TypeSafeDiagnosingMatcher; +import org.junit.Before; +import org.junit.Test; + +/** + * Unit test for {@link AlbumAccessor}. + * + * @author David ‘Bombe’ Roden + */ +public class AlbumAccessorTest { + + private final AlbumAccessor albumAccessor = new AlbumAccessor(); + private final Album album = mock(Album.class); + + @Before + public void setupAlbum() { + when(album.getId()).thenReturn("Album"); + when(album.getTitle()).thenReturn("Album Title"); + } + + @Test + public void backlinksAreGenerated() { + Sone sone = mock(Sone.class); + Profile profile = new Profile(sone); + when(sone.getId()).thenReturn("Sone"); + when(sone.getName()).thenReturn("Sone Name"); + when(sone.getProfile()).thenReturn(profile); + Album parentAlbum = mock(Album.class); + when(parentAlbum.isRoot()).thenReturn(true); + when(album.getSone()).thenReturn(sone); + when(album.getParent()).thenReturn(parentAlbum); + List backlinks = + (List) albumAccessor.get(null, album, "backlinks"); + assertThat(backlinks, contains(isLink("sone=Sone", "Sone Name"), + isLink("album=Album", "Album Title"))); + } + + @Test + public void nameIsGenerated() { + assertThat((String) albumAccessor.get(null, album, "id"), + is("Album")); + assertThat((String) albumAccessor.get(null, album, "title"), + is("Album Title")); + } + + private static Matcher isLink(final String target, + final String name) { + return new TypeSafeDiagnosingMatcher() { + @Override + protected boolean matchesSafely(Object item, + Description mismatchDescription) { + if (!TestUtil.callPrivateMethod(item, "getTarget") + .contains(target)) { + mismatchDescription.appendText("link does not contain ") + .appendValue(target); + return false; + } + if (!TestUtil.callPrivateMethod(item, "getName") + .equals(name)) { + mismatchDescription.appendText("is not named ") + .appendValue(name); + return false; + } + return true; + } + + @Override + public void describeTo(Description description) { + description.appendText("link containing ") + .appendValue(target); + description.appendText(", named ").appendValue(name); + } + }; + } + +} diff --git a/src/test/java/net/pterodactylus/sone/template/CollectionAccessorTest.java b/src/test/java/net/pterodactylus/sone/template/CollectionAccessorTest.java new file mode 100644 index 0000000..d0e5057 --- /dev/null +++ b/src/test/java/net/pterodactylus/sone/template/CollectionAccessorTest.java @@ -0,0 +1,57 @@ +package net.pterodactylus.sone.template; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.Collection; + +import net.pterodactylus.sone.data.Profile; +import net.pterodactylus.sone.data.Sone; + +import org.junit.Before; +import org.junit.Test; + +/** + * Unit test for {@link CollectionAccessor}. + * + * @author David ‘Bombe’ Roden + */ +public class CollectionAccessorTest { + + private final CollectionAccessor accessor = new CollectionAccessor(); + private final Collection collection = new ArrayList(); + + @Before + public void setupCollection() { + collection.add(new Object()); + collection.add(createSone("One", "1.", "First")); + collection.add(new Object()); + collection.add(createSone("Two", "2.", "Second")); + } + + private Sone createSone(String firstName, String middleName, + String lastName) { + Sone sone = mock(Sone.class); + Profile profile = new Profile(sone); + profile.setFirstName(firstName).setMiddleName(middleName).setLastName( + lastName); + when(sone.getProfile()).thenReturn(profile); + return sone; + } + + @Test + public void soneNamesAreConcatenatedCorrectly() { + assertThat(accessor.get(null, collection, "soneNames"), + is((Object) "One 1. First, Two 2. Second")); + } + + @Test + public void sizeIsReportedCorrectly() { + assertThat(accessor.get(null, collection, "size"), + is((Object) Integer.valueOf(4))); + } + +} diff --git a/src/test/java/net/pterodactylus/sone/template/CssClassNameFilterTest.java b/src/test/java/net/pterodactylus/sone/template/CssClassNameFilterTest.java new file mode 100644 index 0000000..7b47567 --- /dev/null +++ b/src/test/java/net/pterodactylus/sone/template/CssClassNameFilterTest.java @@ -0,0 +1,35 @@ +package net.pterodactylus.sone.template; + +import static java.util.Collections.emptyMap; +import static org.hamcrest.MatcherAssert.assertThat; + +import java.util.Map; + +import org.hamcrest.Matchers; +import org.junit.Test; + +/** + * Unit test for {@link CssClassNameFilter}. + * + * @author David ‘Bombe’ Roden + */ +public class CssClassNameFilterTest { + + private static final Map EMPTY_MAP = emptyMap(); + private final CssClassNameFilter filter = new CssClassNameFilter(); + + @Test + public void stringsAreFiltered() { + String allCharacters = "name with äöü"; + String filteredCharacters = "name_with____"; + assertThat(filter.format(null, allCharacters, EMPTY_MAP), + Matchers.is(filteredCharacters)); + } + + @Test + public void nullIsFiltered() { + assertThat(filter.format(null, null, EMPTY_MAP), + Matchers.is("null")); + } + +} diff --git a/src/test/java/net/pterodactylus/sone/template/GetPagePluginTest.java b/src/test/java/net/pterodactylus/sone/template/GetPagePluginTest.java new file mode 100644 index 0000000..febd06a --- /dev/null +++ b/src/test/java/net/pterodactylus/sone/template/GetPagePluginTest.java @@ -0,0 +1,79 @@ +package net.pterodactylus.sone.template; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.HashMap; +import java.util.Map; + +import net.pterodactylus.sone.web.page.FreenetRequest; +import net.pterodactylus.util.template.TemplateContext; + +import freenet.support.api.HTTPRequest; + +import org.junit.Before; +import org.junit.Test; + +/** + * Unit test for {@link GetPagePlugin}. + * + * @author David ‘Bombe’ Roden + */ +public class GetPagePluginTest { + + private final GetPagePlugin plugin = new GetPagePlugin(); + private final TemplateContext context = mock(TemplateContext.class); + private final FreenetRequest request = mock(FreenetRequest.class); + private final Map parameters = + new HashMap(); + private HTTPRequest httpRequest = mock(HTTPRequest.class); + + @Before + public void setupTemplateContext() { + when(context.get("request")).thenReturn(request); + when(request.getHttpRequest()).thenReturn(httpRequest); + when(httpRequest.getParam("page")).thenReturn("1"); + } + + @Test + public void fullySpecifiedPluginCallSetsCorrectValue() { + parameters.put("request", "request"); + parameters.put("parameter", "page"); + parameters.put("key", "page-key"); + plugin.execute(context, parameters); + verify(context).set("page-key", 1); + } + + @Test + public void missingRequestParameterStillSetsCorrectValue() { + parameters.put("parameter", "page"); + parameters.put("key", "page-key"); + plugin.execute(context, parameters); + verify(context).set("page-key", 1); + } + + @Test + public void missingParameterParameterStillSetsCorrectValue() { + parameters.put("request", "request"); + parameters.put("key", "page-key"); + plugin.execute(context, parameters); + verify(context).set("page-key", 1); + } + + @Test + public void missingKeyParameterStillSetsCorrectValue() { + parameters.put("request", "request"); + parameters.put("parameter", "page"); + plugin.execute(context, parameters); + verify(context).set("page", 1); + } + + @Test + public void unparseablePageSetsPageZero() { + parameters.put("parameter", "wrong-parameter"); + plugin.execute(context, parameters); + verify(context).set("page", 0); + } + +} diff --git a/src/test/java/net/pterodactylus/sone/template/HttpRequestAccessorTest.java b/src/test/java/net/pterodactylus/sone/template/HttpRequestAccessorTest.java new file mode 100644 index 0000000..37c6260 --- /dev/null +++ b/src/test/java/net/pterodactylus/sone/template/HttpRequestAccessorTest.java @@ -0,0 +1,51 @@ +package net.pterodactylus.sone.template; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import net.pterodactylus.util.template.TemplateContext; + +import freenet.support.api.HTTPRequest; + +import org.hamcrest.Matchers; +import org.junit.Before; +import org.junit.Test; + +/** + * Unit test for {@link HttpRequestAccessor}. + * + * @author David ‘Bombe’ Roden + */ +public class HttpRequestAccessorTest { + + private static final String REQUEST_PATH = "/the/real/path"; + private static final String USER_AGENT = "Test/1.0"; + private static final String HEADER_PATH = "/some/path"; + private final HttpRequestAccessor accessor = new HttpRequestAccessor(); + private final TemplateContext context = mock(TemplateContext.class); + private final HTTPRequest httpRequest = mock(HTTPRequest.class); + + @Before + public void setupHttpRequest() { + when(httpRequest.getPath()).thenReturn(REQUEST_PATH); + when(httpRequest.getHeader("User-Agent")).thenReturn(USER_AGENT); + when(httpRequest.getHeader("Path")).thenReturn(HEADER_PATH); + } + + @Test + public void preferCallingMethodsInsteadOfReturningHeaders() { + assertThat(accessor.get(context, httpRequest, "path"), + Matchers.is(REQUEST_PATH)); + verify(httpRequest, never()).getHeader("Path"); + } + + @Test + public void headerIsReturnedCorrectly() { + assertThat(accessor.get(context, httpRequest, "User-Agent"), + Matchers.is(USER_AGENT)); + } + +} diff --git a/src/test/java/net/pterodactylus/sone/template/IdentityAccessorTest.java b/src/test/java/net/pterodactylus/sone/template/IdentityAccessorTest.java new file mode 100644 index 0000000..33149e0 --- /dev/null +++ b/src/test/java/net/pterodactylus/sone/template/IdentityAccessorTest.java @@ -0,0 +1,81 @@ +package net.pterodactylus.sone.template; + +import static java.util.Arrays.asList; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.HashSet; +import java.util.Set; + +import net.pterodactylus.sone.core.Core; +import net.pterodactylus.sone.freenet.wot.Identity; +import net.pterodactylus.sone.freenet.wot.IdentityManager; +import net.pterodactylus.sone.freenet.wot.OwnIdentity; + +import org.hamcrest.Matchers; +import org.junit.Before; +import org.junit.Test; + +/** + * Unit test for {@link IdentityAccessor}. + * + * @author David ‘Bombe’ Roden + */ +public class IdentityAccessorTest { + + private static final String TEST_ID = + "LrNQbyBBZW-7pHqChtp9lfPA7eXFPW~FLbJ2WrvEx5g"; + private static final String TEST_ID_WITH_CHANGED_LETTER = + "LrMQbyBBZW-7pHqChtp9lfPA7eXFPW~FLbJ2WrvEx5g"; + private final Core core = mock(Core.class); + private final IdentityAccessor accessor = new IdentityAccessor(core); + private final IdentityManager identityManager = + mock(IdentityManager.class); + private final OwnIdentity identity = mock(OwnIdentity.class); + + @Before + public void setupCore() { + when(core.getIdentityManager()).thenReturn(identityManager); + } + + @Before + public void setupIdentity() { + setupIdentity(identity, TEST_ID, "Test"); + } + + private void setupIdentity(Identity identity, String id, + String nickname) { + when(identity.getId()).thenReturn(id); + when(identity.getNickname()).thenReturn(nickname); + } + + private void serveIdentities(Set identities) { + when(identityManager.getAllOwnIdentities()).thenReturn(identities); + } + + @Test + public void accessorReturnsTheCorrectlyAbbreviatedNickname() { + OwnIdentity ownIdentity = mock(OwnIdentity.class); + setupIdentity(ownIdentity, TEST_ID_WITH_CHANGED_LETTER, "Test"); + serveIdentities(new HashSet(asList(identity, ownIdentity))); + assertThat(accessor.get(null, identity, "uniqueNickname"), + Matchers.is("Test@LrN")); + } + + @Test + public void accessorComparesTheFullLengthIfNecessary() { + OwnIdentity ownIdentity = mock(OwnIdentity.class); + setupIdentity(ownIdentity, TEST_ID, "Test"); + serveIdentities(new HashSet(asList(identity, ownIdentity))); + assertThat(accessor.get(null, identity, "uniqueNickname"), + Matchers.is("Test@" + TEST_ID)); + } + + @Test + public void reflectionAccessorIsUsedForOtherMembers() { + assertThat(accessor.get(null, identity, "hashCode"), + Matchers.is(identity.hashCode())); + } + +} diff --git a/src/test/java/net/pterodactylus/sone/text/SoneTextParserTest.java b/src/test/java/net/pterodactylus/sone/text/SoneTextParserTest.java index 2c04b23..2ac6db7 100644 --- a/src/test/java/net/pterodactylus/sone/text/SoneTextParserTest.java +++ b/src/test/java/net/pterodactylus/sone/text/SoneTextParserTest.java @@ -22,13 +22,14 @@ import java.io.StringReader; import java.util.Arrays; import java.util.Collection; -import com.google.common.base.Optional; - -import junit.framework.TestCase; import net.pterodactylus.sone.data.Sone; -import net.pterodactylus.sone.data.SoneImpl; +import net.pterodactylus.sone.data.impl.IdOnlySone; import net.pterodactylus.sone.database.SoneProvider; +import com.google.common.base.Function; +import com.google.common.base.Optional; +import junit.framework.TestCase; + /** * JUnit test case for {@link SoneTextParser}. * @@ -181,21 +182,22 @@ public class SoneTextParserTest extends TestCase { */ private static class TestSoneProvider implements SoneProvider { + @Override + public Function> soneLoader() { + return new Function>() { + @Override + public Optional apply(String soneId) { + return getSone(soneId); + } + }; + } + /** * {@inheritDoc} */ @Override public Optional getSone(final String soneId) { - return Optional.of(new SoneImpl(soneId, false) { - - /** - * {@inheritDoc} - */ - @Override - public String getName() { - return soneId; - } - }); + return Optional.of(new IdOnlySone(soneId)); } /** diff --git a/src/test/java/net/pterodactylus/sone/utils/DefaultOptionTest.java b/src/test/java/net/pterodactylus/sone/utils/DefaultOptionTest.java new file mode 100644 index 0000000..065e1d4 --- /dev/null +++ b/src/test/java/net/pterodactylus/sone/utils/DefaultOptionTest.java @@ -0,0 +1,87 @@ +package net.pterodactylus.sone.utils; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; + +import javax.annotation.Nullable; + +import com.google.common.base.Predicate; +import org.junit.Test; + +/** + * Unit test for {@link DefaultOption}. + * + * @author David ‘Bombe’ Roden + */ +public class DefaultOptionTest { + + private final Object defaultValue = new Object(); + private final Object acceptedValue = new Object(); + private final Predicate matchesAcceptedValue = new Predicate() { + @Override + public boolean apply(@Nullable Object object) { + return acceptedValue.equals(object); + } + }; + + @Test + public void defaultOptionReturnsDefaultValueWhenUnset() { + DefaultOption defaultOption = new DefaultOption(defaultValue); + assertThat(defaultOption.get(), is(defaultValue)); + } + + @Test + public void defaultOptionReturnsNullForRealWhenUnset() { + DefaultOption defaultOption = new DefaultOption(defaultValue); + assertThat(defaultOption.getReal(), nullValue()); + } + + @Test + public void defaultOptionWillReturnSetValue() { + DefaultOption defaultOption = new DefaultOption(defaultValue); + Object newValue = new Object(); + defaultOption.set(newValue); + assertThat(defaultOption.get(), is(newValue)); + } + + @Test + public void defaultOptionWithValidatorAcceptsValidValues() { + DefaultOption defaultOption = new DefaultOption(defaultValue, matchesAcceptedValue); + defaultOption.set(acceptedValue); + assertThat(defaultOption.get(), is(acceptedValue)); + } + + @Test(expected = IllegalArgumentException.class) + public void defaultOptionWithValidatorRejectsInvalidValues() { + DefaultOption defaultOption = new DefaultOption(defaultValue, matchesAcceptedValue); + defaultOption.set(new Object()); + } + + @Test + public void defaultOptionValidatesObjectsCorrectly() { + DefaultOption defaultOption = new DefaultOption(defaultValue, matchesAcceptedValue); + assertThat(defaultOption.validate(acceptedValue), is(true)); + assertThat(defaultOption.validate(new Object()), is(false)); + } + + @Test + public void settingToNullWillRestoreDefaultValue() { + DefaultOption defaultOption = new DefaultOption(defaultValue); + defaultOption.set(null); + assertThat(defaultOption.get(), is(defaultValue)); + } + + @Test + public void validateWithoutValidatorWillValidateNull() { + DefaultOption defaultOption = new DefaultOption(defaultValue); + assertThat(defaultOption.validate(null), is(true)); + } + + @Test + public void validateWithValidatorWillValidateNull() { + DefaultOption defaultOption = new DefaultOption(defaultValue, matchesAcceptedValue); + assertThat(defaultOption.validate(null), is(true)); + } + +} diff --git a/src/test/java/net/pterodactylus/sone/utils/IntegerRangePredicateTest.java b/src/test/java/net/pterodactylus/sone/utils/IntegerRangePredicateTest.java new file mode 100644 index 0000000..b2d078e --- /dev/null +++ b/src/test/java/net/pterodactylus/sone/utils/IntegerRangePredicateTest.java @@ -0,0 +1,55 @@ +package net.pterodactylus.sone.utils; + +import static net.pterodactylus.sone.utils.IntegerRangePredicate.range; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +import net.pterodactylus.sone.TestUtil; + +import org.junit.Test; + +/** + * Unit test for {@link IntegerRangePredicate}. + * + * @author David ‘Bombe’ Roden + */ +public class IntegerRangePredicateTest { + + private final IntegerRangePredicate predicate = + new IntegerRangePredicate(-50, 50); + + @Test + public void predicateMatchesNumberWithinBounds() { + assertThat(predicate.apply(17), is(true)); + } + + @Test + public void predicateMatchesLowerBoundary() { + assertThat(predicate.apply(-50), is(true)); + } + + @Test + public void predicateDoesNotMatchOneBelowLowerBoundary() { + assertThat(predicate.apply(-51), is(false)); + } + + @Test + public void predicateMatchesUpperBoundary() { + assertThat(predicate.apply(50), is(true)); + } + + @Test + public void predicateDoesNotMatchesOneAboveUpperBoundary() { + assertThat(predicate.apply(51), is(false)); + } + + @Test + public void staticCreatorMethodCreatesPredicate() { + IntegerRangePredicate predicate = range(-50, 50); + assertThat(TestUtil.getPrivateField(predicate, "lowerBound"), + is(-50)); + assertThat(TestUtil.getPrivateField(predicate, "upperBound"), + is(50)); + } + +} diff --git a/src/test/java/net/pterodactylus/sone/utils/NumberParsersTest.java b/src/test/java/net/pterodactylus/sone/utils/NumberParsersTest.java new file mode 100644 index 0000000..00c2263 --- /dev/null +++ b/src/test/java/net/pterodactylus/sone/utils/NumberParsersTest.java @@ -0,0 +1,84 @@ +package net.pterodactylus.sone.utils; + +import static net.pterodactylus.sone.utils.NumberParsers.parseInt; +import static net.pterodactylus.sone.utils.NumberParsers.parseLong; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; + +import org.junit.Test; + +/** + * Unit test for {@link NumberParsers}. + * + * @author David ‘Bombe’ Roden + */ +public class NumberParsersTest { + + @Test + // yes, this test is for coverage only. + public void constructorCanBeCalled() { + new NumberParsers(); + } + + @Test + public void nullIsParsedToDefaultInt() { + assertThat(parseInt(null, 17), is(17)); + } + + @Test + public void notANumberIsParsedToDefaultInt() { + assertThat(parseInt("not a number", 18), is(18)); + } + + @Test + public void intIsCorrectlyParsed() { + assertThat(parseInt("19", 0), is(19)); + } + + @Test + public void valueTooLargeForIntIsParsedToDefault() { + assertThat(parseInt("2147483648", 20), is(20)); + } + + @Test + public void valueTooSmallForIntIsParsedToDefault() { + assertThat(parseInt("-2147483649", 20), is(20)); + } + + @Test + public void nullCanBeDefaultIntValue() { + assertThat(parseInt("not a number", null), nullValue()); + } + + @Test + public void nullIsParsedToDefaultLong() { + assertThat(parseLong(null, 17L), is(17L)); + } + + @Test + public void notANumberIsParsedToDefaultLong() { + assertThat(parseLong("not a number", 18L), is(18L)); + } + + @Test + public void LongIsCorrectlyParsed() { + assertThat(parseLong("19", 0L), is(19L)); + } + + @Test + public void valueTooLargeForLongIsParsedToDefault() { + assertThat(parseLong("9223372036854775808", 20L), is(20L)); + } + + @Test + public void valueTooSmallForLongIsParsedToDefault() { + assertThat(parseLong("-9223372036854775809", 20L), is(20L)); + } + + @Test + public void nullCanBeDefaultLongValue() { + assertThat(parseLong("not a number", null), nullValue()); + } + +} diff --git a/src/test/java/net/pterodactylus/sone/utils/OptionalsTest.java b/src/test/java/net/pterodactylus/sone/utils/OptionalsTest.java new file mode 100644 index 0000000..0f7dee3 --- /dev/null +++ b/src/test/java/net/pterodactylus/sone/utils/OptionalsTest.java @@ -0,0 +1,52 @@ +package net.pterodactylus.sone.utils; + +import java.util.Arrays; +import java.util.List; + +import com.google.common.base.Optional; +import com.google.common.collect.FluentIterable; +import org.hamcrest.MatcherAssert; +import org.hamcrest.Matchers; +import org.junit.Test; + +/** + * Unit test for {@link Optionals}. + * + * @author David ‘Bombe’ Roden + */ +public class OptionalsTest { + + private final Object object1 = new Object(); + private final Object object2 = new Object(); + private final Object object3 = new Object(); + + @Test + public void canCreateOptionals() { + new Optionals(); + } + + @Test + public void isPresentFiltersCorrectOptionals() { + List> optionals = Arrays.asList( + Optional.of(object1), Optional.absent(), + Optional.of(object2), Optional.absent(), + Optional.of(object3), Optional.absent() + ); + List> filteredOptionals = + FluentIterable.from(optionals).filter(Optionals.isPresent()).toList(); + MatcherAssert.assertThat(filteredOptionals, Matchers.contains( + Optional.of(object1), Optional.of(object2), Optional.of(object3))); + } + + @Test + public void getReturnsCorrectValues() { + List> optionals = Arrays.asList( + Optional.of(object1), + Optional.of(object2), + Optional.of(object3) + ); + List objects = FluentIterable.from(optionals).transform(Optionals.get()).toList(); + MatcherAssert.assertThat(objects, Matchers.contains(object1, object2, object3)); + } + +} diff --git a/src/test/java/net/pterodactylus/sone/web/ajax/BookmarkAjaxPageTest.java b/src/test/java/net/pterodactylus/sone/web/ajax/BookmarkAjaxPageTest.java index 1db89d0..5f2025f 100644 --- a/src/test/java/net/pterodactylus/sone/web/ajax/BookmarkAjaxPageTest.java +++ b/src/test/java/net/pterodactylus/sone/web/ajax/BookmarkAjaxPageTest.java @@ -4,10 +4,13 @@ package net.pterodactylus.sone.web.ajax; +import static com.google.common.base.Optional.of; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.notNullValue; import static org.junit.Assert.assertThat; +import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.argThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; @@ -18,6 +21,7 @@ import java.net.URI; import java.net.URISyntaxException; import net.pterodactylus.sone.core.Core; +import net.pterodactylus.sone.data.Post; import net.pterodactylus.sone.web.WebInterface; import net.pterodactylus.sone.web.page.FreenetRequest; @@ -36,6 +40,8 @@ public class BookmarkAjaxPageTest { public void testBookmarkingExistingPost() throws URISyntaxException { /* create mocks. */ Core core = mock(Core.class); + Post post = mock(Post.class); + when(core.getPost("abc")).thenReturn(of(post)); WebInterface webInterface = mock(WebInterface.class); when(webInterface.getCore()).thenReturn(core); HTTPRequest httpRequest = new HTTPRequestImpl(new URI("/ajax/bookmark.ajax?post=abc"), "GET"); @@ -51,8 +57,7 @@ public class BookmarkAjaxPageTest { assertThat(jsonReturnObject.isSuccess(), is(true)); /* verify behaviour. */ - verify(core, times(1)).bookmarkPost(anyString()); - verify(core).bookmarkPost("abc"); + verify(core).bookmarkPost(post); } @Test @@ -75,7 +80,7 @@ public class BookmarkAjaxPageTest { assertThat(((JsonErrorReturnObject) jsonReturnObject).getError(), is("invalid-post-id")); /* verify behaviour. */ - verify(core, never()).bookmarkPost(anyString()); + verify(core, never()).bookmarkPost(any(Post.class)); } } diff --git a/src/test/resources/net/pterodactylus/sone/core/sone-inserter-faulty-manifest.txt b/src/test/resources/net/pterodactylus/sone/core/sone-inserter-faulty-manifest.txt new file mode 100644 index 0000000..7d39bdc --- /dev/null +++ b/src/test/resources/net/pterodactylus/sone/core/sone-inserter-faulty-manifest.txt @@ -0,0 +1 @@ +<%include stuff> diff --git a/src/test/resources/net/pterodactylus/sone/core/sone-inserter-invalid-manifest.txt b/src/test/resources/net/pterodactylus/sone/core/sone-inserter-invalid-manifest.txt new file mode 100644 index 0000000..625c86f --- /dev/null +++ b/src/test/resources/net/pterodactylus/sone/core/sone-inserter-invalid-manifest.txt @@ -0,0 +1 @@ +Sone Version: <% version %> diff --git a/src/test/resources/net/pterodactylus/sone/core/sone-inserter-manifest.txt b/src/test/resources/net/pterodactylus/sone/core/sone-inserter-manifest.txt new file mode 100644 index 0000000..a5818bf --- /dev/null +++ b/src/test/resources/net/pterodactylus/sone/core/sone-inserter-manifest.txt @@ -0,0 +1,3 @@ +Sone Version: <% version> +Core Startup: <% core.startupTime> +Sone ID: <% currentSone.id> diff --git a/src/test/resources/net/pterodactylus/sone/core/sone-parser-missing-client-name.xml b/src/test/resources/net/pterodactylus/sone/core/sone-parser-missing-client-name.xml new file mode 100644 index 0000000..04aab22 --- /dev/null +++ b/src/test/resources/net/pterodactylus/sone/core/sone-parser-missing-client-name.xml @@ -0,0 +1,7 @@ + + + 0 + + + + diff --git a/src/test/resources/net/pterodactylus/sone/core/sone-parser-missing-client-version.xml b/src/test/resources/net/pterodactylus/sone/core/sone-parser-missing-client-version.xml new file mode 100644 index 0000000..cfcb3e6 --- /dev/null +++ b/src/test/resources/net/pterodactylus/sone/core/sone-parser-missing-client-version.xml @@ -0,0 +1,9 @@ + + + 0 + + some-client + + + + diff --git a/src/test/resources/net/pterodactylus/sone/core/sone-parser-missing-protocol-version.xml b/src/test/resources/net/pterodactylus/sone/core/sone-parser-missing-protocol-version.xml new file mode 100644 index 0000000..9a60f4f --- /dev/null +++ b/src/test/resources/net/pterodactylus/sone/core/sone-parser-missing-protocol-version.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/src/test/resources/net/pterodactylus/sone/core/sone-parser-negative-protocol-version.xml b/src/test/resources/net/pterodactylus/sone/core/sone-parser-negative-protocol-version.xml new file mode 100644 index 0000000..2907965 --- /dev/null +++ b/src/test/resources/net/pterodactylus/sone/core/sone-parser-negative-protocol-version.xml @@ -0,0 +1,4 @@ + + + -1 + diff --git a/src/test/resources/net/pterodactylus/sone/core/sone-parser-no-payload.xml b/src/test/resources/net/pterodactylus/sone/core/sone-parser-no-payload.xml new file mode 100644 index 0000000..62db3ab --- /dev/null +++ b/src/test/resources/net/pterodactylus/sone/core/sone-parser-no-payload.xml @@ -0,0 +1,6 @@ + + + 0 + + + diff --git a/src/test/resources/net/pterodactylus/sone/core/sone-parser-no-profile.xml b/src/test/resources/net/pterodactylus/sone/core/sone-parser-no-profile.xml new file mode 100644 index 0000000..b178772 --- /dev/null +++ b/src/test/resources/net/pterodactylus/sone/core/sone-parser-no-profile.xml @@ -0,0 +1,5 @@ + + + 0 + + diff --git a/src/test/resources/net/pterodactylus/sone/core/sone-parser-no-time.xml b/src/test/resources/net/pterodactylus/sone/core/sone-parser-no-time.xml new file mode 100644 index 0000000..0ce3565 --- /dev/null +++ b/src/test/resources/net/pterodactylus/sone/core/sone-parser-no-time.xml @@ -0,0 +1,4 @@ + + + 0 + diff --git a/src/test/resources/net/pterodactylus/sone/core/sone-parser-not-xml.xml b/src/test/resources/net/pterodactylus/sone/core/sone-parser-not-xml.xml new file mode 100644 index 0000000..0ced647 --- /dev/null +++ b/src/test/resources/net/pterodactylus/sone/core/sone-parser-not-xml.xml @@ -0,0 +1 @@ +Not an XML file. diff --git a/src/test/resources/net/pterodactylus/sone/core/sone-parser-profile-duplicate-field-name.xml b/src/test/resources/net/pterodactylus/sone/core/sone-parser-profile-duplicate-field-name.xml new file mode 100644 index 0000000..600886f --- /dev/null +++ b/src/test/resources/net/pterodactylus/sone/core/sone-parser-profile-duplicate-field-name.xml @@ -0,0 +1,17 @@ + + + 0 + + + + + field + value + + + field + value + + + + diff --git a/src/test/resources/net/pterodactylus/sone/core/sone-parser-profile-empty-field-name.xml b/src/test/resources/net/pterodactylus/sone/core/sone-parser-profile-empty-field-name.xml new file mode 100644 index 0000000..a4abec4 --- /dev/null +++ b/src/test/resources/net/pterodactylus/sone/core/sone-parser-profile-empty-field-name.xml @@ -0,0 +1,12 @@ + + + 0 + + + + + + + + + diff --git a/src/test/resources/net/pterodactylus/sone/core/sone-parser-profile-missing-field-name.xml b/src/test/resources/net/pterodactylus/sone/core/sone-parser-profile-missing-field-name.xml new file mode 100644 index 0000000..87ccb9f --- /dev/null +++ b/src/test/resources/net/pterodactylus/sone/core/sone-parser-profile-missing-field-name.xml @@ -0,0 +1,10 @@ + + + 0 + + + + + + + diff --git a/src/test/resources/net/pterodactylus/sone/core/sone-parser-time-not-numeric.xml b/src/test/resources/net/pterodactylus/sone/core/sone-parser-time-not-numeric.xml new file mode 100644 index 0000000..367ef55 --- /dev/null +++ b/src/test/resources/net/pterodactylus/sone/core/sone-parser-time-not-numeric.xml @@ -0,0 +1,5 @@ + + + 0 + + diff --git a/src/test/resources/net/pterodactylus/sone/core/sone-parser-too-large-protocol-version.xml b/src/test/resources/net/pterodactylus/sone/core/sone-parser-too-large-protocol-version.xml new file mode 100644 index 0000000..c43c607 --- /dev/null +++ b/src/test/resources/net/pterodactylus/sone/core/sone-parser-too-large-protocol-version.xml @@ -0,0 +1,4 @@ + + + 99999 + diff --git a/src/test/resources/net/pterodactylus/sone/core/sone-parser-with-client-info.xml b/src/test/resources/net/pterodactylus/sone/core/sone-parser-with-client-info.xml new file mode 100644 index 0000000..0cf4377 --- /dev/null +++ b/src/test/resources/net/pterodactylus/sone/core/sone-parser-with-client-info.xml @@ -0,0 +1,10 @@ + + + 0 + + some-client + some-version + + + + diff --git a/src/test/resources/net/pterodactylus/sone/core/sone-parser-with-image.xml b/src/test/resources/net/pterodactylus/sone/core/sone-parser-with-image.xml new file mode 100644 index 0000000..40b221f --- /dev/null +++ b/src/test/resources/net/pterodactylus/sone/core/sone-parser-with-image.xml @@ -0,0 +1,32 @@ + + + 0 + + + image-id + + + + album-id-1 + album-title + album-description + + + image-id + 1407197508000 + KSK@GPLv3.txt + image-title + image-description + 1920 + 1080 + + + + + album-id-2 + album-id-1 + album-title-2 + album-description-2 + + + diff --git a/src/test/resources/net/pterodactylus/sone/core/sone-parser-with-invalid-image-height.xml b/src/test/resources/net/pterodactylus/sone/core/sone-parser-with-invalid-image-height.xml new file mode 100644 index 0000000..5344499 --- /dev/null +++ b/src/test/resources/net/pterodactylus/sone/core/sone-parser-with-invalid-image-height.xml @@ -0,0 +1,29 @@ + + + 0 + + + + + album-id-1 + album-title + album-description + + + image-id + 1407197508000 + KSK@GPLv3.txt + image-title + 1920 + -1080 + + + + + album-id-2 + album-id-1 + album-title-2 + album-description-2 + + + diff --git a/src/test/resources/net/pterodactylus/sone/core/sone-parser-with-invalid-image-width.xml b/src/test/resources/net/pterodactylus/sone/core/sone-parser-with-invalid-image-width.xml new file mode 100644 index 0000000..5a0ae77 --- /dev/null +++ b/src/test/resources/net/pterodactylus/sone/core/sone-parser-with-invalid-image-width.xml @@ -0,0 +1,29 @@ + + + 0 + + + + + album-id-1 + album-title + album-description + + + image-id + 1407197508000 + KSK@GPLv3.txt + image-title + -1920 + 1080 + + + + + album-id-2 + album-id-1 + album-title-2 + album-description-2 + + + diff --git a/src/test/resources/net/pterodactylus/sone/core/sone-parser-with-invalid-parent-album-id.xml b/src/test/resources/net/pterodactylus/sone/core/sone-parser-with-invalid-parent-album-id.xml new file mode 100644 index 0000000..af2a7eb --- /dev/null +++ b/src/test/resources/net/pterodactylus/sone/core/sone-parser-with-invalid-parent-album-id.xml @@ -0,0 +1,14 @@ + + + 0 + + + + + album-id-1 + album-id-7 + album-title + album-description + + + diff --git a/src/test/resources/net/pterodactylus/sone/core/sone-parser-with-invalid-post-reply-time.xml b/src/test/resources/net/pterodactylus/sone/core/sone-parser-with-invalid-post-reply-time.xml new file mode 100644 index 0000000..751407a --- /dev/null +++ b/src/test/resources/net/pterodactylus/sone/core/sone-parser-with-invalid-post-reply-time.xml @@ -0,0 +1,14 @@ + + + 0 + + + + + reply-id + post-id + + reply-text + + + diff --git a/src/test/resources/net/pterodactylus/sone/core/sone-parser-with-invalid-post-time.xml b/src/test/resources/net/pterodactylus/sone/core/sone-parser-with-invalid-post-time.xml new file mode 100644 index 0000000..ad4df33 --- /dev/null +++ b/src/test/resources/net/pterodactylus/sone/core/sone-parser-with-invalid-post-time.xml @@ -0,0 +1,13 @@ + + + 0 + + + + + post-id + + text + + + diff --git a/src/test/resources/net/pterodactylus/sone/core/sone-parser-with-invalid-recipient.xml b/src/test/resources/net/pterodactylus/sone/core/sone-parser-with-invalid-recipient.xml new file mode 100644 index 0000000..e627651 --- /dev/null +++ b/src/test/resources/net/pterodactylus/sone/core/sone-parser-with-invalid-recipient.xml @@ -0,0 +1,14 @@ + + + 0 + + + + + post-id + + text + 123456789012345678901234567890123456789012 + + + diff --git a/src/test/resources/net/pterodactylus/sone/core/sone-parser-with-liked-post-ids.xml b/src/test/resources/net/pterodactylus/sone/core/sone-parser-with-liked-post-ids.xml new file mode 100644 index 0000000..16a5af0 --- /dev/null +++ b/src/test/resources/net/pterodactylus/sone/core/sone-parser-with-liked-post-ids.xml @@ -0,0 +1,9 @@ + + + 0 + + + + liked-post-id + + diff --git a/src/test/resources/net/pterodactylus/sone/core/sone-parser-with-liked-post-reply-ids.xml b/src/test/resources/net/pterodactylus/sone/core/sone-parser-with-liked-post-reply-ids.xml new file mode 100644 index 0000000..592da4c --- /dev/null +++ b/src/test/resources/net/pterodactylus/sone/core/sone-parser-with-liked-post-reply-ids.xml @@ -0,0 +1,9 @@ + + + 0 + + + + liked-post-reply-id + + diff --git a/src/test/resources/net/pterodactylus/sone/core/sone-parser-with-multiple-albums.xml b/src/test/resources/net/pterodactylus/sone/core/sone-parser-with-multiple-albums.xml new file mode 100644 index 0000000..f27edab --- /dev/null +++ b/src/test/resources/net/pterodactylus/sone/core/sone-parser-with-multiple-albums.xml @@ -0,0 +1,19 @@ + + + 0 + + + + + album-id-1 + album-title + album-description + + + album-id-2 + album-id-1 + album-title-2 + album-description-2 + + + diff --git a/src/test/resources/net/pterodactylus/sone/core/sone-parser-with-profile.xml b/src/test/resources/net/pterodactylus/sone/core/sone-parser-with-profile.xml new file mode 100644 index 0000000..a57a698 --- /dev/null +++ b/src/test/resources/net/pterodactylus/sone/core/sone-parser-with-profile.xml @@ -0,0 +1,13 @@ + + + 0 + + + first + middle + last + 18 + 12 + 1976 + + diff --git a/src/test/resources/net/pterodactylus/sone/core/sone-parser-with-recipient.xml b/src/test/resources/net/pterodactylus/sone/core/sone-parser-with-recipient.xml new file mode 100644 index 0000000..14dabe1 --- /dev/null +++ b/src/test/resources/net/pterodactylus/sone/core/sone-parser-with-recipient.xml @@ -0,0 +1,14 @@ + + + 0 + + + + + post-id + + text + 1234567890123456789012345678901234567890123 + + + diff --git a/src/test/resources/net/pterodactylus/sone/core/sone-parser-with-valid-post-reply-time.xml b/src/test/resources/net/pterodactylus/sone/core/sone-parser-with-valid-post-reply-time.xml new file mode 100644 index 0000000..4d77cb2 --- /dev/null +++ b/src/test/resources/net/pterodactylus/sone/core/sone-parser-with-valid-post-reply-time.xml @@ -0,0 +1,14 @@ + + + 0 + + + + + reply-id + post-id + + reply-text + + + diff --git a/src/test/resources/net/pterodactylus/sone/core/sone-parser-with-valid-post-time.xml b/src/test/resources/net/pterodactylus/sone/core/sone-parser-with-valid-post-time.xml new file mode 100644 index 0000000..fdfb493 --- /dev/null +++ b/src/test/resources/net/pterodactylus/sone/core/sone-parser-with-valid-post-time.xml @@ -0,0 +1,13 @@ + + + 0 + + + + + post-id + + text + + + diff --git a/src/test/resources/net/pterodactylus/sone/core/sone-parser-with-zero-time.xml b/src/test/resources/net/pterodactylus/sone/core/sone-parser-with-zero-time.xml new file mode 100644 index 0000000..90a4834 --- /dev/null +++ b/src/test/resources/net/pterodactylus/sone/core/sone-parser-with-zero-time.xml @@ -0,0 +1,6 @@ + + + 0 + + + diff --git a/src/test/resources/net/pterodactylus/sone/core/sone-parser-without-album-id.xml b/src/test/resources/net/pterodactylus/sone/core/sone-parser-without-album-id.xml new file mode 100644 index 0000000..f925496 --- /dev/null +++ b/src/test/resources/net/pterodactylus/sone/core/sone-parser-without-album-id.xml @@ -0,0 +1,10 @@ + + + 0 + + + + + + + diff --git a/src/test/resources/net/pterodactylus/sone/core/sone-parser-without-album-title.xml b/src/test/resources/net/pterodactylus/sone/core/sone-parser-without-album-title.xml new file mode 100644 index 0000000..bacddeb --- /dev/null +++ b/src/test/resources/net/pterodactylus/sone/core/sone-parser-without-album-title.xml @@ -0,0 +1,11 @@ + + + 0 + + + + + album-id-1 + + + diff --git a/src/test/resources/net/pterodactylus/sone/core/sone-parser-without-albums.xml b/src/test/resources/net/pterodactylus/sone/core/sone-parser-without-albums.xml new file mode 100644 index 0000000..ecf5d63 --- /dev/null +++ b/src/test/resources/net/pterodactylus/sone/core/sone-parser-without-albums.xml @@ -0,0 +1,8 @@ + + + 0 + + + + + diff --git a/src/test/resources/net/pterodactylus/sone/core/sone-parser-without-fields.xml b/src/test/resources/net/pterodactylus/sone/core/sone-parser-without-fields.xml new file mode 100644 index 0000000..09e8fab --- /dev/null +++ b/src/test/resources/net/pterodactylus/sone/core/sone-parser-without-fields.xml @@ -0,0 +1,8 @@ + + + 0 + + + + + diff --git a/src/test/resources/net/pterodactylus/sone/core/sone-parser-without-image-height.xml b/src/test/resources/net/pterodactylus/sone/core/sone-parser-without-image-height.xml new file mode 100644 index 0000000..62d49cd --- /dev/null +++ b/src/test/resources/net/pterodactylus/sone/core/sone-parser-without-image-height.xml @@ -0,0 +1,28 @@ + + + 0 + + + + + album-id-1 + album-title + album-description + + + image-id + 1407197508000 + KSK@GPLv3.txt + image-title + 1920 + + + + + album-id-2 + album-id-1 + album-title-2 + album-description-2 + + + diff --git a/src/test/resources/net/pterodactylus/sone/core/sone-parser-without-image-id.xml b/src/test/resources/net/pterodactylus/sone/core/sone-parser-without-image-id.xml new file mode 100644 index 0000000..d40c071 --- /dev/null +++ b/src/test/resources/net/pterodactylus/sone/core/sone-parser-without-image-id.xml @@ -0,0 +1,23 @@ + + + 0 + + + + + album-id-1 + album-title + album-description + + + + + + + album-id-2 + album-id-1 + album-title-2 + album-description-2 + + + diff --git a/src/test/resources/net/pterodactylus/sone/core/sone-parser-without-image-key.xml b/src/test/resources/net/pterodactylus/sone/core/sone-parser-without-image-key.xml new file mode 100644 index 0000000..ec53bca --- /dev/null +++ b/src/test/resources/net/pterodactylus/sone/core/sone-parser-without-image-key.xml @@ -0,0 +1,25 @@ + + + 0 + + + + + album-id-1 + album-title + album-description + + + image-id + 1407197508000 + + + + + album-id-2 + album-id-1 + album-title-2 + album-description-2 + + + diff --git a/src/test/resources/net/pterodactylus/sone/core/sone-parser-without-image-time.xml b/src/test/resources/net/pterodactylus/sone/core/sone-parser-without-image-time.xml new file mode 100644 index 0000000..271e341 --- /dev/null +++ b/src/test/resources/net/pterodactylus/sone/core/sone-parser-without-image-time.xml @@ -0,0 +1,24 @@ + + + 0 + + + + + album-id-1 + album-title + album-description + + + image-id + + + + + album-id-2 + album-id-1 + album-title-2 + album-description-2 + + + diff --git a/src/test/resources/net/pterodactylus/sone/core/sone-parser-without-image-title.xml b/src/test/resources/net/pterodactylus/sone/core/sone-parser-without-image-title.xml new file mode 100644 index 0000000..bc1ce10 --- /dev/null +++ b/src/test/resources/net/pterodactylus/sone/core/sone-parser-without-image-title.xml @@ -0,0 +1,26 @@ + + + 0 + + + + + album-id-1 + album-title + album-description + + + image-id + 1407197508000 + KSK@GPLv3.txt + + + + + album-id-2 + album-id-1 + album-title-2 + album-description-2 + + + diff --git a/src/test/resources/net/pterodactylus/sone/core/sone-parser-without-image-width.xml b/src/test/resources/net/pterodactylus/sone/core/sone-parser-without-image-width.xml new file mode 100644 index 0000000..8426101 --- /dev/null +++ b/src/test/resources/net/pterodactylus/sone/core/sone-parser-without-image-width.xml @@ -0,0 +1,27 @@ + + + 0 + + + + + album-id-1 + album-title + album-description + + + image-id + 1407197508000 + KSK@GPLv3.txt + image-title + + + + + album-id-2 + album-id-1 + album-title-2 + album-description-2 + + + diff --git a/src/test/resources/net/pterodactylus/sone/core/sone-parser-without-images.xml b/src/test/resources/net/pterodactylus/sone/core/sone-parser-without-images.xml new file mode 100644 index 0000000..2b201e8 --- /dev/null +++ b/src/test/resources/net/pterodactylus/sone/core/sone-parser-without-images.xml @@ -0,0 +1,21 @@ + + + 0 + + + + + album-id-1 + album-title + album-description + + + + + album-id-2 + album-id-1 + album-title-2 + album-description-2 + + + diff --git a/src/test/resources/net/pterodactylus/sone/core/sone-parser-without-liked-post-ids.xml b/src/test/resources/net/pterodactylus/sone/core/sone-parser-without-liked-post-ids.xml new file mode 100644 index 0000000..a8b9a6c --- /dev/null +++ b/src/test/resources/net/pterodactylus/sone/core/sone-parser-without-liked-post-ids.xml @@ -0,0 +1,8 @@ + + + 0 + + + + + diff --git a/src/test/resources/net/pterodactylus/sone/core/sone-parser-without-liked-post-reply-ids.xml b/src/test/resources/net/pterodactylus/sone/core/sone-parser-without-liked-post-reply-ids.xml new file mode 100644 index 0000000..511e214 --- /dev/null +++ b/src/test/resources/net/pterodactylus/sone/core/sone-parser-without-liked-post-reply-ids.xml @@ -0,0 +1,8 @@ + + + 0 + + + + + diff --git a/src/test/resources/net/pterodactylus/sone/core/sone-parser-without-post-id.xml b/src/test/resources/net/pterodactylus/sone/core/sone-parser-without-post-id.xml new file mode 100644 index 0000000..637a51f --- /dev/null +++ b/src/test/resources/net/pterodactylus/sone/core/sone-parser-without-post-id.xml @@ -0,0 +1,10 @@ + + + 0 + + + + + + + diff --git a/src/test/resources/net/pterodactylus/sone/core/sone-parser-without-post-reply-id.xml b/src/test/resources/net/pterodactylus/sone/core/sone-parser-without-post-reply-id.xml new file mode 100644 index 0000000..c54fca0 --- /dev/null +++ b/src/test/resources/net/pterodactylus/sone/core/sone-parser-without-post-reply-id.xml @@ -0,0 +1,10 @@ + + + 0 + + + + + + + diff --git a/src/test/resources/net/pterodactylus/sone/core/sone-parser-without-post-reply-post-id.xml b/src/test/resources/net/pterodactylus/sone/core/sone-parser-without-post-reply-post-id.xml new file mode 100644 index 0000000..321d857 --- /dev/null +++ b/src/test/resources/net/pterodactylus/sone/core/sone-parser-without-post-reply-post-id.xml @@ -0,0 +1,11 @@ + + + 0 + + + + + reply-id + + + diff --git a/src/test/resources/net/pterodactylus/sone/core/sone-parser-without-post-reply-text.xml b/src/test/resources/net/pterodactylus/sone/core/sone-parser-without-post-reply-text.xml new file mode 100644 index 0000000..0ec68aa --- /dev/null +++ b/src/test/resources/net/pterodactylus/sone/core/sone-parser-without-post-reply-text.xml @@ -0,0 +1,13 @@ + + + 0 + + + + + reply-id + post-id + + + + diff --git a/src/test/resources/net/pterodactylus/sone/core/sone-parser-without-post-reply-time.xml b/src/test/resources/net/pterodactylus/sone/core/sone-parser-without-post-reply-time.xml new file mode 100644 index 0000000..ae6ffcb --- /dev/null +++ b/src/test/resources/net/pterodactylus/sone/core/sone-parser-without-post-reply-time.xml @@ -0,0 +1,12 @@ + + + 0 + + + + + reply-id + post-id + + + diff --git a/src/test/resources/net/pterodactylus/sone/core/sone-parser-without-post-text.xml b/src/test/resources/net/pterodactylus/sone/core/sone-parser-without-post-text.xml new file mode 100644 index 0000000..062e2f8 --- /dev/null +++ b/src/test/resources/net/pterodactylus/sone/core/sone-parser-without-post-text.xml @@ -0,0 +1,12 @@ + + + 0 + + + + + post-id + + + + diff --git a/src/test/resources/net/pterodactylus/sone/core/sone-parser-without-post-time.xml b/src/test/resources/net/pterodactylus/sone/core/sone-parser-without-post-time.xml new file mode 100644 index 0000000..83477e5 --- /dev/null +++ b/src/test/resources/net/pterodactylus/sone/core/sone-parser-without-post-time.xml @@ -0,0 +1,11 @@ + + + 0 + + + + + post-id + + +