<modelVersion>4.0.0</modelVersion>
<groupId>net.pterodactylus</groupId>
<artifactId>sone</artifactId>
- <version>0.8.9</version>
+ <version>0.9-rc1</version>
<dependencies>
<dependency>
<groupId>net.pterodactylus</groupId>
<scope>test</scope>
</dependency>
<dependency>
+ <groupId>org.hamcrest</groupId>
+ <artifactId>hamcrest-all</artifactId>
+ <version>1.3</version>
+ </dependency>
+ <dependency>
<groupId>org.freenetproject</groupId>
<artifactId>fred</artifactId>
- <version>0.7.5.1405</version>
+ <version>0.7.5.1467.99.3</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.freenetproject</groupId>
<artifactId>freenet-ext</artifactId>
- <version>26</version>
+ <version>29</version>
<scope>provided</scope>
</dependency>
<dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
- <version>14.0-rc1</version>
+ <version>14.0.1</version>
</dependency>
<dependency>
<groupId>commons-lang</groupId>
<footer>© 2010–2013 David ‘Bombe’ Roden</footer>
</configuration>
</plugin>
+ <plugin>
+ <groupId>org.jacoco</groupId>
+ <artifactId>jacoco-maven-plugin</artifactId>
+ <version>0.7.1.201405082137</version>
+ <executions>
+ <execution>
+ <id>default-prepare-agent</id>
+ <goals>
+ <goal>prepare-agent</goal>
+ </goals>
+ </execution>
+ <execution>
+ <id>default-report</id>
+ <phase>prepare-package</phase>
+ <goals>
+ <goal>report</goal>
+ </goals>
+ </execution>
+ <execution>
+ <id>default-check</id>
+ <goals>
+ <goal>check</goal>
+ </goals>
+ <configuration>
+ <rules>
+ <!-- implmentation is needed only for Maven 2 -->
+ <rule implementation="org.jacoco.maven.RuleConfiguration">
+ <element>BUNDLE</element>
+ <limits>
+ <!-- implmentation is needed only for Maven 2 -->
+ <limit implementation="org.jacoco.report.check.Limit">
+ <counter>COMPLEXITY</counter>
+ <value>COVEREDRATIO</value>
+ <minimum>0.60</minimum>
+ </limit>
+ </limits>
+ </rule>
+ </rules>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
</plugins>
</build>
</project>
--- /dev/null
+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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class ConfigurationSoneParser {
+
+ private final Configuration configuration;
+ private final Sone sone;
+ private final String sonePrefix;
+ private final Map<String, Album> albums = new HashMap<String, Album>();
+ private final List<Album> topLevelAlbums = new ArrayList<Album>();
+ private final Map<String, Image> images = new HashMap<String, Image>();
+
+ 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<Post> parsePosts(PostBuilderFactory postBuilderFactory)
+ throws InvalidPostFound {
+ Set<Post> posts = new HashSet<Post>();
+ 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<PostReply> parsePostReplies(
+ PostReplyBuilderFactory postReplyBuilderFactory) {
+ Set<PostReply> replies = new HashSet<PostReply>();
+ 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<String> parseLikedPostIds() {
+ Set<String> likedPostIds = new HashSet<String>();
+ while (true) {
+ String likedPostId =
+ getString("/Likes/Post/" + likedPostIds.size() + "/ID",
+ null);
+ if (likedPostId == null) {
+ break;
+ }
+ likedPostIds.add(likedPostId);
+ }
+ return likedPostIds;
+ }
+
+ public Set<String> parseLikedPostReplyIds() {
+ Set<String> likedPostReplyIds = new HashSet<String>();
+ while (true) {
+ String likedReplyId = getString(
+ "/Likes/Reply/" + likedPostReplyIds.size() + "/ID", null);
+ if (likedReplyId == null) {
+ break;
+ }
+ likedPostReplyIds.add(likedReplyId);
+ }
+ return likedPostReplyIds;
+ }
+
+ public Set<String> parseFriends() {
+ Set<String> friends = new HashSet<String>();
+ while (true) {
+ String friendId =
+ getString("/Friends/" + friends.size() + "/ID", null);
+ if (friendId == null) {
+ break;
+ }
+ friends.add(friendId);
+ }
+ return friends;
+ }
+
+ public List<Album> 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<String, Album> 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<String, Image> 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 { }
+
+}
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;
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;
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;
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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
*/
+@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;
/** The trust updater. */
private final WebOfTrustUpdater webOfTrustUpdater;
- /** The FCP interface. */
- private volatile FcpInterface fcpInterface;
-
/** The times Sones were followed. */
private final Map<String, Long> soneFollowingTimes = new HashMap<String, Long>();
/* synchronize access on this on sones. */
private final Map<Sone, SoneRescuer> soneRescuers = new HashMap<Sone, SoneRescuer>();
- /** All Sones. */
- /* synchronize access on this on itself. */
- private final Map<String, Sone> sones = new HashMap<String, Sone>();
-
/** All known Sones. */
private final Set<String> knownSones = new HashSet<String>();
/** The post database. */
private final Database database;
- /** All bookmarked posts. */
- /* synchronize access on itself. */
- private final Set<String> bookmarkedPosts = new HashSet<String>();
-
/** Trusted identities, sorted by own identities. */
private final Multimap<OwnIdentity, Identity> trustedIdentities = Multimaps.synchronizedSetMultimap(HashMultimap.<OwnIdentity, Identity>create());
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);
}
//
}
/**
- * 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
}
/**
- * 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
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);
}
}
+ public SoneBuilder soneBuilder() {
+ return database.newSoneBuilder();
+ }
+
/**
* {@inheritDocs}
*/
@Override
public Collection<Sone> getSones() {
- synchronized (sones) {
- return ImmutableSet.copyOf(sones.values());
- }
+ return database.getSones();
+ }
+
+ @Override
+ public Function<String, Optional<Sone>> soneLoader() {
+ return database.soneLoader();
}
/**
*/
@Override
public Optional<Sone> getSone(String id) {
- synchronized (sones) {
- return Optional.fromNullable(sones.get(id));
- }
+ return database.getSone(id);
}
/**
*/
@Override
public Collection<Sone> getLocalSones() {
- synchronized (sones) {
- return FluentIterable.from(sones.values()).filter(new Predicate<Sone>() {
-
- @Override
- public boolean apply(Sone sone) {
- return sone.isLocal();
- }
- }).toSet();
- }
+ return database.getLocalSones();
}
/**
*
* @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> sone = database.getSone(id);
+ if (sone.isPresent() && sone.get().isLocal()) {
+ return sone.get();
}
+ return null;
}
/**
*/
@Override
public Collection<Sone> getRemoteSones() {
- synchronized (sones) {
- return FluentIterable.from(sones.values()).filter(new Predicate<Sone>() {
-
- @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();
}
/**
* {@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();
}
/**
}
/**
- * 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
* 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);
}
/**
* @return All bookmarked posts
*/
public Set<Post> getBookmarkedPosts() {
- Set<Post> posts = new HashSet<Post>();
- synchronized (bookmarkedPosts) {
- for (String bookmarkedPostId : bookmarkedPosts) {
- Optional<Post> 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();
}
/**
*
* @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> 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();
}
/**
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;
}
/**
return null;
}
Sone sone = addLocalSone(ownIdentity);
- sone.getOptions().addBooleanOption("AutoFollow", new DefaultOption<Boolean>(false));
- sone.getOptions().addBooleanOption("EnableSoneInsertNotifications", new DefaultOption<Boolean>(false));
- sone.getOptions().addBooleanOption("ShowNotification/NewSones", new DefaultOption<Boolean>(true));
- sone.getOptions().addBooleanOption("ShowNotification/NewPosts", new DefaultOption<Boolean>(true));
- sone.getOptions().addBooleanOption("ShowNotification/NewReplies", new DefaultOption<Boolean>(true));
- sone.getOptions().addEnumOption("ShowCustomAvatars", new DefaultOption<ShowCustomAvatars>(ShowCustomAvatars.NEVER));
followSone(sone, "nwa8lHa271k2QvJ8aa0Ov7IHAV-DFOCFgmDt3X6BpCI");
touchConfiguration();
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<Sone> 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;
}
/**
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();
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);
* {@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<Sone> 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<Post> existingPosts = database.getPosts(sone.getId());
- for (Post oldPost : existingPosts) {
- if (!sone.getPosts().contains(oldPost)) {
- eventBus.post(new PostRemovedEvent(oldPost));
- }
+ List<Object> 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<PostReply> 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<Object> collectEventsForChangesInSone(Sone oldSone,
+ final Sone newSone) {
+ final List<Object> events = new ArrayList<Object>();
+ 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;
}
/**
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 {
}
logger.info(String.format("Loading local Sone: %s", sone));
- /* initialize options. */
- sone.getOptions().addBooleanOption("AutoFollow", new DefaultOption<Boolean>(false));
- sone.getOptions().addBooleanOption("EnableSoneInsertNotifications", new DefaultOption<Boolean>(false));
- sone.getOptions().addBooleanOption("ShowNotification/NewSones", new DefaultOption<Boolean>(true));
- sone.getOptions().addBooleanOption("ShowNotification/NewPosts", new DefaultOption<Boolean>(true));
- sone.getOptions().addBooleanOption("ShowNotification/NewReplies", new DefaultOption<Boolean>(true));
- sone.getOptions().addEnumOption("ShowCustomAvatars", new DefaultOption<ShowCustomAvatars>(ShowCustomAvatars.NEVER));
-
/* load Sone. */
String sonePrefix = "Sone/" + sone.getId();
Long soneTime = configuration.getLongValue(sonePrefix + "/Time").getValue(null);
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<Post> posts = new HashSet<Post>();
- 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<Post> posts;
+ try {
+ posts = configurationSoneParser.parsePosts(database);
+ } catch (InvalidPostFound ipf) {
+ logger.log(Level.WARNING, "Invalid post found, aborting load!");
+ return;
}
/* load replies. */
- Set<PostReply> replies = new HashSet<PostReply>();
- 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<PostReply> replies;
+ try {
+ replies = configurationSoneParser.parsePostReplies(database);
+ } catch (InvalidPostReplyFound iprf) {
+ logger.log(Level.WARNING, "Invalid reply found, aborting load!");
+ return;
}
/* load post likes. */
- Set<String> likedPostIds = new HashSet<String>();
- while (true) {
- String likedPostId = configuration.getStringValue(sonePrefix + "/Likes/Post/" + likedPostIds.size() + "/ID").getValue(null);
- if (likedPostId == null) {
- break;
- }
- likedPostIds.add(likedPostId);
- }
+ Set<String> likedPostIds =
+ configurationSoneParser.parseLikedPostIds();
/* load reply likes. */
- Set<String> likedReplyIds = new HashSet<String>();
- while (true) {
- String likedReplyId = configuration.getStringValue(sonePrefix + "/Likes/Reply/" + likedReplyIds.size() + "/ID").getValue(null);
- if (likedReplyId == null) {
- break;
- }
- likedReplyIds.add(likedReplyId);
- }
-
- /* load friends. */
- Set<String> friends = new HashSet<String>();
- while (true) {
- String friendId = configuration.getStringValue(sonePrefix + "/Friends/" + friends.size() + "/ID").getValue(null);
- if (friendId == null) {
- break;
- }
- friends.add(friendId);
- }
+ Set<String> likedReplyIds =
+ configurationSoneParser.parseLikedPostReplyIds();
/* load albums. */
- List<Album> topLevelAlbums = new ArrayList<Album>();
- 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<Album> 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<String, Image> 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().<ShowCustomAvatars> 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) {
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);
}
*
* @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
* @return The created post
*/
public Post createPost(Sone sone, Optional<Sone> recipient, String text) {
- return createPost(sone, recipient, System.currentTimeMillis(), text);
- }
-
- /**
- * Creates a new post.
- *
- * @param sone
- * The Sone that creates the post
- * @param recipient
- * The recipient Sone, or {@code null} if this post does not have
- * a recipient
- * @param time
- * The time of the post
- * @param text
- * The text of the post
- * @return The created post
- */
- public Post createPost(Sone sone, Optional<Sone> 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()) {
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());
}
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;
}
}
}
- /**
- * 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);
}
/**
* @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);
}
/**
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;
}
}
/**
- * 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
* @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;
}
* 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
*/
}
/**
- * 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
@Override
public void serviceStop() {
localElementTicker.shutdownNow();
- synchronized (sones) {
+ synchronized (soneInserters) {
for (Entry<Sone, SoneInserter> 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();
}
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<Album> albums = FluentIterable.from(sone.getRootAlbum().getAlbums()).transformAndConcat(Album.FLATTENER).toList();
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().<ShowCustomAvatars> 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();
/* 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;
/* 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();
* Loads the configuration.
*/
private void loadConfiguration() {
- /* create options. */
- options.addIntegerOption("InsertionDelay", new DefaultOption<Integer>(60, new IntegerRangePredicate(0, Integer.MAX_VALUE), new OptionWatcher<Integer>() {
-
- @Override
- public void optionChanged(Option<Integer> option, Integer oldValue, Integer newValue) {
- SoneInserter.setInsertionDelay(newValue);
- }
-
- }));
- options.addIntegerOption("PostsPerPage", new DefaultOption<Integer>(10, new IntegerRangePredicate(1, Integer.MAX_VALUE)));
- options.addIntegerOption("ImagesPerPage", new DefaultOption<Integer>(9, new IntegerRangePredicate(1, Integer.MAX_VALUE)));
- options.addIntegerOption("CharactersPerPost", new DefaultOption<Integer>(400, Predicates.<Integer> or(new IntegerRangePredicate(50, Integer.MAX_VALUE), Predicates.equalTo(-1))));
- options.addIntegerOption("PostCutOffLength", new DefaultOption<Integer>(200, Predicates.<Integer> or(new IntegerRangePredicate(50, Integer.MAX_VALUE), Predicates.equalTo(-1))));
- options.addBooleanOption("RequireFullAccess", new DefaultOption<Boolean>(false));
- options.addIntegerOption("PositiveTrust", new DefaultOption<Integer>(75, new IntegerRangePredicate(0, 100)));
- options.addIntegerOption("NegativeTrust", new DefaultOption<Integer>(-25, new IntegerRangePredicate(-100, 100)));
- options.addStringOption("TrustComment", new DefaultOption<String>("Set from Sone Web Interface"));
- options.addBooleanOption("ActivateFcpInterface", new DefaultOption<Boolean>(false, new OptionWatcher<Boolean>() {
-
- @Override
- @SuppressWarnings("synthetic-access")
- public void optionChanged(Option<Boolean> option, Boolean oldValue, Boolean newValue) {
- fcpInterface.setActive(newValue);
- }
- }));
- options.addIntegerOption("FcpFullAccessRequired", new DefaultOption<Integer>(2, new OptionWatcher<Integer>() {
-
- @Override
- @SuppressWarnings("synthetic-access")
- public void optionChanged(Option<Integer> 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;
}
++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));
- }
}
/**
*/
@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));
}
/**
OwnIdentity ownIdentity = identityRemovedEvent.ownIdentity();
Identity identity = identityRemovedEvent.identity();
trustedIdentities.remove(ownIdentity, identity);
- boolean foundIdentity = false;
for (Entry<OwnIdentity, Collection<Identity>> 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> 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()));
}
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);
+ }
+
+ }
+
}
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;
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;
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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
*/
+@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;
/** The not-Sone-related USK callbacks. */
private final Map<FreenetURI, USKCallback> uriUskCallbacks = Collections.synchronizedMap(new HashMap<FreenetURI, USKCallback>());
+ private final RequestClient imageInserts = new RequestClient() {
+ @Override
+ public boolean persistent() {
+ return false;
+ }
+
+ @Override
+ public boolean realTimeFlag() {
+ return true;
+ }
+ };
+
/**
* Creates a new Freenet interface.
*
* @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;
}
}
/**
- * 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.
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);
}
}
- /**
- * 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);
}
}
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);
}
};
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);
/** The client putter. */
private ClientPutter clientPutter;
+ private Bucket bucket;
/** The final URI. */
private volatile FreenetURI resultingUri;
eventBus.post(new ImageInsertStartedEvent(image));
}
+ public void setBucket(Bucket bucket) {
+ this.bucket = bucket;
+ }
+
//
// ACTIONS
//
*/
@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. */
}
/**
*/
@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. */
}
* {@inheritDoc}
*/
@Override
- public void onGeneratedMetadata(Bucket metadata, BaseClientPutter clientPutter, ObjectContainer objectContainer) {
+ public void onGeneratedMetadata(Bucket metadata, BaseClientPutter clientPutter) {
/* ignore, we don’t care. */
}
* {@inheritDoc}
*/
@Override
- public void onGeneratedURI(FreenetURI generatedUri, BaseClientPutter clientPutter, ObjectContainer objectContainer) {
+ public void onGeneratedURI(FreenetURI generatedUri, BaseClientPutter clientPutter) {
resultingUri = generatedUri;
}
*/
@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<Image, InsertToken> {
+
+ @Override
+ public InsertToken apply(Image image) {
+ return new InsertToken(image);
}
}
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;
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
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<Image, InsertToken> insertTokenSupplier;
/** The tokens of running inserts. */
private final Map<String, InsertToken> insertTokens = Collections.synchronizedMap(new HashMap<String, InsertToken>());
*
* @param freenetInterface
* The freenet interface
+ * @param insertTokenSupplier
+ * The supplier for insert tokens
*/
- public ImageInserter(FreenetInterface freenetInterface) {
+ public ImageInserter(FreenetInterface freenetInterface, Function<Image, InsertToken> insertTokenSupplier) {
this.freenetInterface = freenetInterface;
+ this.insertTokenSupplier = insertTokenSupplier;
}
/**
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) {
import java.util.HashMap;
import java.util.Map;
+import net.pterodactylus.sone.utils.Option;
+
import com.google.common.base.Predicate;
/**
*/
public class Options {
- /**
- * Contains current and default value of an option.
- *
- * @param <T>
- * The type of the option
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
- public static interface Option<T> {
-
- /**
- * 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 <T>
- * The type of the option
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
- public static interface OptionWatcher<T> {
-
- /**
- * 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<T> option, T oldValue, T newValue);
-
- }
-
- /**
- * Basic implementation of an {@link Option} that notifies an
- * {@link OptionWatcher} if the value changes.
- *
- * @param <T>
- * The type of the option
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
- public static class DefaultOption<T> implements Option<T> {
-
- /** The default value. */
- private final T defaultValue;
-
- /** The current value. */
- private volatile T value;
-
- /** The validator. */
- private Predicate<T> validator;
-
- /** The option watcher. */
- private final OptionWatcher<T> optionWatcher;
-
- /**
- * Creates a new default option.
- *
- * @param defaultValue
- * The default value of the option
- */
- public DefaultOption(T defaultValue) {
- this(defaultValue, (OptionWatcher<T>) 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<T> 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<T> 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<T> validator, OptionWatcher<T> 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<String, Option<Boolean>> booleanOptions = Collections.synchronizedMap(new HashMap<String, Option<Boolean>>());
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
*/
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<Integer> insertionDelay =
+ new DefaultOption<Integer>(60, range(0, MAX_VALUE));
+ private final Option<Integer> postsPerPage =
+ new DefaultOption<Integer>(10, range(1, MAX_VALUE));
+ private final Option<Integer> imagesPerPage =
+ new DefaultOption<Integer>(9, range(1, MAX_VALUE));
+ private final Option<Integer> charactersPerPost =
+ new DefaultOption<Integer>(400, Predicates.<Integer>or(
+ range(50, MAX_VALUE), equalTo(-1)));
+ private final Option<Integer> postCutOffLength =
+ new DefaultOption<Integer>(200, range(50, MAX_VALUE));
+ private final Option<Boolean> requireFullAccess =
+ new DefaultOption<Boolean>(false);
+ private final Option<Integer> positiveTrust =
+ new DefaultOption<Integer>(75, range(0, 100));
+ private final Option<Integer> negativeTrust =
+ new DefaultOption<Integer>(-25, range(-100, 100));
+ private final Option<String> trustComment =
+ new DefaultOption<String>("Set from Sone Web Interface");
+ private final Option<Boolean> activateFcpInterface =
+ new DefaultOption<Boolean>(false);
+ private final Option<FullAccessRequired> fcpFullAccessRequired =
+ new DefaultOption<FullAccessRequired>(ALWAYS);
+
+ public Preferences(EventBus eventBus) {
+ this.eventBus = eventBus;
}
/**
* @return The insertion delay
*/
public int getInsertionDelay() {
- return options.getIntegerOption("InsertionDelay").get();
+ return insertionDelay.get();
}
/**
* {@code false} otherwise
*/
public boolean validateInsertionDelay(Integer insertionDelay) {
- return options.getIntegerOption("InsertionDelay").validate(insertionDelay);
+ return this.insertionDelay.validate(insertionDelay);
}
/**
* @return This preferences
*/
public Preferences setInsertionDelay(Integer insertionDelay) {
- options.getIntegerOption("InsertionDelay").set(insertionDelay);
+ this.insertionDelay.set(insertionDelay);
+ eventBus.post(new InsertionDelayChangedEvent(getInsertionDelay()));
return this;
}
* @return The number of posts to show per page
*/
public int getPostsPerPage() {
- return options.getIntegerOption("PostsPerPage").get();
+ return postsPerPage.get();
}
/**
* {@code false} otherwise
*/
public boolean validatePostsPerPage(Integer postsPerPage) {
- return options.getIntegerOption("PostsPerPage").validate(postsPerPage);
+ return this.postsPerPage.validate(postsPerPage);
}
/**
* @return This preferences object
*/
public Preferences setPostsPerPage(Integer postsPerPage) {
- options.getIntegerOption("PostsPerPage").set(postsPerPage);
+ this.postsPerPage.set(postsPerPage);
return this;
}
* @return The number of images to show per page
*/
public int getImagesPerPage() {
- return options.getIntegerOption("ImagesPerPage").get();
+ return imagesPerPage.get();
}
/**
* {@code false} otherwise
*/
public boolean validateImagesPerPage(Integer imagesPerPage) {
- return options.getIntegerOption("ImagesPerPage").validate(imagesPerPage);
+ return this.imagesPerPage.validate(imagesPerPage);
}
/**
* @return This preferences object
*/
public Preferences setImagesPerPage(Integer imagesPerPage) {
- options.getIntegerOption("ImagesPerPage").set(imagesPerPage);
+ this.imagesPerPage.set(imagesPerPage);
return this;
}
* @return The numbers of characters per post
*/
public int getCharactersPerPost() {
- return options.getIntegerOption("CharactersPerPost").get();
+ return charactersPerPost.get();
}
/**
* {@code false} otherwise
*/
public boolean validateCharactersPerPost(Integer charactersPerPost) {
- return options.getIntegerOption("CharactersPerPost").validate(charactersPerPost);
+ return this.charactersPerPost.validate(charactersPerPost);
}
/**
* @return This preferences objects
*/
public Preferences setCharactersPerPost(Integer charactersPerPost) {
- options.getIntegerOption("CharactersPerPost").set(charactersPerPost);
+ this.charactersPerPost.set(charactersPerPost);
return this;
}
* @return The number of characters of the snippet
*/
public int getPostCutOffLength() {
- return options.getIntegerOption("PostCutOffLength").get();
+ return postCutOffLength.get();
}
/**
* valid, {@code false} otherwise
*/
public boolean validatePostCutOffLength(Integer postCutOffLength) {
- return options.getIntegerOption("PostCutOffLength").validate(postCutOffLength);
+ return this.postCutOffLength.validate(postCutOffLength);
}
/**
* @return This preferences
*/
public Preferences setPostCutOffLength(Integer postCutOffLength) {
- options.getIntegerOption("PostCutOffLength").set(postCutOffLength);
+ this.postCutOffLength.set(postCutOffLength);
return this;
}
* otherwise
*/
public boolean isRequireFullAccess() {
- return options.getBooleanOption("RequireFullAccess").get();
+ return requireFullAccess.get();
}
/**
* otherwise
*/
public void setRequireFullAccess(Boolean requireFullAccess) {
- options.getBooleanOption("RequireFullAccess").set(requireFullAccess);
+ this.requireFullAccess.set(requireFullAccess);
}
/**
* @return The positive trust
*/
public int getPositiveTrust() {
- return options.getIntegerOption("PositiveTrust").get();
+ return positiveTrust.get();
}
/**
* otherwise
*/
public boolean validatePositiveTrust(Integer positiveTrust) {
- return options.getIntegerOption("PositiveTrust").validate(positiveTrust);
+ return this.positiveTrust.validate(positiveTrust);
}
/**
* @return This preferences
*/
public Preferences setPositiveTrust(Integer positiveTrust) {
- options.getIntegerOption("PositiveTrust").set(positiveTrust);
+ this.positiveTrust.set(positiveTrust);
return this;
}
* @return The negative trust
*/
public int getNegativeTrust() {
- return options.getIntegerOption("NegativeTrust").get();
+ return negativeTrust.get();
}
/**
* otherwise
*/
public boolean validateNegativeTrust(Integer negativeTrust) {
- return options.getIntegerOption("NegativeTrust").validate(negativeTrust);
+ return this.negativeTrust.validate(negativeTrust);
}
/**
* @return The preferences
*/
public Preferences setNegativeTrust(Integer negativeTrust) {
- options.getIntegerOption("NegativeTrust").set(negativeTrust);
+ this.negativeTrust.set(negativeTrust);
return this;
}
* @return The trust comment
*/
public String getTrustComment() {
- return options.getStringOption("TrustComment").get();
+ return trustComment.get();
}
/**
* @return This preferences
*/
public Preferences setTrustComment(String trustComment) {
- options.getStringOption("TrustComment").set(trustComment);
+ this.trustComment.set(trustComment);
return this;
}
* {@code false} otherwise
*/
public boolean isFcpInterfaceActive() {
- return options.getBooleanOption("ActivateFcpInterface").get();
+ return activateFcpInterface.get();
}
/**
* 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;
}
* is required
*/
public FullAccessRequired getFcpFullAccessRequired() {
- return FullAccessRequired.values()[options.getIntegerOption("FcpFullAccessRequired").get()];
+ return fcpFullAccessRequired.get();
}
/**
* 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();
+ }
+
}
--- /dev/null
+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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+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]);
+ }
+
+}
--- /dev/null
+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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class SoneChangeDetector {
+
+ private final Sone oldSone;
+ private Optional<PostProcessor> newPostProcessor = absent();
+ private Optional<PostProcessor> removedPostProcessor = absent();
+ private Optional<PostReplyProcessor> newPostReplyProcessor = absent();
+ private Optional<PostReplyProcessor> 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<PostReply> postReplies,
+ Optional<PostReplyProcessor> postReplyProcessor) {
+ for (PostReply postReply : postReplies) {
+ notifyPostReplyProcessor(postReplyProcessor, postReply);
+ }
+ }
+
+ private void notifyPostReplyProcessor(
+ Optional<PostReplyProcessor> postReplyProcessor,
+ PostReply postReply) {
+ if (postReplyProcessor.isPresent()) {
+ postReplyProcessor.get()
+ .processPostReply(postReply);
+ }
+ }
+
+ private void processPosts(FluentIterable<Post> posts,
+ Optional<PostProcessor> newPostProcessor) {
+ for (Post post : posts) {
+ notifyPostProcessor(newPostProcessor, post);
+ }
+ }
+
+ private void notifyPostProcessor(Optional<PostProcessor> postProcessor,
+ Post newPost) {
+ if (postProcessor.isPresent()) {
+ postProcessor.get().processPost(newPost);
+ }
+ }
+
+ private <T> Predicate<T> notContainedIn(final Collection<T> posts) {
+ return new Predicate<T>() {
+ @Override
+ public boolean apply(T element) {
+ return !posts.contains(element);
+ }
+ };
+ }
+
+ public interface PostProcessor {
+
+ void processPost(Post post);
+
+ }
+
+ public interface PostReplyProcessor {
+
+ void processPostReply(PostReply postReply);
+
+ }
+
+}
-/*
- * 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 <http://www.gnu.org/licenses/>.
- */
-
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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
*/
-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<Sone> sones = new HashSet<Sone>();
-
- /**
- * 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<Post> posts = new HashSet<Post>();
- 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<PostReply> replies = new HashSet<PostReply>();
- 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<String> likedPostIds = new HashSet<String>();
- 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<String> likedReplyIds = new HashSet<String>();
- 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<Album> topLevelAlbums = new ArrayList<Album>();
- 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);
}
--- /dev/null
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+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<Sone> sones = new HashSet<Sone>();
+
+ /**
+ * 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);
+ }
+ }
+
+}
/**
* 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
/**
* 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
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;
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;
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.
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();
/** 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.
* 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> sone = core.getSone(soneId);
+ if (!sone.isPresent()) {
+ return false;
+ }
+ return core.isLocked(sone.get());
+ }
+
+ @Override
+ public String getFingerprint() {
+ final Optional<Sone> 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;
}
/**
* @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);
}
/**
* @return The fingerprint of the last insert
*/
public String getLastInsertFingerprint() {
- return lastInsertFingerprint;
+ return soneModificationDetector.getOriginalFingerprint();
}
/**
* The fingerprint of the last insert
*/
public void setLastInsertFingerprint(String lastInsertFingerprint) {
- this.lastInsertFingerprint = lastInsertFingerprint;
+ soneModificationDetector.setFingerprint(lastInsertFingerprint);
}
/**
* otherwise
*/
public boolean isModified() {
- return modified;
+ return soneModificationDetector.isModified();
}
//
*/
@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<Sone> 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. */
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);
}
*/
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;
}
}
}
}
}
+ @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
*
* @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
*/
- private class InsertInformation {
+ @VisibleForTesting
+ class InsertInformation implements Closeable {
/** All properties of the Sone, copied for thread safety. */
private final Map<String, Object> soneProperties = new HashMap<String, Object>();
+ private final String fingerprint;
+ private final ManifestCreator manifestCreator;
/**
* Creates a new insert information container.
* The sone to insert
*/
public InsertInformation(Sone sone) {
+ this.fingerprint = sone.getFingerprint();
+ Map<String, Object> soneProperties = new HashMap<String, Object>();
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<String>(sone.getLikedPostIds()));
soneProperties.put("likedReplyIds", new HashSet<String>(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;
}
//
HashMap<String, Object> manifestEntries = new HashMap<String, Object>();
/* 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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+ @VisibleForTesting
+ static class ManifestCreator implements Closeable {
+
+ private final Core core;
+ private final Map<String, Object> soneProperties;
+ private final Set<Bucket> buckets = new HashSet<Bucket>();
+
+ ManifestCreator(Core core, Map<String, Object> 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();
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();
}
}
--- /dev/null
+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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+class SoneModificationDetector {
+
+ private final Ticker ticker;
+ private final LockableFingerprintProvider lockableFingerprintProvider;
+ private final AtomicInteger insertionDelay;
+ private Optional<Long> 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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+ static interface LockableFingerprintProvider {
+
+ boolean isLocked();
+ String getFingerprint();
+
+ }
+
+}
--- /dev/null
+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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+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<Post> posts = new HashSet<Post>();
+ 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<PostReply> replies = new HashSet<PostReply>();
+ 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<String> likedPostIds = new HashSet<String>();
+ 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<String> likedReplyIds = new HashSet<String>();
+ 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<String, Image> allImages = new HashMap<String, Image>();
+ List<Album> topLevelAlbums = new ArrayList<Album>();
+ 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;
+
+ }
+
+}
*
* @return {@code true} if the Sone rescuer is currently fetching a Sone
*/
+ @SuppressWarnings("unused") // used in rescue.html
public boolean isFetching() {
return fetching;
}
*
* @return The edition that is currently being downloaded
*/
+ @SuppressWarnings("unused") // used in rescue.html
public long getCurrentEdition() {
return currentEdition;
}
*
* @return The next edition the Sone rescuer can download
*/
+ @SuppressWarnings("unused") // used in rescue.html
public long getNextEdition() {
return currentEdition - 1;
}
* @return {@code true} if the last fetch was successful, {@code false}
* otherwise
*/
+ @SuppressWarnings("unused") // used in rescue.html
public boolean isLastFetchSuccessful() {
return lastFetchSuccessful;
}
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;
/**
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.
package net.pterodactylus.sone.core;
+import static java.util.logging.Logger.getLogger;
+
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
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;
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;
-/*
- * 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 <http://www.gnu.org/licenses/>.
- */
-
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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
*/
-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<WebOfTrustUpdateJob> updateJobs = new LinkedBlockingQueue<WebOfTrustUpdateJob>();
-
- /**
- * 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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
- 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.
- * <p/>
- * 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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
- 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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
- 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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
- 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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
- 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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
- 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);
}
--- /dev/null
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+@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<WebOfTrustUpdateJob> updateJobs = new LinkedBlockingQueue<WebOfTrustUpdateJob>();
+
+ /**
+ * 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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+ @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.
+ * <p/>
+ * 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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+ @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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+ @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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+ @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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+ @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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+ @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);
+ }
+
+ }
+
+}
--- /dev/null
+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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class InsertionDelayChangedEvent {
+
+ private final int insertionDelay;
+
+ public InsertionDelayChangedEvent(int insertionDelay) {
+ this.insertionDelay = insertionDelay;
+ }
+
+ public int getInsertionDelay() {
+ return insertionDelay;
+ }
+
+}
*/
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;
+ }
+
}
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
Album update() throws IllegalStateException;
+ class AlbumTitleMustNotBeEmpty extends IllegalStateException { }
+
}
}
+++ /dev/null
-/*
- * 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 <http://www.gnu.org/licenses/>.
- */
-
-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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-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<Album> albums = new ArrayList<Album>();
-
- /** The image IDs in order. */
- private final List<String> imageIds = new ArrayList<String>();
-
- /** The images in this album. */
- private final Map<String, Image> images = new HashMap<String, Image>();
-
- /** 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<Album> getAlbums() {
- return new ArrayList<Album>(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<Image> getImages() {
- return new ArrayList<Image>(Collections2.filter(Collections2.transform(imageIds, new Function<String, Image>() {
-
- @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<String> title = absent();
-
- private Optional<String> description = absent();
-
- private Optional<String> 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);
- }
-
-}
package net.pterodactylus.sone.data;
+import static com.google.common.base.Objects.equal;
+
/**
* Container for the client information of a {@link Sone}.
*
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());
+ }
+
}
Image update() throws IllegalStateException;
+ class ImageTitleMustNotBeEmpty extends IllegalStateException { }
+
}
}
+++ /dev/null
-/*
- * 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 <http://www.gnu.org/licenses/>.
- */
-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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-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> sone = absent();
-
- private Optional<Long> creationTime = absent();
-
- private Optional<String> key = absent();
-
- private Optional<String> title = absent();
-
- private Optional<String> description = absent();
-
- private Optional<Integer> width = absent();
-
- private Optional<Integer> 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);
- }
-
-}
package net.pterodactylus.sone.data;
+import static com.google.common.base.Optional.absent;
+
import java.util.Comparator;
import com.google.common.base.Optional;
@Override
public boolean apply(Post post) {
- return (post == null) ? false : post.getTime() <= System.currentTimeMillis();
+ return (post != null) && (post.getTime() <= System.currentTimeMillis());
}
};
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
*/
public Post setKnown(boolean known);
+ /**
+ * Shell for a post that has not yet been loaded.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’
+ * Roden</a>
+ */
+ 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<String> getRecipientId() {
+ return absent();
+ }
+
+ @Override
+ public Optional<Sone> 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;
+ }
+
+ }
+
}
@Override
public boolean apply(PostReply postReply) {
- return (postReply == null) ? false : postReply.getPost().isPresent();
+ return (postReply != null) && postReply.getPost().isPresent();
}
};
*/
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;
}
}
+ /**
+ * Exception that signals the addition of a field with an empty name.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+ public static class EmptyFieldName extends IllegalArgumentException { }
+
+ /**
+ * Exception that signals the addition of a field that already exists.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+ public static class DuplicateField extends IllegalArgumentException { }
+
}
*/
@Override
public boolean apply(Reply<?> reply) {
- return (reply == null) ? false : reply.getTime() <= System.currentTimeMillis();
+ return (reply != null) && (reply.getTime() <= System.currentTimeMillis());
}
};
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;
@Override
public boolean apply(Sone sone) {
- return (sone == null) ? false : sone.getTime() != 0;
+ return (sone != null) && (sone.getTime() != 0);
}
};
@Override
public boolean apply(Sone sone) {
- return (sone == null) ? false : sone.getIdentity() instanceof OwnIdentity;
+ return (sone != null) && (sone.getIdentity() instanceof OwnIdentity);
}
};
@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<Sone, String> toSoneXmlUri =
+ new Function<Sone, String>() {
+ @Nonnull
+ @Override
+ public String apply(@Nullable Sone input) {
+ return input.getRequestUri()
+ .setMetaString(new String[] { "sone.xml" })
+ .toString();
+ }
+ };
+
+ public static final Function<Sone, List<Album>> toAllAlbums = new Function<Sone, List<Album>>() {
+ @Override
+ public List<Album> apply(@Nullable Sone sone) {
+ return (sone == null) ? Collections.<Album>emptyList() : FLATTENER.apply(
+ sone.getRootAlbum());
+ }
+ };
+
+ public static final Function<Sone, List<Image>> toAllImages = new Function<Sone, List<Image>>() {
+ @Override
+ public List<Image> apply(@Nullable Sone sone) {
+ return (sone == null) ? Collections.<Image>emptyList() :
+ from(FLATTENER.apply(sone.getRootAlbum()))
+ .transformAndConcat(IMAGES).toList();
}
};
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
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
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
*
* @return The friend Sones of this Sone
*/
- List<String> getFriends();
+ Collection<String> getFriends();
/**
* Returns whether this Sone has the given Sone as a friend Sone.
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
*
* @return The options of this Sone
*/
- Options getOptions();
+ SoneOptions getOptions();
/**
* Sets the options of this Sone.
* The options of this Sone
*/
/* TODO - remove this method again, maybe add an option provider */
- void setOptions(Options options);
+ void setOptions(SoneOptions options);
}
+++ /dev/null
-/*
- * 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 <http://www.gnu.org/licenses/>.
- */
-
-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.
- * <p/>
- * Operations that modify the Sone need to synchronize on the Sone in question.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-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<String> friendSones = new CopyOnWriteArraySet<String>();
-
- /** All posts. */
- private final Set<Post> posts = new CopyOnWriteArraySet<Post>();
-
- /** All replies. */
- private final Set<PostReply> replies = new CopyOnWriteArraySet<PostReply>();
-
- /** The IDs of all liked posts. */
- private final Set<String> likedPostIds = new CopyOnWriteArraySet<String>();
-
- /** The IDs of all liked replies. */
- private final Set<String> likedReplyIds = new CopyOnWriteArraySet<String>();
-
- /** 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<String> getFriends() {
- List<String> friends = new ArrayList<String>(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<Post> getPosts() {
- List<Post> sortedPosts;
- synchronized (this) {
- sortedPosts = new ArrayList<Post>(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<Post> 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<PostReply> 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<PostReply> 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<String> 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<String> 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<String> 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<String> 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<PostReply> replies = new ArrayList<PostReply>(getReplies());
- Collections.sort(replies, Reply.TIME_COMPARATOR);
- hash.putString("Replies(");
- for (PostReply reply : replies) {
- hash.putString("Reply(").putString(reply.getId()).putString(")");
- }
- hash.putString(")");
-
- List<String> likedPostIds = new ArrayList<String>(getLikedPostIds());
- Collections.sort(likedPostIds);
- hash.putString("LikedPosts(");
- for (String likedPostId : likedPostIds) {
- hash.putString("Post(").putString(likedPostId).putString(")");
- }
- hash.putString(")");
-
- List<String> likedReplyIds = new ArrayList<String>(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<Sone>
- //
-
- /** {@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() + ")]";
- }
-
-}
--- /dev/null
+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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+ 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;
+ }
+
+ }
+
+}
import static com.google.common.base.Preconditions.checkState;
+import net.pterodactylus.sone.data.Sone;
import net.pterodactylus.sone.database.AlbumBuilder;
/**
/** The ID of the album to create. */
protected String id;
+ protected Sone sone;
@Override
public AlbumBuilder randomId() {
return this;
}
+ public AlbumBuilder by(Sone sone) {
+ this.sone = sone;
+ return this;
+ }
+
//
// PROTECTED METHODS
//
*/
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");
}
}
--- /dev/null
+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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+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");
+ }
+
+}
package net.pterodactylus.sone.data.impl;
import net.pterodactylus.sone.data.Album;
-import net.pterodactylus.sone.data.AlbumImpl;
import net.pterodactylus.sone.database.AlbumBuilder;
/**
@Override
public Album build() throws IllegalStateException {
validate();
- return randomId ? new AlbumImpl() : new AlbumImpl(id);
+ return randomId ? new AlbumImpl(sone) : new AlbumImpl(sone, id);
}
}
--- /dev/null
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+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<Album> albums = new ArrayList<Album>();
+
+ /** The image IDs in order. */
+ private final List<String> imageIds = new ArrayList<String>();
+
+ /** The images in this album. */
+ private final Map<String, Image> images = new HashMap<String, Image>();
+
+ /** 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<Album> getAlbums() {
+ return new ArrayList<Album>(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<Image> getImages() {
+ return new ArrayList<Image>(Collections2.filter(Collections2.transform(imageIds, new Function<String, Image>() {
+
+ @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<String> title = absent();
+
+ private Optional<String> description = absent();
+
+ private Optional<String> 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);
+ }
+
+}
--- /dev/null
+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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+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<String> getFriends() {
+ return emptyList();
+ }
+
+ @Override
+ public boolean hasFriend(String friendSoneId) {
+ return false;
+ }
+
+ @Override
+ public List<Post> getPosts() {
+ return emptyList();
+ }
+
+ @Override
+ public Sone setPosts(Collection<Post> posts) {
+ return this;
+ }
+
+ @Override
+ public void addPost(Post post) {
+ }
+
+ @Override
+ public void removePost(Post post) {
+ }
+
+ @Override
+ public Set<PostReply> getReplies() {
+ return emptySet();
+ }
+
+ @Override
+ public Sone setReplies(Collection<PostReply> replies) {
+ return this;
+ }
+
+ @Override
+ public void addReply(PostReply reply) {
+ }
+
+ @Override
+ public void removeReply(PostReply reply) {
+ }
+
+ @Override
+ public Set<String> getLikedPostIds() {
+ return emptySet();
+ }
+
+ @Override
+ public Sone setLikePostIds(Set<String> 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<String> getLikedReplyIds() {
+ return emptySet();
+ }
+
+ @Override
+ public Sone setLikeReplyIds(Set<String> 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;
+ }
+
+}
package net.pterodactylus.sone.data.impl;
import net.pterodactylus.sone.data.Image;
-import net.pterodactylus.sone.data.ImageImpl;
import net.pterodactylus.sone.database.ImageBuilder;
/**
--- /dev/null
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+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> sone = absent();
+
+ private Optional<Long> creationTime = absent();
+
+ private Optional<String> key = absent();
+
+ private Optional<String> title = absent();
+
+ private Optional<String> description = absent();
+
+ private Optional<Integer> width = absent();
+
+ private Optional<Integer> 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);
+ }
+
+}
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;
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;
*/
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;
*/
@Override
public String getId() {
- return id.toString();
+ return id;
+ }
+
+ @Override
+ public boolean isLoaded() {
+ return true;
}
/**
--- /dev/null
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+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.
+ * <p/>
+ * Operations that modify the Sone need to synchronize on the Sone in question.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+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<Post> posts = new CopyOnWriteArraySet<Post>();
+
+ /** All replies. */
+ private final Set<PostReply> replies = new CopyOnWriteArraySet<PostReply>();
+
+ /** The IDs of all liked posts. */
+ private final Set<String> likedPostIds = new CopyOnWriteArraySet<String>();
+
+ /** The IDs of all liked replies. */
+ private final Set<String> likedReplyIds = new CopyOnWriteArraySet<String>();
+
+ /** 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<String> 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<Post> getPosts() {
+ List<Post> sortedPosts;
+ synchronized (this) {
+ sortedPosts = new ArrayList<Post>(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<Post> 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<PostReply> 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<PostReply> 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<String> 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<String> 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<String> 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<String> 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<PostReply> replies = new ArrayList<PostReply>(getReplies());
+ Collections.sort(replies, Reply.TIME_COMPARATOR);
+ hash.putString("Replies(");
+ for (PostReply reply : replies) {
+ hash.putString("Reply(").putString(reply.getId()).putString(")");
+ }
+ hash.putString(")");
+
+ List<String> likedPostIds = new ArrayList<String>(getLikedPostIds());
+ Collections.sort(likedPostIds);
+ hash.putString("LikedPosts(");
+ for (String likedPostId : likedPostIds) {
+ hash.putString("Post(").putString(likedPostId).putString(")");
+ }
+ hash.putString(")");
+
+ List<String> likedReplyIds = new ArrayList<String>(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<Sone>
+ //
+
+ /** {@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() + ")]";
+ }
+
+}
package net.pterodactylus.sone.database;
import net.pterodactylus.sone.data.Album;
+import net.pterodactylus.sone.data.Sone;
/**
* Builder for {@link Album} objects.
*/
AlbumBuilder withId(String id);
+ AlbumBuilder by(Sone sone);
+
/**
* Creates the album.
*
--- /dev/null
+package net.pterodactylus.sone.database;
+
+import java.util.Set;
+
+import net.pterodactylus.sone.data.Post;
+
+/**
+ * Database interface for bookmark-related functionality.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public interface BookmarkDatabase {
+
+ void bookmarkPost(Post post);
+ void unbookmarkPost(Post post);
+ boolean isPostBookmarked(Post post);
+ Set<Post> getBookmarkedPosts();
+
+}
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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
*/
-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.
--- /dev/null
+package net.pterodactylus.sone.database;
+
+/**
+ * Combines a {@link FriendProvider} and a {@link FriendStore} into a friend database.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public interface FriendDatabase extends FriendProvider, FriendStore {
+
+}
--- /dev/null
+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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public interface FriendProvider {
+
+ Collection<String> getFriends(Sone localSone);
+ boolean isFriend(Sone localSone, String friendSoneId);
+
+}
--- /dev/null
+package net.pterodactylus.sone.database;
+
+import net.pterodactylus.sone.data.Sone;
+
+/**
+ * Stores information about the {@link Sone#getFriends() friends} of a {@link Sone}.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public interface FriendStore {
+
+ void addFriend(Sone localSone, String friendSoneId);
+ void removeFriend(Sone localSone, String friendSoneId);
+
+}
package net.pterodactylus.sone.database;
+import net.pterodactylus.sone.database.memory.MemoryDatabase;
+
+import com.google.inject.ImplementedBy;
+
/**
* Factory for {@link PostBuilder}s.
*
* @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
*/
+@ImplementedBy(MemoryDatabase.class)
public interface PostBuilderFactory {
/**
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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
*/
+@ImplementedBy(MemoryDatabase.class)
public interface PostProvider {
/**
package net.pterodactylus.sone.database;
+import net.pterodactylus.sone.database.memory.MemoryDatabase;
+
+import com.google.inject.ImplementedBy;
+
/**
* Factory for {@link PostReplyBuilder}s.
*
* @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
*/
+@ImplementedBy(MemoryDatabase.class)
public interface PostReplyBuilderFactory {
/**
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<PostReply> postReplies) throws IllegalArgumentException;
-
- /**
* Removes the given post reply from this store.
*
* @param postReply
*/
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);
-
}
*/
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<Post> 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);
-
}
--- /dev/null
+package net.pterodactylus.sone.database;
+
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.freenet.wot.Identity;
+
+/**
+ * Builder for {@link Sone} objects.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public interface SoneBuilder {
+
+ SoneBuilder from(Identity identity);
+ SoneBuilder local();
+
+ Sone build() throws IllegalStateException;
+
+}
--- /dev/null
+package net.pterodactylus.sone.database;
+
+/**
+ * Factory for {@link SoneBuilder}s.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public interface SoneBuilderFactory {
+
+ SoneBuilder newSoneBuilder();
+
+}
--- /dev/null
+package net.pterodactylus.sone.database;
+
+/**
+ * Combines a {@link SoneProvider} and a {@link SoneStore} into a Sone
+ * database.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public interface SoneDatabase extends SoneProvider, SoneBuilderFactory, SoneStore {
+
+}
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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
*/
+@ImplementedBy(Core.class)
public interface SoneProvider {
+ Function<String, Optional<Sone>> soneLoader();
+
/**
* Returns the Sone with the given ID, or {@link Optional#absent()} if it
* does not exist.
--- /dev/null
+package net.pterodactylus.sone.database;
+
+import net.pterodactylus.sone.data.Sone;
+
+/**
+ * Interface for a store for {@link Sone}s.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public interface SoneStore {
+
+ void storeSone(Sone sone);
+ void removeSone(Sone sone);
+
+}
--- /dev/null
+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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+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<String> loadFriends(String localSoneId) {
+ return loadIds("Sone/" + localSoneId + "/Friends");
+ }
+
+ public void saveFriends(String soneId, Collection<String> friends) {
+ saveIds("Sone/" + soneId + "/Friends", friends);
+ }
+
+ public synchronized Set<String> loadKnownPosts() {
+ return loadIds("KnownPosts");
+ }
+
+ public synchronized Set<String> loadKnownPostReplies() {
+ return loadIds("KnownReplies");
+ }
+
+ public synchronized Set<String> loadBookmarkedPosts() {
+ return loadIds("Bookmarks/Post");
+ }
+
+ private Set<String> loadIds(String prefix) {
+ Set<String> ids = new HashSet<String>();
+ 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<String> bookmarkedPosts) {
+ saveIds("Bookmarks/Post", bookmarkedPosts);
+ }
+
+ private void saveIds(String prefix, Collection<String> 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);
+ }
+ }
+
+}
--- /dev/null
+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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class MemoryBookmarkDatabase implements BookmarkDatabase {
+
+ private final ReadWriteLock lock = new ReentrantReadWriteLock();
+ private final MemoryDatabase memoryDatabase;
+ private final ConfigurationLoader configurationLoader;
+ private final Set<String> bookmarkedPosts = new HashSet<String>();
+
+ public MemoryBookmarkDatabase(MemoryDatabase memoryDatabase,
+ ConfigurationLoader configurationLoader) {
+ this.memoryDatabase = memoryDatabase;
+ this.configurationLoader = configurationLoader;
+ }
+
+ public void start() {
+ loadBookmarkedPosts();
+ }
+
+ private void loadBookmarkedPosts() {
+ Set<String> 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<Post> getBookmarkedPosts() {
+ lock.readLock().lock();
+ try {
+ return from(bookmarkedPosts).transform(
+ new Function<String, Post>() {
+ @Override
+ public Post apply(String postId) {
+ return memoryDatabase.getPost(postId)
+ .or(new EmptyPost(postId));
+ }
+ }).toSet();
+ } finally {
+ lock.readLock().unlock();
+ }
+ }
+
+}
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;
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;
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;
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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
*/
+@Singleton
public class MemoryDatabase extends AbstractService implements Database {
/** The lock. */
/** The configuration. */
private final Configuration configuration;
+ private final ConfigurationLoader configurationLoader;
+
+ private final Map<String, Sone> allSones = new HashMap<String, Sone>();
/** All posts by their ID. */
private final Map<String, Post> allPosts = new HashMap<String, Post>();
/** All posts by their Sones. */
- private final Map<String, Collection<Post>> sonePosts = new HashMap<String, Collection<Post>>();
-
- /** All posts by their recipient. */
- private final Map<String, Collection<Post>> recipientPosts = new HashMap<String, Collection<Post>>();
+ private final Multimap<String, Post> sonePosts = HashMultimap.create();
/** Whether posts are known. */
private final Set<String> knownPosts = new HashSet<String>();
public int compare(String leftString, String rightString) {
return leftString.compareTo(rightString);
}
- }, PostReply.TIME_COMPARATOR);
-
- /** Replies by post. */
- private final Map<String, SortedSet<PostReply>> postReplies = new HashMap<String, SortedSet<PostReply>>();
+ }, TIME_COMPARATOR);
/** Whether post replies are known. */
private final Set<String> knownPostReplies = new HashSet<String>();
private final Map<String, Album> allAlbums = new HashMap<String, Album>();
+ private final Multimap<String, Album> soneAlbums = HashMultimap.create();
private final Map<String, Image> allImages = new HashMap<String, Image>();
+ private final Multimap<String, Image> soneImages = HashMultimap.create();
+
+ private final MemoryBookmarkDatabase memoryBookmarkDatabase;
+ private final MemoryFriendDatabase memoryFriendDatabase;
/**
* Creates a new memory 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);
}
//
/** {@inheritDocs} */
@Override
protected void doStart() {
+ memoryBookmarkDatabase.start();
loadKnownPosts();
loadKnownPostReplies();
notifyStarted();
@Override
protected void doStop() {
try {
+ memoryBookmarkDatabase.stop();
save();
notifyStopped();
} catch (DatabaseException de1) {
}
}
+ @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<Post> removedPosts = sonePosts.removeAll(sone.getId());
+ for (Post removedPost : removedPosts) {
+ allPosts.remove(removedPost.getId());
+ }
+ Collection<PostReply> removedPostReplies =
+ sonePostReplies.removeAll(sone.getId());
+ for (PostReply removedPostReply : removedPostReplies) {
+ allPostReplies.remove(removedPostReply.getId());
+ }
+ Collection<Album> removedAlbums =
+ soneAlbums.removeAll(sone.getId());
+ for (Album removedAlbum : removedAlbums) {
+ allAlbums.remove(removedAlbum.getId());
+ }
+ Collection<Image> removedImages =
+ soneImages.removeAll(sone.getId());
+ for (Image removedImage : removedImages) {
+ allImages.remove(removedImage.getId());
+ }
+ } finally {
+ lock.writeLock().unlock();
+ }
+ }
+
+ @Override
+ public Function<String, Optional<Sone>> soneLoader() {
+ return new Function<String, Optional<Sone>>() {
+ @Override
+ public Optional<Sone> apply(String soneId) {
+ return getSone(soneId);
+ }
+ };
+ }
+
+ @Override
+ public Optional<Sone> getSone(String soneId) {
+ lock.readLock().lock();
+ try {
+ return fromNullable(allSones.get(soneId));
+ } finally {
+ lock.readLock().unlock();
+ }
+ }
+
+ @Override
+ public Collection<Sone> getSones() {
+ lock.readLock().lock();
+ try {
+ return new HashSet<Sone>(allSones.values());
+ } finally {
+ lock.readLock().unlock();
+ }
+ }
+
+ @Override
+ public Collection<Sone> getLocalSones() {
+ lock.readLock().lock();
+ try {
+ return from(allSones.values()).filter(LOCAL_SONE_FILTER).toSet();
+ } finally {
+ lock.readLock().unlock();
+ }
+ }
+
+ @Override
+ public Collection<Sone> getRemoteSones() {
+ lock.readLock().lock();
+ try {
+ return from(allSones.values())
+ .filter(not(LOCAL_SONE_FILTER)) .toSet();
+ } finally {
+ lock.readLock().unlock();
+ }
+ }
+
+ @Override
+ public Collection<String> 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
//
/** {@inheritDocs} */
@Override
- public Collection<Post> getDirectedPosts(String recipientId) {
+ public Collection<Post> getDirectedPosts(final String recipientId) {
lock.readLock().lock();
try {
- Collection<Post> posts = recipientPosts.get(recipientId);
- return (posts == null) ? Collections.<Post>emptySet() : new HashSet<Post>(posts);
+ return from(sonePosts.values()).filter(new Predicate<Post>() {
+ @Override
+ public boolean apply(Post post) {
+ return post.getRecipientId().asSet().contains(recipientId);
+ }
+ }).toSet();
} finally {
lock.readLock().unlock();
}
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();
}
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<Post> 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
//
/** {@inheritDocs} */
@Override
- public List<PostReply> getReplies(String postId) {
+ public List<PostReply> getReplies(final String postId) {
lock.readLock().lock();
try {
- if (!postReplies.containsKey(postId)) {
- return Collections.emptyList();
- }
- return new ArrayList<PostReply>(postReplies.get(postId));
+ return from(allPostReplies.values())
+ .filter(new Predicate<PostReply>() {
+ @Override
+ public boolean apply(PostReply postReply) {
+ return postReply.getPostId().equals(postId);
+ }
+ }).toSortedList(TIME_COMPARATOR);
} finally {
lock.readLock().unlock();
}
lock.writeLock().lock();
try {
allPostReplies.put(postReply.getId(), postReply);
- if (postReplies.containsKey(postReply.getPostId())) {
- postReplies.get(postReply.getPostId()).add(postReply);
- } else {
- TreeSet<PostReply> replies = new TreeSet<PostReply>(Reply.TIME_COMPARATOR);
- replies.add(postReply);
- postReplies.put(postReply.getPostId(), replies);
- }
- } finally {
- lock.writeLock().unlock();
- }
- }
-
- /** {@inheritDocs} */
- @Override
- public void storePostReplies(Sone sone, Collection<PostReply> 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<PostReply> replies = new TreeSet<PostReply>(Reply.TIME_COMPARATOR);
- replies.add(postReply);
- this.postReplies.put(postReply.getPostId(), replies);
- }
- }
} finally {
lock.writeLock().unlock();
}
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();
}
lock.writeLock().lock();
try {
allAlbums.put(album.getId(), album);
+ soneAlbums.put(album.getSone().getId(), album);
} finally {
lock.writeLock().unlock();
}
lock.writeLock().lock();
try {
allAlbums.remove(album.getId());
+ soneAlbums.remove(album.getSone().getId(), album);
} finally {
lock.writeLock().unlock();
}
lock.writeLock().lock();
try {
allImages.put(image.getId(), image);
+ soneImages.put(image.getSone().getId(), image);
} finally {
lock.writeLock().unlock();
}
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<Post> getBookmarkedPosts() {
+ return memoryBookmarkDatabase.getBookmarkedPosts();
+ }
+
//
// PACKAGE-PRIVATE METHODS
//
* @return All posts
*/
private Collection<Post> getPostsFrom(String soneId) {
- Collection<Post> 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<Post>();
- 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<Post> getPostsTo(String recipientId) {
- Collection<Post> posts = null;
- lock.readLock().lock();
- try {
- posts = recipientPosts.get(recipientId);
- } finally {
- lock.readLock().unlock();
- }
- if (posts != null) {
- return posts;
- }
-
- posts = new HashSet<Post>();
- lock.writeLock().lock();
- try {
- recipientPosts.put(recipientId, posts);
- } finally {
- lock.writeLock().unlock();
- }
-
- return posts;
}
/** Loads the known posts. */
private void loadKnownPosts() {
+ Set<String> 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();
}
}
}
- /**
- * 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<PostReply> 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<String> 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();
}
--- /dev/null
+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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+class MemoryFriendDatabase {
+
+ private final ReadWriteLock lock = new ReentrantReadWriteLock();
+ private final ConfigurationLoader configurationLoader;
+ private final Multimap<String, String> soneFriends = HashMultimap.create();
+
+ MemoryFriendDatabase(ConfigurationLoader configurationLoader) {
+ this.configurationLoader = configurationLoader;
+ }
+
+ Collection<String> 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();
+ }
+ }
+
+}
return id.toString();
}
+ @Override
+ public boolean isLoaded() {
+ return true;
+ }
+
/**
* {@inheritDoc}
*/
--- /dev/null
+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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+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);
+ }
+
+}
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;
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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
*/
+@Singleton
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> fullAccessRequired = new AtomicReference<FullAccessRequired>(FullAccessRequired.ALWAYS);
/** All available FCP commands. */
private final Map<String, AbstractSoneCommand> commands = Collections.synchronizedMap(new HashMap<String, AbstractSoneCommand>());
// 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"));
}
//
* {@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) {
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) {
}
}
+ @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());
+ }
+
}
--- /dev/null
+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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class FcpInterfaceActivatedEvent {
+
+}
--- /dev/null
+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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class FcpInterfaceDeactivatedEvent {
+
+}
--- /dev/null
+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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class FullAccessRequiredChanged {
+
+ private final FullAccessRequired fullAccessRequired;
+
+ public FullAccessRequiredChanged(FullAccessRequired fullAccessRequired) {
+ this.fullAccessRequired = fullAccessRequired;
+ }
+
+ public FullAccessRequired getFullAccessRequired() {
+ return fullAccessRequired;
+ }
+
+}
--- /dev/null
+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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+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();
+ }
+
+}
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;
/** 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;
*
* @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();
- }
}
/**
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);
}
}
+++ /dev/null
-/*
- * 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 <http://www.gnu.org/licenses/>.
- */
-
-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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-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. */
- }
-
-}
import com.google.common.eventbus.EventBus;
import com.google.inject.Inject;
+import com.google.inject.Singleton;
import freenet.pluginmanager.FredPluginTalker;
import freenet.pluginmanager.PluginNotFoundException;
*
* @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
*/
+@Singleton
public class PluginConnector implements FredPluginTalker {
/** The event bus. */
--- /dev/null
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class Context {
+
+ public static final Function<Context, String> extractContext = new Function<Context, String>() {
+ @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;
+ }
+
+}
// 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<String> getContexts() {
return Collections.unmodifiableSet(contexts);
}
- /**
- * {@inheritDoc}
- */
@Override
public boolean hasContext(String context) {
return contexts.contains(context);
}
- /**
- * {@inheritDoc}
- */
@Override
public void setContexts(Collection<String> 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<String, String> getProperties() {
return Collections.unmodifiableMap(properties);
}
- /**
- * {@inheritDoc}
- */
@Override
public void setProperties(Map<String, String> 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)) {
return identity.getId().equals(getId());
}
- /**
- * {@inheritDoc}
- */
@Override
public String toString() {
return getClass().getSimpleName() + "[id=" + id + ",nickname=" + nickname + ",contexts=" + contexts + ",properties=" + properties + "]";
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.
*/
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
//
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
*/
public interface Identity {
+ public static final Function<Identity, Set<String>> TO_CONTEXTS = new Function<Identity, Set<String>>() {
+ @Override
+ public Set<String> apply(Identity identity) {
+ return (identity == null) ? Collections.<String>emptySet() : identity.getContexts();
+ }
+ };
+
+ public static final Function<Identity, Map<String, String>> TO_PROPERTIES = new Function<Identity, Map<String, String>>() {
+ @Override
+ public Map<String, String> apply(Identity input) {
+ return (input == null) ? Collections.<String, String>emptyMap() : input.getProperties();
+ }
+ };
+
/**
* Returns the ID of the identity.
*
* @param context
* The context to add
*/
- public void addContext(String context);
+ public Identity addContext(String context);
/**
* Sets all contexts of this identity.
* @param context
* The context to remove
*/
- public void removeContext(String context);
+ public Identity removeContext(String context);
/**
* Returns all properties of this 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.
* @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
* @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.
* The own identity that removed the trust assignment for this
* identity
*/
- public void removeTrust(OwnIdentity ownIdentity);
+ public Identity removeTrust(OwnIdentity ownIdentity);
}
--- /dev/null
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class IdentityChangeDetector {
+
+ private final Map<String, Identity> oldIdentities;
+ private Optional<IdentityProcessor> onNewIdentity = absent();
+ private Optional<IdentityProcessor> onRemovedIdentity = absent();
+ private Optional<IdentityProcessor> onChangedIdentity = absent();
+ private Optional<IdentityProcessor> onUnchangedIdentity = absent();
+
+ public IdentityChangeDetector(Collection<? extends Identity> 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<? extends Identity> 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<Identity> identities) {
+ notify(onRemovedIdentity, identities);
+ }
+
+ private void notifyForNewIdentities(FluentIterable<? extends Identity> newIdentities) {
+ notify(onNewIdentity, newIdentities);
+ }
+
+ private void notifyForChangedIdentities(FluentIterable<? extends Identity> identities) {
+ notify(onChangedIdentity, identities);
+ }
+
+ private void notifyForUnchangedIdentities(FluentIterable<? extends Identity> identities) {
+ notify(onUnchangedIdentity, identities);
+ }
+
+ private void notify(Optional<IdentityProcessor> identityProcessor, Iterable<? extends Identity> identities) {
+ if (!identityProcessor.isPresent()) {
+ return;
+ }
+ for (Identity identity : identities) {
+ identityProcessor.get().processIdentity(identity);
+ }
+ }
+
+ private static Predicate<Identity> hasChanged(final Map<String, Identity> oldIdentities) {
+ return new Predicate<Identity>() {
+ @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<Identity> containedIn(final Map<String, Identity> identities) {
+ return new Predicate<Identity>() {
+ @Override
+ public boolean apply(Identity identity) {
+ return identities.containsKey(identity.getId());
+ }
+ };
+ }
+
+ private static Predicate<String> notAContextOf(final Identity identity) {
+ return new Predicate<String>() {
+ @Override
+ public boolean apply(String context) {
+ return (identity == null) ? false : !identity.getContexts().contains(context);
+ }
+ };
+ }
+
+ private static Predicate<Identity> notContainedIn(final Collection<? extends Identity> newIdentities) {
+ return new Predicate<Identity>() {
+ @Override
+ public boolean apply(Identity identity) {
+ return (identity == null) ? false : !newIdentities.contains(identity);
+ }
+ };
+ }
+
+ private static Predicate<Entry<String, String>> notAPropertyOf(final Identity identity) {
+ return new Predicate<Entry<String, String>>() {
+ @Override
+ public boolean apply(Entry<String, String> property) {
+ return (property == null) ? false : !identity.getProperties().containsKey(property.getKey());
+ }
+ };
+ }
+
+ private static Predicate<Entry<String, String>> hasADifferentValueThanIn(final Identity newIdentity) {
+ return new Predicate<Entry<String, String>>() {
+ @Override
+ public boolean apply(Entry<String, String> property) {
+ return (property == null) ? false : !newIdentity.getProperty(property.getKey()).equals(property.getValue());
+ }
+ };
+ }
+
+ private static Map<String, Identity> convertToMap(Collection<? extends Identity> identities) {
+ ImmutableMap.Builder<String, Identity> mapBuilder = ImmutableMap.builder();
+ for (Identity identity : identities) {
+ mapBuilder.put(identity.getId(), identity);
+ }
+ return mapBuilder.build();
+ }
+
+ public interface IdentityProcessor {
+
+ void processIdentity(Identity identity);
+
+ }
+
+}
--- /dev/null
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ * @see IdentityChangeDetector
+ */
+public class IdentityChangeEventSender {
+
+ private final EventBus eventBus;
+ private final Map<OwnIdentity, Collection<Identity>> oldIdentities;
+
+ public IdentityChangeEventSender(EventBus eventBus, Map<OwnIdentity, Collection<Identity>> oldIdentities) {
+ this.eventBus = eventBus;
+ this.oldIdentities = oldIdentities;
+ }
+
+ public void detectChanges(Map<OwnIdentity, Collection<Identity>> 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<OwnIdentity, Collection<Identity>> 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<OwnIdentity, Collection<Identity>> 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<OwnIdentity, Collection<Identity>> newIdentities, Map<OwnIdentity, Collection<Identity>> oldIdentities) {
+ return new DefaultIdentityProcessor(oldIdentities, newIdentities);
+ }
+
+ private class DefaultIdentityProcessor implements IdentityProcessor {
+
+ private final Map<OwnIdentity, Collection<Identity>> oldIdentities;
+ private final Map<OwnIdentity, Collection<Identity>> newIdentities;
+
+ public DefaultIdentityProcessor(Map<OwnIdentity, Collection<Identity>> oldIdentities, Map<OwnIdentity, Collection<Identity>> 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));
+ }
+ };
+ }
+
+ }
+
+}
--- /dev/null
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class IdentityLoader {
+
+ private final WebOfTrustConnector webOfTrustConnector;
+ private final Optional<Context> context;
+
+ public IdentityLoader(WebOfTrustConnector webOfTrustConnector) {
+ this(webOfTrustConnector, Optional.<Context>absent());
+ }
+
+ @Inject
+ public IdentityLoader(WebOfTrustConnector webOfTrustConnector, Optional<Context> context) {
+ this.webOfTrustConnector = webOfTrustConnector;
+ this.context = context;
+ }
+
+ public Map<OwnIdentity, Collection<Identity>> loadIdentities() throws WebOfTrustException {
+ Collection<OwnIdentity> currentOwnIdentities = webOfTrustConnector.loadAllOwnIdentities();
+ return loadTrustedIdentitiesForOwnIdentities(currentOwnIdentities);
+ }
+
+ private Map<OwnIdentity, Collection<Identity>> loadTrustedIdentitiesForOwnIdentities(Collection<OwnIdentity> ownIdentities) throws PluginException {
+ Map<OwnIdentity, Collection<Identity>> currentIdentities = new HashMap<OwnIdentity, Collection<Identity>>();
+
+ for (OwnIdentity ownIdentity : ownIdentities) {
+ if (identityDoesNotHaveTheCorrectContext(ownIdentity)) {
+ currentIdentities.put(ownIdentity, Collections.<Identity>emptySet());
+ continue;
+ }
+
+ Set<Identity> 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());
+ }
+
+}
-/*
- * 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 <http://www.gnu.org/licenses/>.
- */
-
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.
- * <p>
- * 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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
*/
-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<String, OwnIdentity> currentOwnIdentities = new HashMap<String, OwnIdentity>();
-
- /** 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<OwnIdentity> 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<OwnIdentity> getAllOwnIdentities() {
- return new HashSet<OwnIdentity>(currentOwnIdentities.values());
- }
-
- //
- // SERVICE METHODS
- //
-
- /**
- * {@inheritDoc}
- */
- @Override
- protected void serviceRun() {
- Map<OwnIdentity, Map<String, Identity>> oldIdentities = Collections.emptyMap();
- while (!shouldStop()) {
- Map<OwnIdentity, Map<String, Identity>> currentIdentities = new HashMap<OwnIdentity, Map<String, Identity>>();
- Map<String, OwnIdentity> currentOwnIdentities = new HashMap<String, OwnIdentity>();
-
- Set<OwnIdentity> 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<String, Identity> identities = new HashMap<String, Identity>();
- 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<Identity> 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<String> oldContexts = oldIdentity.getContexts();
- Set<String> 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<String, String> oldProperties = oldIdentity.getProperties();
- Map<String, String> 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<String, String> 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<String, OwnIdentity> 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<OwnIdentity> getAllOwnIdentities();
}
--- /dev/null
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+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.
+ * <p>
+ * 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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+@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<OwnIdentity> 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<OwnIdentity> getAllOwnIdentities() {
+ synchronized (currentOwnIdentities) {
+ return new HashSet<OwnIdentity>(currentOwnIdentities);
+ }
+ }
+
+ //
+ // SERVICE METHODS
+ //
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void serviceRun() {
+ Map<OwnIdentity, Collection<Identity>> oldIdentities = new HashMap<OwnIdentity, Collection<Identity>>();
+
+ while (!shouldStop()) {
+ try {
+ Map<OwnIdentity, Collection<Identity>> 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);
+ }
+ }
+
+}
*/
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);
+
}
package net.pterodactylus.sone.freenet.wot;
+import static com.google.common.base.Objects.equal;
+
/**
* Container class for trust in the web of 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 + "]";
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;
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;
*
* @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
*/
+@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";
* @throws PluginException
* if an error occured talking to the Web of Trust plugin
*/
- public Set<Identity> 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<Identity> loadTrustedIdentities(OwnIdentity ownIdentity, Optional<String> 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<Identity> identities = new HashSet<Identity>();
int identityCounter = -1;
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);
}
* @return The created simple field set constructor
*/
public static SimpleFieldSetConstructor create(boolean shortLived) {
- SimpleFieldSetConstructor simpleFieldSetConstructor = new SimpleFieldSetConstructor(shortLived);
- return simpleFieldSetConstructor;
+ return new SimpleFieldSetConstructor(shortLived);
}
}
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());
+ }
+
}
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());
+ }
+
}
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;
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;
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;
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;
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;
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. */
@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
});
}
+ private TypeLiteral<Optional<Context>> getOptionalContextTypeLiteral() {
+ return new TypeLiteral<Optional<Context>>() {
+ };
+ }
+
};
Injector injector = Guice.createInjector(freenetModule, soneModule);
core = injector.getInstance(Core.class);
/* 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);
}
/**
webOfTrustConnector.stop();
} catch (Throwable t1) {
logger.log(Level.SEVERE, "Error while shutting down!", t1);
- } finally {
- /* shutdown logger. */
- Logging.shutdown();
}
}
List<Notification> filteredNotifications = new ArrayList<Notification>();
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<Post> filteredNotification = filterNewPostNotification((ListNotification<Post>) notification, currentSone, true);
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<PostReply> filteredNotification = filterNewReplyNotification((ListNotification<PostReply>) notification, currentSone);
*/
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) {
*/
@Override
public Object get(TemplateContext templateContext, Object object, String member) {
- if (object == null) {
- return null;
- }
Collection<?> collection = (Collection<?>) object;
if (member.equals("soneNames")) {
List<Sone> sones = new ArrayList<Sone>();
Identity identity = (Identity) object;
if ("uniqueNickname".equals(member)) {
int minLength = -1;
- boolean found = false;
- Set<OwnIdentity> ownIdentities = null;
- ownIdentities = core.getIdentityManager().getAllOwnIdentities();
+ boolean found;
+ Set<OwnIdentity> ownIdentities = core.getIdentityManager().getAllOwnIdentities();
do {
boolean unique = true;
String abbreviatedWantedNickname = getAbbreviatedNickname(identity, ++minLength);
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;
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);
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;
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;
*/
@Override
public Object format(TemplateContext templateContext, Object data, Map<String, Object> 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();
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);
}
/* always show your own avatars. */
return avatarId;
}
- ShowCustomAvatars showCustomAvatars = currentSone.getOptions().<ShowCustomAvatars> getEnumOption("ShowCustomAvatars").get();
+ ShowCustomAvatars showCustomAvatars = currentSone.getOptions().getShowCustomAvatars();
if (showCustomAvatars == ShowCustomAvatars.NEVER) {
return null;
}
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) {
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;
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;
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;
package net.pterodactylus.sone.text;
+import static java.util.logging.Logger.getLogger;
+
import java.io.BufferedReader;
import java.io.IOException;
import java.io.Reader;
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;
public class SoneTextParser implements Parser<SoneTextParserContext> {
/** 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]");
* don’t use create=true above, we don’t want
* the empty shell.
*/
- sone = Optional.<Sone>of(new SoneImpl(soneId, false));
+ sone = Optional.<Sone>of(new IdOnlySone(soneId));
}
parts.add(new SonePart(sone.get()));
line = line.substring(50);
--- /dev/null
+package net.pterodactylus.sone.utils;
+
+import com.google.common.base.Predicate;
+
+/**
+ * Basic implementation of an {@link Option}.
+ *
+ * @param <T>
+ * The type of the option
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class DefaultOption<T> implements Option<T> {
+
+ /** The default value. */
+ private final T defaultValue;
+
+ /** The current value. */
+ private volatile T value;
+
+ /** The validator. */
+ private Predicate<T> 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<T> 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;
+ }
+
+}
return (value != null) && (value >= lowerBound) && (value <= upperBound);
}
+ public static IntegerRangePredicate range(int lowerBound, int upperBound) {
+ return new IntegerRangePredicate(lowerBound, upperBound);
+ }
+
}
--- /dev/null
+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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+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;
+ }
+
+}
--- /dev/null
+package net.pterodactylus.sone.utils;
+
+/**
+ * Contains current and default value of an option.
+ *
+ * @param <T>
+ * The type of the option
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public interface Option<T> {
+
+ /**
+ * 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;
+
+}
--- /dev/null
+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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class Optionals {
+
+ public static Predicate<Optional<?>> isPresent() {
+ return new Predicate<Optional<?>>() {
+ @Override
+ public boolean apply(Optional<?> input) {
+ return input.isPresent();
+ }
+ };
+ }
+
+ public static <T> Function<Optional<T>, T> get() {
+ return new Function<Optional<T>, T>() {
+ @Override
+ public T apply(Optional<T> input) {
+ return input.get();
+ }
+ };
+ }
+
+}
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.
*
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> post = webInterface.getCore().getPost(id);
+ if (post.isPresent()) {
+ webInterface.getCore().bookmarkPost(post.get());
+ }
throw new RedirectException(returnPage);
}
}
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;
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;
@Override
public boolean apply(Post post) {
- return post.getSone() != null;
+ return post.isLoaded();
}
});
List<Post> sortedPosts = new ArrayList<Post>(loadedPosts);
Collections.sort(sortedPosts, Post.TIME_COMPARATOR);
- Pagination<Post> pagination = new Pagination<Post>(sortedPosts, webInterface.getCore().getPreferences().getPostsPerPage()).setPage(Numbers.safeParseInteger(request.getHttpRequest().getParam("page"), 0));
+ Pagination<Post> pagination = new Pagination<Post>(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());
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;
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());
}
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<Sone> 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);
}
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());
}
package net.pterodactylus.sone.web;
+import static java.util.logging.Logger.getLogger;
+
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
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;
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.
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");
}
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");
}
}
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);
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;
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");
}
}
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());
}
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();
}
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;
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);
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);
}
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;
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;
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"));
albums.addAll(from(sone.getRootAlbum().getAlbums()).transformAndConcat(FLATTENER).filter(NOT_EMPTY).toList());
}
Collections.sort(albums, TITLE_COMPARATOR);
- Pagination<Album> albumPagination = new Pagination<Album>(albums, 12).setPage(Numbers.safeParseInteger(request.getHttpRequest().getParam("page"), 0));
+ Pagination<Album> albumPagination = new Pagination<Album>(albums, 12).setPage(parseInt(request.getHttpRequest().getParam("page"), 0));
templateContext.set("albumPagination", albumPagination);
templateContext.set("albums", albumPagination.getItems());
return;
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;
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;
allPosts = Collections2.filter(allPosts, Post.FUTURE_POSTS_FILTER);
List<Post> sortedPosts = new ArrayList<Post>(allPosts);
Collections.sort(sortedPosts, Post.TIME_COMPARATOR);
- Pagination<Post> pagination = new Pagination<Post>(sortedPosts, webInterface.getCore().getPreferences().getPostsPerPage()).setPage(Numbers.safeParseInteger(request.getHttpRequest().getParam("page"), 0));
+ Pagination<Post> pagination = new Pagination<Post>(sortedPosts, webInterface.getCore().getPreferences().getPostsPerPage()).setPage(parseInt(request.getHttpRequest().getParam("page"), 0));
templateContext.set("pagination", pagination);
templateContext.set("posts", pagination.getItems());
}
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;
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;
*/
public class KnownSonesPage extends SoneTemplatePage {
+ private static final String defaultSortField = "activity";
+ private static final String defaultSortOrder = "desc";
+
/**
* Creates a “known Sones” page.
*
@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<Sone> knownSones = Collections2.filter(webInterface.getCore().getSones(), Sone.EMPTY_SONE_FILTER);
Collections.sort(sortedSones, Sone.NICE_NAME_COMPARATOR);
}
}
- Pagination<Sone> sonePagination = new Pagination<Sone>(sortedSones, 25).setPage(Numbers.safeParseInteger(request.getHttpRequest().getParam("page"), 0));
+ Pagination<Sone> sonePagination = new Pagination<Sone>(sortedSones, 25).setPage(parseInt(request.getHttpRequest().getParam("page"), 0));
templateContext.set("pagination", sonePagination);
templateContext.set("knownSones", sonePagination.getItems());
}
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);
}
package net.pterodactylus.sone.web;
+import static java.util.logging.Logger.getLogger;
+
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
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;
/** 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.
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");
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;
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;
Collections.sort(sortedPosts, Post.TIME_COMPARATOR);
/* paginate them. */
- Pagination<Post> pagination = new Pagination<Post>(sortedPosts, webInterface.getCore().getPreferences().getPostsPerPage()).setPage(Numbers.safeParseInteger(request.getHttpRequest().getParam("page"), 0));
+ Pagination<Post> pagination = new Pagination<Post>(sortedPosts, webInterface.getCore().getPreferences().getPostsPerPage()).setPage(parseInt(request.getHttpRequest().getParam("page"), 0));
templateContext.set("pagination", pagination);
templateContext.set("posts", pagination.getItems());
}
package net.pterodactylus.sone.web;
+import static net.pterodactylus.sone.utils.NumberParsers.parseInt;
+
import java.util.ArrayList;
import java.util.List;
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;
List<String> fieldErrors = new ArrayList<String>();
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().<ShowCustomAvatars> 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 {
}
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 {
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();
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().<ShowCustomAvatars> 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());
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;
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);
}
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;
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;
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<List<Phrase>, Set<Hit<Post>>> hitCache = CacheBuilder.newBuilder().expireAfterWrite(5, TimeUnit.MINUTES).build(new CacheLoader<List<Phrase>, Set<Hit<Post>>>() {
List<Post> resultPosts = FluentIterable.from(sortedPostHits).transform(new HitMapper<Post>()).toList();
/* pagination. */
- Pagination<Sone> sonePagination = new Pagination<Sone>(resultSones, webInterface.getCore().getPreferences().getPostsPerPage()).setPage(Numbers.safeParseInteger(request.getHttpRequest().getParam("sonePage"), 0));
- Pagination<Post> postPagination = new Pagination<Post>(resultPosts, webInterface.getCore().getPreferences().getPostsPerPage()).setPage(Numbers.safeParseInteger(request.getHttpRequest().getParam("postPage"), 0));
+ Pagination<Sone> sonePagination = new Pagination<Sone>(resultSones, webInterface.getCore().getPreferences().getPostsPerPage()).setPage(fromNullable(tryParse(request.getHttpRequest().getParam("sonePage"))).or(0));
+ Pagination<Post> postPagination = new Pagination<Post>(resultPosts, webInterface.getCore().getPreferences().getPostsPerPage()).setPage(fromNullable(tryParse(request.getHttpRequest().getParam("postPage"))).or(0));
templateContext.set("sonePagination", sonePagination);
templateContext.set("soneHits", sonePagination.getItems());
* @return The parsed phrases
*/
private static List<Phrase> parseSearchPhrases(String query) {
- List<String> parsedPhrases = null;
+ List<String> parsedPhrases;
try {
parsedPhrases = StringEscaper.parseLine(query);
} catch (TextException te1) {
*/
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;
}
/**
@Override
public boolean apply(Hit<?> hit) {
- return (hit == null) ? false : hit.getScore() > 0;
+ return (hit != null) && (hit.getScore() > 0);
}
};
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.
*
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> 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<Post> 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");
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);
}
package net.pterodactylus.sone.web;
+import static java.util.logging.Logger.getLogger;
+
import java.awt.Image;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
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;
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.
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);
Bucket fileBucket = uploadedFile.getData();
InputStream imageInputStream = null;
ByteArrayOutputStream imageDataOutputStream = null;
- net.pterodactylus.sone.data.Image image = null;
try {
imageInputStream = fileBucket.getInputStream();
/* TODO - check length */
}
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);
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;
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;
List<Post> sonePosts = sone.get().getPosts();
sonePosts.addAll(webInterface.getCore().getDirectedPosts(sone.get().getId()));
Collections.sort(sonePosts, Post.TIME_COMPARATOR);
- Pagination<Post> postPagination = new Pagination<Post>(sonePosts, webInterface.getCore().getPreferences().getPostsPerPage()).setPage(Numbers.safeParseInteger(request.getHttpRequest().getParam("postPage"), 0));
+ Pagination<Post> postPagination = new Pagination<Post>(sonePosts, webInterface.getCore().getPreferences().getPostsPerPage()).setPage(parseInt(request.getHttpRequest().getParam("postPage"), 0));
templateContext.set("postPagination", postPagination);
templateContext.set("posts", postPagination.getItems());
Set<PostReply> replies = sone.get().getReplies();
});
- Pagination<Post> repliedPostPagination = new Pagination<Post>(posts, webInterface.getCore().getPreferences().getPostsPerPage()).setPage(Numbers.safeParseInteger(request.getHttpRequest().getParam("repliedPostPage"), 0));
+ Pagination<Post> repliedPostPagination = new Pagination<Post>(posts, webInterface.getCore().getPreferences().getPostsPerPage()).setPage(parseInt(request.getHttpRequest().getParam("repliedPostPage"), 0));
templateContext.set("repliedPostPagination", repliedPostPagination);
templateContext.set("repliedPosts", repliedPostPagination.getItems());
}
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;
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;
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;
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();
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<Sone>("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<Post>("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<Post>("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<PostReply>("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<PostReply>("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<Post>("mention-notification", "posts", mentionNotificationTemplate, false);
- Template lockedSonesTemplate = TemplateParser.parse(createReader("/templates/notify/lockedSonesNotification.html"));
+ Template lockedSonesTemplate = parseTemplate("/templates/notify/lockedSonesNotification.html");
lockedSonesNotification = new ListNotification<Sone>("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<Image>("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<Image>("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>("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
//
if (soneId == null) {
return null;
}
- return getCore().getLocalSone(soneId, false);
+ return getCore().getLocalSone(soneId);
}
/**
*/
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);
}
*/
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);
}
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);
}
}, 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() {
* 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<FreenetRequest>("", "index.html")));
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<FreenetRequest>("css/", "/static/css/", "text/css")));
}
/**
- * 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}).
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);
}
}
}
+ 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
//
}
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);
}
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());
+ }
}
/**
*/
@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());
+ }
}
/**
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());
+ }
}
/**
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());
}
}
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);
}
}
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);
}
}
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);
}
}
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.
*
if ((id == null) || (id.length() == 0)) {
return createErrorJsonObject("invalid-post-id");
}
- webInterface.getCore().bookmarkPost(id);
+ Optional<Post> post = webInterface.getCore().getPost(id);
+ if (post.isPresent()) {
+ webInterface.getCore().bookmarkPost(post.get());
+ }
return createSuccessJsonObject();
}
String recipientId = request.getHttpRequest().getParam("recipient");
Optional<Sone> 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;
}
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());
}
@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");
}
}
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");
+ }
}
}
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();
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;
}
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;
}
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;
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;
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();
@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");
}
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.
*
if ((id == null) || (id.length() == 0)) {
return createErrorJsonObject("invalid-post-id");
}
- webInterface.getCore().unbookmarkPost(id);
+ Optional<Post> post = webInterface.getCore().getPost(id);
+ if (post.isPresent()) {
+ webInterface.getCore().unbookmarkPost(post.get());
+ }
return createSuccessJsonObject();
}
@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");
}
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;
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;
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;
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());
}
*/
@Override
public boolean isLinkExcepted(URI link) {
- return (page instanceof FreenetPage) ? ((FreenetPage) page).isLinkExcepted(link) : false;
+ return (page instanceof FreenetPage) && ((FreenetPage) page).isLinkExcepted(link);
}
}
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.
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.
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.
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.
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}.
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<seconds}.
Notification.SoneInsertAborted.Text=Your Sone sone://{0} could not be inserted.
-# 100, 454-456
+# 60, 100, 308-310, 312-314, 463-465
Page.Index.PostList.Title=ポストフィード
Page.Index.PostList.Text.NoPostYet=誰も投稿していません。ぜひ投稿をスタートしましょう!
Page.Index.PostList.Text.FollowSomeSones=もしくは誰もフォローしてないですか? {link}既知のSone{/link}を表示し、面白そうなSoneをフォローしましょう。
-Page.Index.PostList.Text.AutoFollowOption=You also have the option of automatically following newly discovered Sones. Take a look at the {link}options{/link} to activate the auto-follow feature!
+Page.Index.PostList.Text.AutoFollowOption=新しく発見したSoneを自動的にフォローすることができます。{link}オプション{/link}から設定が可能です。
Page.New.Title=新しい投稿と返信 - Sone
Page.New.Page.Title=新しい投稿と返信
Page.NoPermission.Page.Title=不正なアクセス
Page.NoPermission.Text.NoPermission=許可されていないアクセスが検知されました。これ以上続行すると防衛機構が作動するかも知れません!
+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=通知を消す - Sone
Page.WotPluginMissing.Text.WotRequired=Web of TrustはSoneで必要な機構であるため、Soneの読み込みにはまずはWeb of Trustが読み出されている必要があります。
Notification.SoneIsInserting.Text=あなたのSone sone://{0}は現在インサート中です。
Notification.SoneIsInserted.Text=あなたのSone sone://{0}は{1,number}{1,choice,0#秒|1#秒|1<秒}でインサートされました。
Notification.SoneInsertAborted.Text=あなたのSone sone://{0}のインサートに失敗しました。
-# 100
+# 60, 100, 308-310, 312-314
Page.NoPermission.Page.Title=Ikke-autorisert tilgang
Page.NoPermission.Text.NoPermission=Du prøvde å gjøre noe som du ikke har tilstrekkelige rettigheter til. Vennligst avstå fra slike handlinger i framtiden ellers vil vi bli tvunget til å ta til motgrep!
+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=Fjern varsel - Sone
Page.WotPluginMissing.Text.WotRequired=Fordi 'Web Of Trust' er en integrert del av Sone, må 'Web Of Trust'-tillegget bli lastet for å kunne kjøre Sone.
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<seconds}.
Notification.SoneInsertAborted.Text=Your Sone sone://{0} could not be inserted.
-# 100, 120-121, 454-456
+# 60, 100, 120-121, 308-310, 312-314, 463-465
Page.NoPermission.Page.Title=Nieupoważniony dostęp
Page.NoPermission.Text.NoPermission=Próbowałeś zrobić coś do czego nie masz wystarczającej autoryzacji. Na przyszłość nie podejmuj tego typu działań, albo będziemy zmuszeni podjać odpowiednie kroki.
+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=Odrzuć powiadomienie - Sone
Page.WotPluginMissing.Text.WotRequired=Sieć Zaufania jest integralną częścią Sone. Należy załądować wtyczkę Sieci Zaufania, aby móc korzystać z Sone.
Notification.SoneIsInserting.Text=Twoje Sone sone://{0} jest w tej chili wysyłane.
Notification.SoneIsInserted.Text=Twoje sone://{0} zostało wysłane w {1,number} {1,choice,0#seconds|1#second|1<seconds}.
Notification.SoneInsertAborted.Text=Twoje Sone sone://{0} nie mogło zostać wysłane.
-# 100
+# 60, 100, 308-310, 312-314
Page.NoPermission.Page.Title=Неавторизованный доступ
Page.NoPermission.Text.NoPermission=Вы пытались сделать что-то, для чего у вас недостаточно авторизации. Пожалуйска воздержитесь от подобных действий в будущем или мы будем вынуждены применить контр-меры!
+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=Скрыть уведомление - Sone
Page.WotPluginMissing.Text.WotRequired=Так как Web of Trust - неотъемлимая часть Sone, дополнение Web of Trust должно быть запущего для работы Sone.
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<seconds}.
Notification.SoneInsertAborted.Text=Your Sone sone://{0} could not be inserted.
-# 100, 120-121, 454-456
+# 60, 100, 120-121, 308-310, 312-314, 463-465
--- /dev/null
+<%include include/head.html>
+
+ <h1><%= Page.EmptyAlbumTitle.Page.Title|l10n|html></h1>
+
+ <p><%= Page.EmptyAlbumTitle.Text.EmptyAlbumTitle|l10n|html></p>
+
+<%include include/tail.html>
--- /dev/null
+<%include include/head.html>
+
+ <h1><%= Page.EmptyImageTitle.Page.Title|l10n|html></h1>
+
+ <p><%= Page.EmptyImageTitle.Text.EmptyImageTitle|l10n|html></p>
+
+<%include include/tail.html>
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;
});
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;
});
<div class="profile-link"><a href="viewSone.html?sone=<% sone.id|html>" title="<% sone.requestUri|html>"><% sone.niceName|html></a></div>
<div class="sone-stats">(<%= 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>)</div>
</div>
- <div class="short-request-uri"><% sone.requestUri|substring start==4 length==43|html></div>
+ <div class="short-request-uri"><% sone.id|html></div>
<div class="hidden"><% sone.blacklisted></div>
<%if sone.local>
<form class="lock<%if sone.locked> hidden<%/if>" action="lockSone.html" method="post">
--- /dev/null
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class Matchers {
+
+ public static Matcher<String> matchesRegex(final String regex) {
+ return new TypeSafeMatcher<String>() {
+ @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<InputStream> delivers(final byte[] data) {
+ return new TypeSafeMatcher<InputStream>() {
+ 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<Post> isPost(String postId, long time,
+ String text, Optional<String> recipient) {
+ return new PostMatcher(postId, time, text, recipient);
+ }
+
+ public static Matcher<Post> isPostWithId(String postId) {
+ return new PostIdMatcher(postId);
+ }
+
+ public static Matcher<PostReply> isPostReply(String postReplyId,
+ String postId, long time, String text) {
+ return new PostReplyMatcher(postReplyId, postId, time, text);
+ }
+
+ public static Matcher<Album> isAlbum(final String albumId,
+ final String parentAlbumId,
+ final String title, final String albumDescription,
+ final String imageId) {
+ return new TypeSafeDiagnosingMatcher<Album>() {
+ @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<Image> 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<Image>() {
+ @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<Post> {
+
+ private final String postId;
+ private final long time;
+ private final String text;
+ private final Optional<String> recipient;
+
+ private PostMatcher(String postId, long time, String text,
+ Optional<String> 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<Post> {
+
+ 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<PostReply> {
+
+ 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);
+ }
+
+ }
+
+}
--- /dev/null
+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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class TestAlbumBuilder implements AlbumBuilder {
+
+ private final Album album = mock(Album.class);
+ private final List<Album> albums = new ArrayList<Album>();
+ private final List<Image> images = new ArrayList<Image>();
+ private Album parentAlbum;
+ private String title;
+ private String description;
+ private String imageId;
+
+ public TestAlbumBuilder() {
+ when(album.getTitle()).thenAnswer(new Answer<String>() {
+ @Override
+ public String answer(InvocationOnMock invocation) {
+ return title;
+ }
+ });
+ when(album.getDescription()).thenAnswer(new Answer<String>() {
+ @Override
+ public String answer(InvocationOnMock invocation) {
+ return description;
+ }
+ });
+ when(album.getAlbumImage()).thenAnswer(new Answer<Image>() {
+ @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<Void>() {
+ @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<Void>() {
+ @Override
+ public Void answer(InvocationOnMock invocation) {
+ images.add((Image) invocation.getArguments()[0]);
+ return null;
+ }
+ }).when(album).addImage(any(Image.class));
+ doAnswer(new Answer<Void>() {
+ @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<Album>() {
+ @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;
+ }
+
+}
--- /dev/null
+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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+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;
+ }
+
+}
--- /dev/null
+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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+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;
+ }
+
+}
--- /dev/null
+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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+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;
+ }
+
+}
--- /dev/null
+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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+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> 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> 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);
+ }
+ }
+
+}
--- /dev/null
+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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class TestValue<T> implements Value<T> {
+
+ private final AtomicReference<T> value = new AtomicReference<T>();
+
+ 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 <T> Value<T> from(T value) {
+ return new TestValue<T>(value);
+ }
+
+}
--- /dev/null
+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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+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.<String>from(null));
+ when(configuration.getIntValue(anyString())).thenReturn(
+ TestValue.<Integer>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<Field> 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<Post> posts =
+ configurationSoneParser.parsePosts(postBuilderFactory);
+ assertThat(posts,
+ Matchers.<Post>containsInAnyOrder(
+ isPost("P0", 1000L, "T0", Optional.<String>absent()),
+ isPost("P1", 1001L, "T1",
+ of("1234567890123456789012345678901234567890123"))));
+ }
+
+ private PostBuilderFactory createPostBuilderFactory() {
+ PostBuilderFactory postBuilderFactory =
+ mock(PostBuilderFactory.class);
+ when(postBuilderFactory.newPostBuilder()).thenAnswer(
+ new Answer<PostBuilder>() {
+ @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<Post> posts = configurationSoneParser.parsePosts(
+ createPostBuilderFactory());
+ assertThat(posts, contains(
+ isPost("P0", 1000L, "T0", Optional.<String>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<PostReply> 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<String> 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<String> 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<String> 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<Album> 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<AlbumBuilder>() {
+ @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<String, Album> 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<ImageBuilder>() {
+ @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());
+ }
+
+}
--- /dev/null
+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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+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));
+ }
+
+}
--- /dev/null
+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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+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<USKCallback> 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<InsertBlock> 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.<ClientPutter>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<InsertBlock> 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<String, Object> manifestEntries = new HashMap<String, Object>();
+ 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> imageInsertStartedEvent = forClass(ImageInsertStartedEvent.class);
+ verify(eventBus).post(imageInsertStartedEvent.capture());
+ assertThat(imageInsertStartedEvent.getValue().image(), is(image));
+ insertToken.cancel();
+ ArgumentCaptor<ImageInsertAbortedEvent> 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> 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> 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> 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> 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());
+ }
+
+}
--- /dev/null
+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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+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<Image, InsertToken> 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();
+ }
+
+}
--- /dev/null
+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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class OptionsTest {
+
+ private final Options options = new Options();
+
+ @Test
+ public void booleanOptionIsAdded() {
+ Option<Boolean> 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<Integer> 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<String> 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<TestEnum> enumOption = mock(Option.class);
+ options.addEnumOption("test", enumOption);
+ assertThat(options.<TestEnum>getEnumOption("test"), is(enumOption));
+ assertThat(options.<TestEnum>getEnumOption("not-test"), nullValue());
+ }
+
+ private enum TestEnum {TEST, NOT_TEST}
+
+}
--- /dev/null
+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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+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)));
+ }
+
+}
--- /dev/null
+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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+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<FullAccessRequiredChanged> 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));
+ }
+
+}
--- /dev/null
+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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+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<PostReply>(
+ asList(oldPostReply, removedPostReply)));
+ when(newSone.getReplies()).thenReturn(
+ new HashSet<PostReply>(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));
+ }
+
+}
--- /dev/null
+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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+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<FreenetURI>() {
+ @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<SoneStatus> 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);
+ }
+
+}
--- /dev/null
+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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+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.<Sone>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<Void>() {
+ @Override
+ public Void answer(InvocationOnMock invocation) throws Throwable {
+ soneInserter.stop();
+ return null;
+ }
+ }).when(core).touchConfiguration();
+ soneInserter.serviceRun();
+ ArgumentCaptor<SoneEvent> 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<FreenetURI>() {
+ @Override
+ public FreenetURI answer(InvocationOnMock invocation) throws Throwable {
+ soneInserter.stop();
+ return finalUri;
+ }
+ });
+ soneInserter.serviceRun();
+ ArgumentCaptor<SoneEvent> 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<FreenetURI>() {
+ @Override
+ public FreenetURI answer(InvocationOnMock invocation) throws Throwable {
+ soneInserter.stop();
+ throw soneException;
+ }
+ });
+ soneInserter.serviceRun();
+ ArgumentCaptor<SoneEvent> 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.<Sone>absent());
+ soneInserter.serviceRun();
+ }
+
+ @Test
+ public void soneInserterCatchesExceptionAndContinues() {
+ SoneModificationDetector soneModificationDetector =
+ mock(SoneModificationDetector.class);
+ final SoneInserter soneInserter =
+ new SoneInserter(core, eventBus, freenetInterface, "SoneId",
+ soneModificationDetector, 1);
+ Answer<Optional<Sone>> stopInserterAndThrowException =
+ new Answer<Optional<Sone>>() {
+ @Override
+ public Optional<Sone> answer(
+ InvocationOnMock invocation) {
+ soneInserter.stop();
+ throw new NullPointerException();
+ }
+ };
+ when(soneModificationDetector.isEligibleForInsert()).thenAnswer(
+ stopInserterAndThrowException);
+ soneInserter.serviceRun();
+ }
+
+ @Test
+ public void templateIsRenderedCorrectlyForManifestElement()
+ throws IOException {
+ Map<String, Object> soneProperties = new HashMap<String, Object>();
+ 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<String, Object> soneProperties = new HashMap<String, Object>();
+ 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<String, Object> soneProperties = new HashMap<String, Object>();
+ 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());
+ }
+
+}
--- /dev/null
+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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+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));
+ }
+
+}
--- /dev/null
+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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+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<Post> createdPosts = new ArrayList<Post>();
+ private Post post = mock(Post.class);
+ private final PostReplyBuilder postReplyBuilder = mock(PostReplyBuilder.class);
+ private final Set<PostReply> createdPostReplies = new HashSet<PostReply>();
+ private PostReply postReply = mock(PostReply.class);
+ private final AlbumBuilder albumBuilder = mock(AlbumBuilder.class);
+ private final ListMultimap<Album, Album>
+ nestedAlbums = ArrayListMultimap.create();
+ private final ListMultimap<Album, Image> albumImages = ArrayListMultimap.create();
+ private Album album = mock(Album.class);
+ private final Map<String, Album> albums = new HashMap<String, Album>();
+ private final ImageBuilder imageBuilder = mock(ImageBuilder.class);
+ private Image image = mock(Image.class);
+ private final Map<String, Image> images = new HashMap<String, Image>();
+
+ @Before
+ public void setupSone() {
+ setupSone(this.sone, Identity.class);
+ }
+
+ private void setupSone(Sone sone, Class<? extends Identity> 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<FreenetURI>() {
+ @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<SoneBuilder>() {
+ @Override
+ public SoneBuilder answer(InvocationOnMock invocation) {
+ return new MemorySoneBuilder(null);
+ }
+ });
+ }
+
+ @Before
+ public void setupPost() {
+ when(post.getRecipientId()).thenReturn(Optional.<String>absent());
+ }
+
+ @Before
+ public void setupPostBuilder() {
+ when(postBuilder.withId(anyString())).thenAnswer(new Answer<PostBuilder>() {
+ @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<PostBuilder>() {
+ @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<PostBuilder>() {
+ @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<PostBuilder>() {
+ @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<PostBuilder>() {
+ @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<Post>() {
+ @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<PostReplyBuilder>() {
+ @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<PostReplyBuilder>() {
+ @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<PostReplyBuilder>() {
+ @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<PostReplyBuilder>() {
+ @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<PostReplyBuilder>() {
+ @Override
+ public PostReplyBuilder answer(InvocationOnMock invocation) throws Throwable {
+ when(postReply.getText()).thenReturn((String) invocation.getArguments()[0]);
+ return postReplyBuilder;
+ }
+ });
+ when(postReplyBuilder.build()).thenAnswer(new Answer<PostReply>() {
+ @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<Void>() {
+ @Override
+ public Void answer(InvocationOnMock invocation) {
+ nestedAlbums.put(album, (Album) invocation.getArguments()[0]);
+ return null;
+ }
+ }).when(album).addAlbum(any(Album.class));
+ doAnswer(new Answer<Void>() {
+ @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<List<Album>>() {
+ @Override
+ public List<Album> answer(InvocationOnMock invocation) {
+ return nestedAlbums.get(album);
+ }
+ });
+ when(album.getImages()).thenAnswer(new Answer<List<Image>>() {
+ @Override
+ public List<Image> 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<AlbumBuilder>() {
+ @Override
+ public AlbumBuilder answer(InvocationOnMock invocation) {
+ when(album.getId()).thenReturn((String) invocation.getArguments()[0]);
+ return albumBuilder;
+ }
+ });
+ when(albumBuilder.randomId()).thenAnswer(new Answer<AlbumBuilder>() {
+ @Override
+ public AlbumBuilder answer(InvocationOnMock invocation) {
+ when(album.getId()).thenReturn(randomUUID().toString());
+ return albumBuilder;
+ }
+ });
+ when(albumBuilder.by(any(Sone.class))).thenAnswer(new Answer<AlbumBuilder>() {
+ @Override
+ public AlbumBuilder answer(InvocationOnMock invocation) {
+ when(album.getSone()).thenReturn((Sone) invocation.getArguments()[0]);
+ return albumBuilder;
+ }
+ });
+ when(albumBuilder.build()).thenAnswer(new Answer<Album>() {
+ @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<Album>() {
+ @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<ImageBuilder>() {
+ @Override
+ public ImageBuilder answer(InvocationOnMock invocation) {
+ when(image.getId()).thenReturn(randomUUID().toString());
+ return imageBuilder;
+ }
+ });
+ when(imageBuilder.withId(anyString())).thenAnswer(new Answer<ImageBuilder>() {
+ @Override
+ public ImageBuilder answer(InvocationOnMock invocation) {
+ when(image.getId()).thenReturn(
+ (String) invocation.getArguments()[0]);
+ return imageBuilder;
+ }
+ });
+ when(imageBuilder.build()).thenAnswer(new Answer<Image>() {
+ @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<Image>() {
+ @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<Post> 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.<String>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<Post> 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<Post> 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.<String>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<PostReply> 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<String>) 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<String>) 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"));
+ }
+
+
+}
--- /dev/null
+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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+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<Sone>() {
+ @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;
+ }
+
+}
--- /dev/null
+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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+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());
+ }
+
+}
--- /dev/null
+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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+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<FreenetURI> uri = forClass(FreenetURI.class);
+ ArgumentCaptor<Callback> 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> 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<Fetched>() {
+ @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);
+ }
+
+}
--- /dev/null
+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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+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<Void>() {
+ @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<Void>() {
+ @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<Void>() {
+ @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<Void>() {
+ @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<Void>() {
+ @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<Void>() {
+ @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));
+ }
+
+}
--- /dev/null
+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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+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(""));
+ }
+
+}
--- /dev/null
+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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+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();
+ }
+
+}
--- /dev/null
+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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class ImageImplTest {
+
+ private final Image image = new ImageImpl();
+
+ @Test(expected = ImageTitleMustNotBeEmpty.class)
+ public void modifierDoesNotAllowTitleDoBeEmpty() {
+ image.modify().setTitle("").update();
+ }
+
+}
--- /dev/null
+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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+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.<String>from(null));
+ Set<String> 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.<String>from(null));
+ Set<String> 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.<String>from(null));
+ Set<String> knownPosts = configurationLoader.loadBookmarkedPosts();
+ assertThat(knownPosts, containsInAnyOrder("Post1", "Post2"));
+ }
+
+ @Test
+ public void loaderCanSaveBookmarkedPosts() throws ConfigurationException {
+ final Value<String> post1 = TestValue.<String>from(null);
+ final Value<String> post2 = TestValue.<String>from(null);
+ final Value<String> post3 = TestValue.<String>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<String> originalPosts = new HashSet<String>(asList("Post1", "Post2"));
+ configurationLoader.saveBookmarkedPosts(originalPosts);
+ HashSet<String> extractedPosts =
+ new HashSet<String>(asList(post1.getValue(), post2.getValue()));
+ assertThat(extractedPosts, containsInAnyOrder("Post1", "Post2"));
+ assertThat(post3.getValue(), nullValue());
+ }
+
+}
--- /dev/null
+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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+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<String, Post> posts = new HashMap<String, Post>();
+
+ @Before
+ public void setupMemoryDatabase() {
+ when(memoryDatabase.getPost(anyString())).thenAnswer(
+ new Answer<Optional<Post>>() {
+ @Override
+ public Optional<Post> 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<Post> allPosts = new HashSet<Post>(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<Post> allPosts = new HashSet<Post>(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<Post> bookmarkedPosts =
+ bookmarkDatabase.getBookmarkedPosts();
+ assertThat(bookmarkedPosts,
+ contains(isPostWithId("PostId1"), isPostWithId("PostId3")));
+ }
+
+}
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}.
*/
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<Post> 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<PostReply> postReplies = new HashSet<PostReply>(
+ 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.<String>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.<String>absent());
+ memoryDatabase.storePost(postWithoutRecipient);
+ assertThat(memoryDatabase.getDirectedPosts(RECIPIENT_ID),
+ contains(postWithRecipient));
+ }
+
+ private Post createPost(Optional<String> 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.<String>absent());
+ PostReply firstPostFirstReply = createPostReply(firstPost, 1000L);
+ Post secondPost = createPost(Optional.<String>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.<Album>absent()));
memoryDatabase.storeAlbum(newAlbum);
assertThat(memoryDatabase.getAlbum(newAlbum.getId()), is(of(newAlbum)));
assertThat(memoryDatabase.getAlbum(newAlbum.getId()), is(Optional.<Album>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.<String>from(null));
+ }
+
+ @Test
+ public void friendsAreReturnedCorrectly() {
+ initializeFriends();
+ when(sone.isLocal()).thenReturn(true);
+ Collection<String> 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<String> 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<String, Value<String>> prepareConfigurationValues() {
+ final Map<String, Value<String>> configurationValues = new HashMap<String, Value<String>>();
+ when(configuration.getStringValue(anyString())).thenAnswer(new Answer<Value<String>>() {
+ @Override
+ public Value<String> answer(InvocationOnMock invocation) throws Throwable {
+ Value<String> stringValue = TestValue.from(null);
+ configurationValues.put((String) invocation.getArguments()[0], stringValue);
+ return stringValue;
+ }
+ });
+ return configurationValues;
+ }
+
+ @Test
+ public void friendIsAddedCorrectlyToLocalSone() {
+ Map<String, Value<String>> 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.<String>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<String, Value<String>> 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.<String>from(null)));
+ assertThat(configurationValues.get("Sone/" + SONE_ID + "/Friends/1/ID"),
+ is(TestValue.<String>from(null)));
+ }
+
+ @Test
+ public void configurationIsNotWrittenWhenANonFriendIsRemoved() {
+ prepareConfigurationValues();
+ when(sone.isLocal()).thenReturn(true);
+ memoryDatabase.removeFriend(sone, "Friend1");
+ verify(configuration).getStringValue(anyString());
+ }
+
}
--- /dev/null
+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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+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));
+ }
+
+}
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;
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);
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;
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);
--- /dev/null
+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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+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"));
+ }
+
+}
--- /dev/null
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+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.<String, String>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.<String, String>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.*"));
+ }
+
+}
--- /dev/null
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+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"));
+ }
+
+}
--- /dev/null
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.freenet.wot;
+
+import java.util.Collection;
+import java.util.Map;
+
+/**
+ * Creates {@link Identity}s and {@link OwnIdentity}s.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class Identities {
+
+ public static OwnIdentity createOwnIdentity(String id, Collection<String> contexts, Map<String, String> 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<String> contexts, Map<String, String> properties) {
+ DefaultIdentity identity = new DefaultIdentity(id, "Nickname" + id, "Request" + id);
+ setContextsAndPropertiesOnIdentity(identity, contexts, properties);
+ return identity;
+ }
+
+ private static void setContextsAndPropertiesOnIdentity(Identity identity, Collection<String> contexts, Map<String, String> properties) {
+ identity.setContexts(contexts);
+ identity.setProperties(properties);
+ }
+
+}
--- /dev/null
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class IdentityChangeDetectorTest {
+
+ private final IdentityChangeDetector identityChangeDetector = new IdentityChangeDetector(createOldIdentities());
+ private final Collection<Identity> newIdentities = newArrayList();
+ private final Collection<Identity> removedIdentities = newArrayList();
+ private final Collection<Identity> changedIdentities = newArrayList();
+ private final Collection<Identity> 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<Identity> 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"));
+ }
+
+}
--- /dev/null
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class IdentityChangeEventSenderTest {
+
+ private final EventBus eventBus = mock(EventBus.class);
+ private final List<OwnIdentity> 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<Identity> identities = asList(
+ createIdentity("I1", Collections.<String>emptyList(), Collections.<String, String>emptyMap()),
+ createIdentity("I2", Collections.<String>emptyList(), Collections.<String, String>emptyMap()),
+ createIdentity("I3", Collections.<String>emptyList(), Collections.<String, String>emptyMap()),
+ createIdentity("I2", asList("Test"), Collections.<String, String>emptyMap())
+ );
+ private final IdentityChangeEventSender identityChangeEventSender = new IdentityChangeEventSender(eventBus, createOldIdentities());
+
+ @Test
+ public void addingAnOwnIdentityIsDetectedAndReportedCorrectly() {
+ Map<OwnIdentity, Collection<Identity>> 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<OwnIdentity, Collection<Identity>> createNewIdentities() {
+ Map<OwnIdentity, Collection<Identity>> oldIdentities = new HashMap<OwnIdentity, Collection<Identity>>();
+ 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<OwnIdentity, Collection<Identity>> createOldIdentities() {
+ Map<OwnIdentity, Collection<Identity>> oldIdentities = new HashMap<OwnIdentity, Collection<Identity>>();
+ 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;
+ }
+
+}
--- /dev/null
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+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<OwnIdentity> 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<OwnIdentity> 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<Identity> createTrustedIdentitiesForFirstOwnIdentity() {
+ return newHashSet(
+ createIdentity("I11", "IN11", "IR11", asList("Test"), ImmutableMap.of("KeyA", "ValueA"))
+ );
+ }
+
+ private Set<Identity> createTrustedIdentitiesForSecondOwnIdentity() {
+ return newHashSet(
+ createIdentity("I21", "IN21", "IR21", asList("Test", "Test2"), ImmutableMap.of("KeyB", "ValueB"))
+ );
+ }
+
+ private Set<Identity> createTrustedIdentitiesForThirdOwnIdentity() {
+ return newHashSet(
+ createIdentity("I31", "IN31", "IR31", asList("Test", "Test3"), ImmutableMap.of("KeyC", "ValueC"))
+ );
+ }
+
+ private Set<Identity> createTrustedIdentitiesForFourthOwnIdentity() {
+ return emptySet();
+ }
+
+ private OwnIdentity createOwnIdentity(String id, String nickname, String requestUri, String insertUri, List<String> contexts, ImmutableMap<String, String> 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<String> contexts, ImmutableMap<String, String> properties) {
+ Identity identity = new DefaultIdentity(id, nickname, requestUri);
+ identity.setContexts(contexts);
+ identity.setProperties(properties);
+ return identity;
+ }
+
+ @Test
+ public void loadingIdentities() throws WebOfTrustException {
+ List<OwnIdentity> ownIdentities = createOwnIdentities();
+ Map<OwnIdentity, Collection<Identity>> 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.<Identity>emptySet());
+ verifyIdentitiesForOwnIdentity(identities, ownIdentities.get(3), createTrustedIdentitiesForFourthOwnIdentity());
+ }
+
+ @Test
+ public void loadingIdentitiesWithoutContext() throws WebOfTrustException {
+ List<OwnIdentity> ownIdentities = createOwnIdentities();
+ Map<OwnIdentity, Collection<Identity>> identities = identityLoaderWithoutContext.loadIdentities();
+ verify(webOfTrustConnector).loadAllOwnIdentities();
+ verify(webOfTrustConnector).loadTrustedIdentities(eq(ownIdentities.get(0)), eq(Optional.<String>absent()));
+ verify(webOfTrustConnector).loadTrustedIdentities(eq(ownIdentities.get(1)), eq(Optional.<String>absent()));
+ verify(webOfTrustConnector).loadTrustedIdentities(eq(ownIdentities.get(2)), eq(Optional.<String>absent()));
+ verify(webOfTrustConnector).loadTrustedIdentities(eq(ownIdentities.get(3)), eq(Optional.<String>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<OwnIdentity, Collection<Identity>> identities, OwnIdentity ownIdentity, Set<Identity> trustedIdentities) {
+ assertThat(identities.get(ownIdentity), Matchers.<Collection<Identity>>is(trustedIdentities));
+ }
+
+}
--- /dev/null
+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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+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();
+ }
+
+}
--- /dev/null
+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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+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)));
+ }
+
+
+}
--- /dev/null
+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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+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())));
+ }
+
+}
--- /dev/null
+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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+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<Object> backlinks =
+ (List<Object>) 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<Object> isLink(final String target,
+ final String name) {
+ return new TypeSafeDiagnosingMatcher<Object>() {
+ @Override
+ protected boolean matchesSafely(Object item,
+ Description mismatchDescription) {
+ if (!TestUtil.<String>callPrivateMethod(item, "getTarget")
+ .contains(target)) {
+ mismatchDescription.appendText("link does not contain ")
+ .appendValue(target);
+ return false;
+ }
+ if (!TestUtil.<String>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);
+ }
+ };
+ }
+
+}
--- /dev/null
+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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class CollectionAccessorTest {
+
+ private final CollectionAccessor accessor = new CollectionAccessor();
+ private final Collection<Object> collection = new ArrayList<Object>();
+
+ @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)));
+ }
+
+}
--- /dev/null
+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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class CssClassNameFilterTest {
+
+ private static final Map<String, Object> 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.<Object>is(filteredCharacters));
+ }
+
+ @Test
+ public void nullIsFiltered() {
+ assertThat(filter.format(null, null, EMPTY_MAP),
+ Matchers.<Object>is("null"));
+ }
+
+}
--- /dev/null
+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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+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<String, String> parameters =
+ new HashMap<String, String>();
+ 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);
+ }
+
+}
--- /dev/null
+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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+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.<Object>is(REQUEST_PATH));
+ verify(httpRequest, never()).getHeader("Path");
+ }
+
+ @Test
+ public void headerIsReturnedCorrectly() {
+ assertThat(accessor.get(context, httpRequest, "User-Agent"),
+ Matchers.<Object>is(USER_AGENT));
+ }
+
+}
--- /dev/null
+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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+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<OwnIdentity> 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.<Object>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.<Object>is("Test@" + TEST_ID));
+ }
+
+ @Test
+ public void reflectionAccessorIsUsedForOtherMembers() {
+ assertThat(accessor.get(null, identity, "hashCode"),
+ Matchers.<Object>is(identity.hashCode()));
+ }
+
+}
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}.
*
*/
private static class TestSoneProvider implements SoneProvider {
+ @Override
+ public Function<String, Optional<Sone>> soneLoader() {
+ return new Function<String, Optional<Sone>>() {
+ @Override
+ public Optional<Sone> apply(String soneId) {
+ return getSone(soneId);
+ }
+ };
+ }
+
/**
* {@inheritDoc}
*/
@Override
public Optional<Sone> getSone(final String soneId) {
- return Optional.<Sone>of(new SoneImpl(soneId, false) {
-
- /**
- * {@inheritDoc}
- */
- @Override
- public String getName() {
- return soneId;
- }
- });
+ return Optional.<Sone>of(new IdOnlySone(soneId));
}
/**
--- /dev/null
+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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class DefaultOptionTest {
+
+ private final Object defaultValue = new Object();
+ private final Object acceptedValue = new Object();
+ private final Predicate<Object> matchesAcceptedValue = new Predicate<Object>() {
+ @Override
+ public boolean apply(@Nullable Object object) {
+ return acceptedValue.equals(object);
+ }
+ };
+
+ @Test
+ public void defaultOptionReturnsDefaultValueWhenUnset() {
+ DefaultOption<Object> defaultOption = new DefaultOption<Object>(defaultValue);
+ assertThat(defaultOption.get(), is(defaultValue));
+ }
+
+ @Test
+ public void defaultOptionReturnsNullForRealWhenUnset() {
+ DefaultOption<Object> defaultOption = new DefaultOption<Object>(defaultValue);
+ assertThat(defaultOption.getReal(), nullValue());
+ }
+
+ @Test
+ public void defaultOptionWillReturnSetValue() {
+ DefaultOption<Object> defaultOption = new DefaultOption<Object>(defaultValue);
+ Object newValue = new Object();
+ defaultOption.set(newValue);
+ assertThat(defaultOption.get(), is(newValue));
+ }
+
+ @Test
+ public void defaultOptionWithValidatorAcceptsValidValues() {
+ DefaultOption<Object> defaultOption = new DefaultOption<Object>(defaultValue, matchesAcceptedValue);
+ defaultOption.set(acceptedValue);
+ assertThat(defaultOption.get(), is(acceptedValue));
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void defaultOptionWithValidatorRejectsInvalidValues() {
+ DefaultOption<Object> defaultOption = new DefaultOption<Object>(defaultValue, matchesAcceptedValue);
+ defaultOption.set(new Object());
+ }
+
+ @Test
+ public void defaultOptionValidatesObjectsCorrectly() {
+ DefaultOption<Object> defaultOption = new DefaultOption<Object>(defaultValue, matchesAcceptedValue);
+ assertThat(defaultOption.validate(acceptedValue), is(true));
+ assertThat(defaultOption.validate(new Object()), is(false));
+ }
+
+ @Test
+ public void settingToNullWillRestoreDefaultValue() {
+ DefaultOption<Object> defaultOption = new DefaultOption<Object>(defaultValue);
+ defaultOption.set(null);
+ assertThat(defaultOption.get(), is(defaultValue));
+ }
+
+ @Test
+ public void validateWithoutValidatorWillValidateNull() {
+ DefaultOption<Object> defaultOption = new DefaultOption<Object>(defaultValue);
+ assertThat(defaultOption.validate(null), is(true));
+ }
+
+ @Test
+ public void validateWithValidatorWillValidateNull() {
+ DefaultOption<Object> defaultOption = new DefaultOption<Object>(defaultValue, matchesAcceptedValue);
+ assertThat(defaultOption.validate(null), is(true));
+ }
+
+}
--- /dev/null
+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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+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.<Integer>getPrivateField(predicate, "lowerBound"),
+ is(-50));
+ assertThat(TestUtil.<Integer>getPrivateField(predicate, "upperBound"),
+ is(50));
+ }
+
+}
--- /dev/null
+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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+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());
+ }
+
+}
--- /dev/null
+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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+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<Optional<Object>> optionals = Arrays.asList(
+ Optional.of(object1), Optional.absent(),
+ Optional.of(object2), Optional.absent(),
+ Optional.of(object3), Optional.absent()
+ );
+ List<Optional<Object>> 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<Optional<Object>> optionals = Arrays.asList(
+ Optional.of(object1),
+ Optional.of(object2),
+ Optional.of(object3)
+ );
+ List<Object> objects = FluentIterable.from(optionals).transform(Optionals.get()).toList();
+ MatcherAssert.assertThat(objects, Matchers.contains(object1, object2, object3));
+ }
+
+}
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;
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;
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");
assertThat(jsonReturnObject.isSuccess(), is(true));
/* verify behaviour. */
- verify(core, times(1)).bookmarkPost(anyString());
- verify(core).bookmarkPost("abc");
+ verify(core).bookmarkPost(post);
}
@Test
assertThat(((JsonErrorReturnObject) jsonReturnObject).getError(), is("invalid-post-id"));
/* verify behaviour. */
- verify(core, never()).bookmarkPost(anyString());
+ verify(core, never()).bookmarkPost(any(Post.class));
}
}
--- /dev/null
+<%include stuff>
--- /dev/null
+Sone Version: <% version %>
--- /dev/null
+Sone Version: <% version>
+Core Startup: <% core.startupTime>
+Sone ID: <% currentSone.id>
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<sone>
+ <protocol-version>0</protocol-version>
+ <client></client>
+ <time>1407197508000</time>
+ <profile></profile>
+</sone>
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<sone>
+ <protocol-version>0</protocol-version>
+ <client>
+ <name>some-client</name>
+ </client>
+ <time>1407197508000</time>
+ <profile></profile>
+</sone>
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<sone>
+ <time>1407197508000</time>
+ <profile></profile>
+</sone>
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<sone>
+ <protocol-version>-1</protocol-version>
+</sone>
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<sone>
+ <protocol-version>0</protocol-version>
+ <time>1407197508000</time>
+ <profile></profile>
+</sone>
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<sone>
+ <protocol-version>0</protocol-version>
+ <time>1407197508000</time>
+</sone>
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<sone>
+ <protocol-version>0</protocol-version>
+</sone>
--- /dev/null
+Not an XML file.
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<sone>
+ <protocol-version>0</protocol-version>
+ <time>1407197508000</time>
+ <profile>
+ <fields>
+ <field>
+ <field-name>field</field-name>
+ <field-name>value</field-name>
+ </field>
+ <field>
+ <field-name>field</field-name>
+ <field-name>value</field-name>
+ </field>
+ </fields>
+ </profile>
+</sone>
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<sone>
+ <protocol-version>0</protocol-version>
+ <time>1407197508000</time>
+ <profile>
+ <fields>
+ <field>
+ <field-name> </field-name>
+ </field>
+ </fields>
+ </profile>
+</sone>
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<sone>
+ <protocol-version>0</protocol-version>
+ <time>1407197508000</time>
+ <profile>
+ <fields>
+ <field></field>
+ </fields>
+ </profile>
+</sone>
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<sone>
+ <protocol-version>0</protocol-version>
+ <time>not-a-number</time>
+</sone>
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<sone>
+ <protocol-version>99999</protocol-version>
+</sone>
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<sone>
+ <protocol-version>0</protocol-version>
+ <client>
+ <name>some-client</name>
+ <version>some-version</version>
+ </client>
+ <time>1407197508000</time>
+ <profile></profile>
+</sone>
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<sone>
+ <protocol-version>0</protocol-version>
+ <time>1407197508000</time>
+ <profile>
+ <avatar>image-id</avatar>
+ </profile>
+ <albums>
+ <album>
+ <id>album-id-1</id>
+ <title>album-title</title>
+ <description>album-description</description>
+ <images>
+ <image>
+ <id>image-id</id>
+ <creation-time>1407197508000</creation-time>
+ <key>KSK@GPLv3.txt</key>
+ <title>image-title</title>
+ <description>image-description</description>
+ <width>1920</width>
+ <height>1080</height>
+ </image>
+ </images>
+ </album>
+ <album>
+ <id>album-id-2</id>
+ <parent>album-id-1</parent>
+ <title>album-title-2</title>
+ <description>album-description-2</description>
+ </album>
+ </albums>
+</sone>
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<sone>
+ <protocol-version>0</protocol-version>
+ <time>1407197508000</time>
+ <profile></profile>
+ <albums>
+ <album>
+ <id>album-id-1</id>
+ <title>album-title</title>
+ <description>album-description</description>
+ <images>
+ <image>
+ <id>image-id</id>
+ <creation-time>1407197508000</creation-time>
+ <key>KSK@GPLv3.txt</key>
+ <title>image-title</title>
+ <width>1920</width>
+ <height>-1080</height>
+ </image>
+ </images>
+ </album>
+ <album>
+ <id>album-id-2</id>
+ <parent>album-id-1</parent>
+ <title>album-title-2</title>
+ <description>album-description-2</description>
+ </album>
+ </albums>
+</sone>
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<sone>
+ <protocol-version>0</protocol-version>
+ <time>1407197508000</time>
+ <profile></profile>
+ <albums>
+ <album>
+ <id>album-id-1</id>
+ <title>album-title</title>
+ <description>album-description</description>
+ <images>
+ <image>
+ <id>image-id</id>
+ <creation-time>1407197508000</creation-time>
+ <key>KSK@GPLv3.txt</key>
+ <title>image-title</title>
+ <width>-1920</width>
+ <height>1080</height>
+ </image>
+ </images>
+ </album>
+ <album>
+ <id>album-id-2</id>
+ <parent>album-id-1</parent>
+ <title>album-title-2</title>
+ <description>album-description-2</description>
+ </album>
+ </albums>
+</sone>
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<sone>
+ <protocol-version>0</protocol-version>
+ <time>1407197508000</time>
+ <profile></profile>
+ <albums>
+ <album>
+ <id>album-id-1</id>
+ <parent>album-id-7</parent>
+ <title>album-title</title>
+ <description>album-description</description>
+ </album>
+ </albums>
+</sone>
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<sone>
+ <protocol-version>0</protocol-version>
+ <time>1407197508000</time>
+ <profile></profile>
+ <replies>
+ <reply>
+ <id>reply-id</id>
+ <post-id>post-id</post-id>
+ <time>not-a-time</time>
+ <text>reply-text</text>
+ </reply>
+ </replies>
+</sone>
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<sone>
+ <protocol-version>0</protocol-version>
+ <time>1407197508000</time>
+ <profile></profile>
+ <posts>
+ <post>
+ <id>post-id</id>
+ <time>invalid-time</time>
+ <text>text</text>
+ </post>
+ </posts>
+</sone>
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<sone>
+ <protocol-version>0</protocol-version>
+ <time>1407197508000</time>
+ <profile></profile>
+ <posts>
+ <post>
+ <id>post-id</id>
+ <time>1407197508000</time>
+ <text>text</text>
+ <recipient>123456789012345678901234567890123456789012</recipient>
+ </post>
+ </posts>
+</sone>
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<sone>
+ <protocol-version>0</protocol-version>
+ <time>1407197508000</time>
+ <profile></profile>
+ <post-likes>
+ <post-like>liked-post-id</post-like>
+ </post-likes>
+</sone>
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<sone>
+ <protocol-version>0</protocol-version>
+ <time>1407197508000</time>
+ <profile></profile>
+ <reply-likes>
+ <reply-like>liked-post-reply-id</reply-like>
+ </reply-likes>
+</sone>
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<sone>
+ <protocol-version>0</protocol-version>
+ <time>1407197508000</time>
+ <profile></profile>
+ <albums>
+ <album>
+ <id>album-id-1</id>
+ <title>album-title</title>
+ <description>album-description</description>
+ </album>
+ <album>
+ <id>album-id-2</id>
+ <parent>album-id-1</parent>
+ <title>album-title-2</title>
+ <description>album-description-2</description>
+ </album>
+ </albums>
+</sone>
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<sone>
+ <protocol-version>0</protocol-version>
+ <time>1407197508000</time>
+ <profile>
+ <first-name>first</first-name>
+ <middle-name>middle</middle-name>
+ <last-name>last</last-name>
+ <birth-day>18</birth-day>
+ <birth-month>12</birth-month>
+ <birth-year>1976</birth-year>
+ </profile>
+</sone>
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<sone>
+ <protocol-version>0</protocol-version>
+ <time>1407197508000</time>
+ <profile></profile>
+ <posts>
+ <post>
+ <id>post-id</id>
+ <time>1407197508000</time>
+ <text>text</text>
+ <recipient>1234567890123456789012345678901234567890123</recipient>
+ </post>
+ </posts>
+</sone>
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<sone>
+ <protocol-version>0</protocol-version>
+ <time>1407197508000</time>
+ <profile></profile>
+ <replies>
+ <reply>
+ <id>reply-id</id>
+ <post-id>post-id</post-id>
+ <time>1407197508000</time>
+ <text>reply-text</text>
+ </reply>
+ </replies>
+</sone>
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<sone>
+ <protocol-version>0</protocol-version>
+ <time>1407197508000</time>
+ <profile></profile>
+ <posts>
+ <post>
+ <id>post-id</id>
+ <time>1407197508000</time>
+ <text>text</text>
+ </post>
+ </posts>
+</sone>
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<sone>
+ <protocol-version>0</protocol-version>
+ <time>0</time>
+ <profile></profile>
+</sone>
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<sone>
+ <protocol-version>0</protocol-version>
+ <time>1407197508000</time>
+ <profile></profile>
+ <albums>
+ <album>
+ </album>
+ </albums>
+</sone>
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<sone>
+ <protocol-version>0</protocol-version>
+ <time>1407197508000</time>
+ <profile></profile>
+ <albums>
+ <album>
+ <id>album-id-1</id>
+ </album>
+ </albums>
+</sone>
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<sone>
+ <protocol-version>0</protocol-version>
+ <time>1407197508000</time>
+ <profile></profile>
+ <albums>
+ </albums>
+</sone>
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<sone>
+ <protocol-version>0</protocol-version>
+ <time>1407197508000</time>
+ <profile>
+ <fields></fields>
+ </profile>
+</sone>
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<sone>
+ <protocol-version>0</protocol-version>
+ <time>1407197508000</time>
+ <profile></profile>
+ <albums>
+ <album>
+ <id>album-id-1</id>
+ <title>album-title</title>
+ <description>album-description</description>
+ <images>
+ <image>
+ <id>image-id</id>
+ <creation-time>1407197508000</creation-time>
+ <key>KSK@GPLv3.txt</key>
+ <title>image-title</title>
+ <width>1920</width>
+ </image>
+ </images>
+ </album>
+ <album>
+ <id>album-id-2</id>
+ <parent>album-id-1</parent>
+ <title>album-title-2</title>
+ <description>album-description-2</description>
+ </album>
+ </albums>
+</sone>
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<sone>
+ <protocol-version>0</protocol-version>
+ <time>1407197508000</time>
+ <profile></profile>
+ <albums>
+ <album>
+ <id>album-id-1</id>
+ <title>album-title</title>
+ <description>album-description</description>
+ <images>
+ <image>
+ </image>
+ </images>
+ </album>
+ <album>
+ <id>album-id-2</id>
+ <parent>album-id-1</parent>
+ <title>album-title-2</title>
+ <description>album-description-2</description>
+ </album>
+ </albums>
+</sone>
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<sone>
+ <protocol-version>0</protocol-version>
+ <time>1407197508000</time>
+ <profile></profile>
+ <albums>
+ <album>
+ <id>album-id-1</id>
+ <title>album-title</title>
+ <description>album-description</description>
+ <images>
+ <image>
+ <id>image-id</id>
+ <creation-time>1407197508000</creation-time>
+ </image>
+ </images>
+ </album>
+ <album>
+ <id>album-id-2</id>
+ <parent>album-id-1</parent>
+ <title>album-title-2</title>
+ <description>album-description-2</description>
+ </album>
+ </albums>
+</sone>
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<sone>
+ <protocol-version>0</protocol-version>
+ <time>1407197508000</time>
+ <profile></profile>
+ <albums>
+ <album>
+ <id>album-id-1</id>
+ <title>album-title</title>
+ <description>album-description</description>
+ <images>
+ <image>
+ <id>image-id</id>
+ </image>
+ </images>
+ </album>
+ <album>
+ <id>album-id-2</id>
+ <parent>album-id-1</parent>
+ <title>album-title-2</title>
+ <description>album-description-2</description>
+ </album>
+ </albums>
+</sone>
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<sone>
+ <protocol-version>0</protocol-version>
+ <time>1407197508000</time>
+ <profile></profile>
+ <albums>
+ <album>
+ <id>album-id-1</id>
+ <title>album-title</title>
+ <description>album-description</description>
+ <images>
+ <image>
+ <id>image-id</id>
+ <creation-time>1407197508000</creation-time>
+ <key>KSK@GPLv3.txt</key>
+ </image>
+ </images>
+ </album>
+ <album>
+ <id>album-id-2</id>
+ <parent>album-id-1</parent>
+ <title>album-title-2</title>
+ <description>album-description-2</description>
+ </album>
+ </albums>
+</sone>
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<sone>
+ <protocol-version>0</protocol-version>
+ <time>1407197508000</time>
+ <profile></profile>
+ <albums>
+ <album>
+ <id>album-id-1</id>
+ <title>album-title</title>
+ <description>album-description</description>
+ <images>
+ <image>
+ <id>image-id</id>
+ <creation-time>1407197508000</creation-time>
+ <key>KSK@GPLv3.txt</key>
+ <title>image-title</title>
+ </image>
+ </images>
+ </album>
+ <album>
+ <id>album-id-2</id>
+ <parent>album-id-1</parent>
+ <title>album-title-2</title>
+ <description>album-description-2</description>
+ </album>
+ </albums>
+</sone>
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<sone>
+ <protocol-version>0</protocol-version>
+ <time>1407197508000</time>
+ <profile></profile>
+ <albums>
+ <album>
+ <id>album-id-1</id>
+ <title>album-title</title>
+ <description>album-description</description>
+ <images>
+ </images>
+ </album>
+ <album>
+ <id>album-id-2</id>
+ <parent>album-id-1</parent>
+ <title>album-title-2</title>
+ <description>album-description-2</description>
+ </album>
+ </albums>
+</sone>
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<sone>
+ <protocol-version>0</protocol-version>
+ <time>1407197508000</time>
+ <profile></profile>
+ <post-likes>
+ </post-likes>
+</sone>
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<sone>
+ <protocol-version>0</protocol-version>
+ <time>1407197508000</time>
+ <profile></profile>
+ <reply-likes>
+ </reply-likes>
+</sone>
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<sone>
+ <protocol-version>0</protocol-version>
+ <time>1407197508000</time>
+ <profile></profile>
+ <posts>
+ <post>
+ </post>
+ </posts>
+</sone>
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<sone>
+ <protocol-version>0</protocol-version>
+ <time>1407197508000</time>
+ <profile></profile>
+ <replies>
+ <reply>
+ </reply>
+ </replies>
+</sone>
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<sone>
+ <protocol-version>0</protocol-version>
+ <time>1407197508000</time>
+ <profile></profile>
+ <replies>
+ <reply>
+ <id>reply-id</id>
+ </reply>
+ </replies>
+</sone>
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<sone>
+ <protocol-version>0</protocol-version>
+ <time>1407197508000</time>
+ <profile></profile>
+ <replies>
+ <reply>
+ <id>reply-id</id>
+ <post-id>post-id</post-id>
+ <time>1407197508000</time>
+ </reply>
+ </replies>
+</sone>
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<sone>
+ <protocol-version>0</protocol-version>
+ <time>1407197508000</time>
+ <profile></profile>
+ <replies>
+ <reply>
+ <id>reply-id</id>
+ <post-id>post-id</post-id>
+ </reply>
+ </replies>
+</sone>
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<sone>
+ <protocol-version>0</protocol-version>
+ <time>1407197508000</time>
+ <profile></profile>
+ <posts>
+ <post>
+ <id>post-id</id>
+ <time>1407197508000</time>
+ </post>
+ </posts>
+</sone>
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<sone>
+ <protocol-version>0</protocol-version>
+ <time>1407197508000</time>
+ <profile></profile>
+ <posts>
+ <post>
+ <id>post-id</id>
+ </post>
+ </posts>
+</sone>