Store Sones in database only.
authorDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Sat, 27 Sep 2014 18:40:14 +0000 (20:40 +0200)
committerDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Sat, 27 Sep 2014 18:40:14 +0000 (20:40 +0200)
12 files changed:
src/main/java/net/pterodactylus/sone/core/Core.java
src/main/java/net/pterodactylus/sone/core/SoneDownloaderImpl.java
src/main/java/net/pterodactylus/sone/data/Sone.java
src/main/java/net/pterodactylus/sone/data/SoneImpl.java
src/main/java/net/pterodactylus/sone/data/impl/AbstractSoneBuilder.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/database/SoneBuilder.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/database/SoneBuilderFactory.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/database/SoneDatabase.java
src/main/java/net/pterodactylus/sone/database/memory/MemoryDatabase.java
src/main/java/net/pterodactylus/sone/database/memory/MemorySoneBuilder.java [new file with mode: 0644]
src/test/java/net/pterodactylus/sone/core/SoneDownloaderTest.java
src/test/java/net/pterodactylus/sone/data/impl/AbstractSoneBuilderTest.java [new file with mode: 0644]

index 9beeac4..ef05eba 100644 (file)
@@ -20,15 +20,11 @@ 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.base.Predicates.not;
 import static com.google.common.primitives.Longs.tryParse;
 import static java.lang.String.format;
 import static java.util.logging.Level.WARNING;
-import static net.pterodactylus.sone.data.Sone.LOCAL_SONE_FILTER;
 import static net.pterodactylus.sone.data.Sone.toAllAlbums;
 
-import java.net.MalformedURLException;
-import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashMap;
 import java.util.HashSet;
@@ -85,6 +81,7 @@ 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.freenet.wot.Identity;
@@ -109,7 +106,6 @@ import com.google.common.base.Optional;
 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;
@@ -117,8 +113,6 @@ import com.google.common.eventbus.Subscribe;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
-import freenet.keys.FreenetURI;
-
 /**
  * The Sone core.
  *
@@ -187,10 +181,6 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
        /* 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>();
 
@@ -317,7 +307,7 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
        public SoneRescuer getSoneRescuer(Sone sone) {
                checkNotNull(sone, "sone must not be null");
                checkArgument(sone.isLocal(), "sone must be local");
-               synchronized (sones) {
+               synchronized (soneRescuers) {
                        SoneRescuer soneRescuer = soneRescuers.get(sone);
                        if (soneRescuer == null) {
                                soneRescuer = new SoneRescuer(this, soneDownloader, sone);
@@ -341,6 +331,10 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
                }
        }
 
+       public SoneBuilder soneBuilder() {
+               return database.newSoneBuilder();
+       }
+
        /**
         * {@inheritDocs}
         */
@@ -376,24 +370,14 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
         *
         * @param id
         *            The ID of the Sone
-        * @param create
-        *            {@code true} to create a new Sone if none exists,
-        *            {@code false} to return null if none exists
         * @return The Sone with the given ID, or {@code null}
         */
-       public Sone getLocalSone(String id, boolean create) {
-               synchronized (sones) {
-                       Sone sone = sones.get(id);
-                       if ((sone == null) && create) {
-                               sone = new SoneImpl(id, true);
-                               sones.put(id, sone);
-                       }
-                       if ((sone != null) && !sone.isLocal()) {
-                               sone = new SoneImpl(id, true);
-                               sones.put(id, sone);
-                       }
-                       return sone;
+       public Sone getLocalSone(String id) {
+               Optional<Sone> sone = database.getSone(id);
+               if (sone.isPresent() && sone.get().isLocal()) {
+                       return sone.get();
                }
+               return null;
        }
 
        /**
@@ -407,22 +391,13 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
        /**
         * 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();
        }
 
        /**
@@ -707,26 +682,19 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
                        return null;
                }
                logger.info(String.format("Adding Sone from OwnIdentity: %s", ownIdentity));
-               synchronized (sones) {
-                       final Sone sone;
-                       try {
-                               sone = getLocalSone(ownIdentity.getId(), true).setIdentity(ownIdentity).setInsertUri(new FreenetURI(ownIdentity.getInsertUri())).setRequestUri(new FreenetURI(ownIdentity.getRequestUri()));
-                       } catch (MalformedURLException mue1) {
-                               logger.log(Level.SEVERE, String.format("Could not convert the Identity’s URIs to Freenet URIs: %s, %s", ownIdentity.getInsertUri(), ownIdentity.getRequestUri()), mue1);
-                               return null;
-                       }
-                       sone.setLatestEdition(Numbers.safeParseLong(ownIdentity.getProperty("Sone.LatestEdition"), (long) 0));
-                       sone.setClient(new Client("Sone", SonePlugin.VERSION.toString()));
-                       sone.setKnown(true);
-                       /* TODO - load posts ’n stuff */
-                       sones.put(ownIdentity.getId(), sone);
-                       SoneInserter soneInserter = new SoneInserter(this, eventBus, freenetInterface, ownIdentity.getId());
+               Sone sone = database.newSoneBuilder().local().from(ownIdentity).build();
+               sone.setLatestEdition(Numbers.safeParseLong(ownIdentity.getProperty("Sone.LatestEdition"), 0L));
+               sone.setClient(new Client("Sone", SonePlugin.VERSION.toString()));
+               sone.setKnown(true);
+               /* TODO - load posts ’n stuff */
+               SoneInserter soneInserter = new SoneInserter(this, eventBus, freenetInterface, ownIdentity.getId());
+               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;
        }
 
        /**
@@ -762,33 +730,32 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
                }
                final Long latestEdition = tryParse(fromNullable(
                                identity.getProperty("Sone.LatestEdition")).or("0"));
-               synchronized (sones) {
-                       final Sone sone = getRemoteSone(identity.getId(), true);
-                       if (sone.isLocal()) {
-                               return sone;
+               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.setRequestUri(SoneUri.create(identity.getRequestUri()));
+               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(latestEdition);
+                       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().isAutoFollow()) {
-                                                       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(soneDownloader.fetchSoneWithUriAction(sone));
-                       return sone;
                }
+               database.storeSone(sone);
+               soneDownloader.addSone(sone);
+               soneDownloaders.execute(soneDownloader.fetchSoneWithUriAction(sone));
+               return sone;
        }
 
        /**
@@ -983,14 +950,11 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
                        });
                        soneChangeDetector.detectChanges(sone);
                        database.storeSone(sone);
-                       synchronized (sones) {
-                               sone.setOptions(storedSone.get().getOptions());
-                               sone.setKnown(storedSone.get().isKnown());
-                               sone.setStatus((sone.getTime() == 0) ? SoneStatus.unknown : SoneStatus.idle);
-                               if (sone.isLocal()) {
-                                       touchConfiguration();
-                               }
-                               sones.put(sone.getId(), sone);
+                       sone.setOptions(storedSone.get().getOptions());
+                       sone.setKnown(storedSone.get().isKnown());
+                       sone.setStatus((sone.getTime() == 0) ? SoneStatus.unknown : SoneStatus.idle);
+                       if (sone.isLocal()) {
+                               touchConfiguration();
                        }
                }
        }
@@ -1008,15 +972,14 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
                        logger.log(Level.WARNING, String.format("Tried to delete Sone of non-own identity: %s", sone));
                        return;
                }
-               synchronized (sones) {
-                       if (!getLocalSones().contains(sone)) {
-                               logger.log(Level.WARNING, String.format("Tried to delete non-local Sone: %s", sone));
-                               return;
-                       }
-                       sones.remove(sone.getId());
-                       SoneInserter soneInserter = soneInserters.remove(sone);
-                       soneInserter.stop();
+               if (!getLocalSones().contains(sone)) {
+                       logger.log(Level.WARNING, String.format("Tried to delete non-local Sone: %s", sone));
+                       return;
                }
+               // FIXME – implement in database
+//             sones.remove(sone.getId());
+               SoneInserter soneInserter = soneInserters.remove(sone);
+               soneInserter.stop();
                webOfTrustUpdater.removeContext((OwnIdentity) sone.getIdentity(), "Sone");
                webOfTrustUpdater.removeProperty((OwnIdentity) sone.getIdentity(), "Sone.LatestEdition");
                try {
@@ -1160,12 +1123,9 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
                        for (Album album : topLevelAlbums) {
                                sone.getRootAlbum().addAlbum(album);
                        }
-                       soneInserters.get(sone).setLastInsertFingerprint(lastInsertFingerprint);
-                       for (Album album : toAllAlbums.apply(sone)) {
-                               database.storeAlbum(album);
-                               for (Image image : album.getImages()) {
-                                       database.storeImage(image);
-                               }
+                       database.storeSone(sone);
+                       synchronized (soneInserters) {
+                               soneInserters.get(sone).setLastInsertFingerprint(lastInsertFingerprint);
                        }
                }
                synchronized (knownSones) {
@@ -1173,11 +1133,9 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
                                knownSones.add(friend);
                        }
                }
-               database.storePosts(sone, posts);
                for (Post post : posts) {
                        post.setKnown(true);
                }
-               database.storePostReplies(sone, replies);
                for (PostReply reply : replies) {
                        reply.setKnown(true);
                }
@@ -1528,10 +1486,10 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
        @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());
                        }
                }
                saveConfiguration();
@@ -1893,11 +1851,10 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
        @Subscribe
        public void identityUpdated(IdentityUpdatedEvent identityUpdatedEvent) {
                Identity identity = identityUpdatedEvent.identity();
-               final Sone sone = getRemoteSone(identity.getId(), false);
+               final Sone sone = getRemoteSone(identity.getId());
                if (sone.isLocal()) {
                        return;
                }
-               sone.setIdentity(identity);
                sone.setLatestEdition(Numbers.safeParseLong(identity.getProperty("Sone.LatestEdition"), sone.getLatestEdition()));
                soneDownloader.addSone(sone);
                soneDownloaders.execute(soneDownloader.fetchSoneAction(sone));
@@ -1935,9 +1892,8 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
                for (PostReply reply : sone.get().getReplies()) {
                        eventBus.post(new PostReplyRemovedEvent(reply));
                }
-               synchronized (sones) {
-                       sones.remove(identity.getId());
-               }
+//             TODO – implement in database
+//             sones.remove(identity.getId());
                eventBus.post(new SoneRemovedEvent(sone.get()));
        }
 
index 1521eaf..28d4521 100644 (file)
@@ -45,6 +45,7 @@ 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.sone.database.SoneBuilder;
 import net.pterodactylus.util.io.Closer;
 import net.pterodactylus.util.logging.Logging;
 import net.pterodactylus.util.number.Numbers;
@@ -278,7 +279,11 @@ public class SoneDownloaderImpl extends AbstractService implements SoneDownloade
                        return null;
                }
 
-               Sone sone = new SoneImpl(originalSone.getId(), originalSone.isLocal()).setIdentity(originalSone.getIdentity());
+               SoneBuilder soneBuilder = core.soneBuilder().from(originalSone.getIdentity());
+               if (originalSone.isLocal()) {
+                       soneBuilder = soneBuilder.local();
+               }
+               Sone sone = soneBuilder.build();
 
                SimpleXML soneXml;
                try {
index c27e8cc..3b36f46 100644 (file)
@@ -28,7 +28,6 @@ import java.util.Comparator;
 import java.util.List;
 import java.util.Set;
 
-import javax.annotation.Nonnegative;
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 
index 86fe3d9..08c104f 100644 (file)
@@ -59,7 +59,7 @@ public class SoneImpl implements Sone {
        private final boolean local;
 
        /** The identity of this Sone. */
-       private Identity identity;
+       private final Identity identity;
 
        /** The URI under which the Sone is stored in Freenet. */
        private volatile FreenetURI requestUri;
@@ -110,13 +110,14 @@ public class SoneImpl implements Sone {
        /**
         * Creates a new Sone.
         *
-        * @param id
-        *              The ID of the Sone
+        * @param identity
+        *              The identity 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;
+       public SoneImpl(Identity identity, boolean local) {
+               this.id = identity.getId();
+               this.identity = identity;
                this.local = local;
        }
 
diff --git a/src/main/java/net/pterodactylus/sone/data/impl/AbstractSoneBuilder.java b/src/main/java/net/pterodactylus/sone/data/impl/AbstractSoneBuilder.java
new file mode 100644 (file)
index 0000000..a214677
--- /dev/null
@@ -0,0 +1,37 @@
+package net.pterodactylus.sone.data.impl;
+
+import static com.google.common.base.Preconditions.checkState;
+
+import net.pterodactylus.sone.database.SoneBuilder;
+import net.pterodactylus.sone.freenet.wot.Identity;
+import net.pterodactylus.sone.freenet.wot.OwnIdentity;
+
+/**
+ * Abstract {@link SoneBuilder} implementation.
+ *
+ * @author <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");
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/sone/database/SoneBuilder.java b/src/main/java/net/pterodactylus/sone/database/SoneBuilder.java
new file mode 100644 (file)
index 0000000..d2047af
--- /dev/null
@@ -0,0 +1,18 @@
+package net.pterodactylus.sone.database;
+
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.freenet.wot.Identity;
+
+/**
+ * Builder for {@link Sone} objects.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public interface SoneBuilder {
+
+       SoneBuilder from(Identity identity);
+       SoneBuilder local();
+
+       Sone build() throws IllegalStateException;
+
+}
diff --git a/src/main/java/net/pterodactylus/sone/database/SoneBuilderFactory.java b/src/main/java/net/pterodactylus/sone/database/SoneBuilderFactory.java
new file mode 100644 (file)
index 0000000..c95251f
--- /dev/null
@@ -0,0 +1,12 @@
+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();
+
+}
index 0f97103..f5c5cda 100644 (file)
@@ -6,6 +6,6 @@ package net.pterodactylus.sone.database;
  *
  * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
  */
-public interface SoneDatabase extends SoneProvider, SoneStore {
+public interface SoneDatabase extends SoneProvider, SoneBuilderFactory, SoneStore {
 
 }
index 00bb5c9..6113d26 100644 (file)
@@ -51,6 +51,7 @@ import net.pterodactylus.sone.database.ImageBuilder;
 import net.pterodactylus.sone.database.PostBuilder;
 import net.pterodactylus.sone.database.PostDatabase;
 import net.pterodactylus.sone.database.PostReplyBuilder;
+import net.pterodactylus.sone.database.SoneBuilder;
 import net.pterodactylus.sone.database.SoneProvider;
 import net.pterodactylus.util.config.Configuration;
 import net.pterodactylus.util.config.ConfigurationException;
@@ -168,6 +169,11 @@ public class MemoryDatabase extends AbstractService implements Database {
        }
 
        @Override
+       public SoneBuilder newSoneBuilder() {
+               return new MemorySoneBuilder();
+       }
+
+       @Override
        public void storeSone(Sone sone) {
                lock.writeLock().lock();
                try {
diff --git a/src/main/java/net/pterodactylus/sone/database/memory/MemorySoneBuilder.java b/src/main/java/net/pterodactylus/sone/database/memory/MemorySoneBuilder.java
new file mode 100644 (file)
index 0000000..8778fb5
--- /dev/null
@@ -0,0 +1,20 @@
+package net.pterodactylus.sone.database.memory;
+
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.data.SoneImpl;
+import net.pterodactylus.sone.data.impl.AbstractSoneBuilder;
+
+/**
+ * Memory-based {@link AbstractSoneBuilder} implementation.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class MemorySoneBuilder extends AbstractSoneBuilder {
+
+       @Override
+       public Sone build() throws IllegalStateException {
+               validate();
+               return new SoneImpl(identity, local);
+       }
+
+}
index c910233..28879bf 100644 (file)
@@ -49,6 +49,8 @@ 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 freenet.client.ClientMetadata;
@@ -78,7 +80,7 @@ public class SoneDownloaderTest {
        private final FreenetInterface freenetInterface = mock(FreenetInterface.class);
        private final SoneDownloaderImpl soneDownloader = new SoneDownloaderImpl(core, freenetInterface);
        private final FreenetURI requestUri = mock(FreenetURI.class);
-       private final Sone sone = mock(Sone.class);
+       private Sone sone = mock(Sone.class);
        private final PostBuilder postBuilder = mock(PostBuilder.class);
        private final List<Post> createdPosts = new ArrayList<Post>();
        private Post post = mock(Post.class);
@@ -96,6 +98,7 @@ public class SoneDownloaderTest {
 
        @Before
        public void setupSone() {
+               Sone sone = SoneDownloaderTest.this.sone;
                Identity identity = mock(Identity.class);
                when(identity.getId()).thenReturn("identity");
                when(sone.getId()).thenReturn("identity");
@@ -118,6 +121,16 @@ public class SoneDownloaderTest {
        }
 
        @Before
+       public void setupSoneBuilder() {
+               when(core.soneBuilder()).thenAnswer(new Answer<SoneBuilder>() {
+                       @Override
+                       public SoneBuilder answer(InvocationOnMock invocation) {
+                               return new MemorySoneBuilder();
+                       }
+               });
+       }
+
+       @Before
        public void setupPost() {
                when(post.getRecipientId()).thenReturn(Optional.<String>absent());
        }
@@ -921,7 +934,7 @@ public class SoneDownloaderTest {
        @Test
        public void exceptionWhileFetchingSoneWillNotUpdateTheCore() throws IOException {
                final Fetched fetchResult = createFetchResult(requestUri, getClass().getResourceAsStream("sone-parser-no-payload.xml"));
-               when(sone.getId()).thenThrow(NullPointerException.class);
+               when(core.soneBuilder()).thenReturn(null);
                when(freenetInterface.fetchUri(requestUri)).thenReturn(fetchResult);
                soneDownloader.fetchSoneAction(sone).run();
                verify(core, never()).updateSone(any(Sone.class));
diff --git a/src/test/java/net/pterodactylus/sone/data/impl/AbstractSoneBuilderTest.java b/src/test/java/net/pterodactylus/sone/data/impl/AbstractSoneBuilderTest.java
new file mode 100644 (file)
index 0000000..b2d86dd
--- /dev/null
@@ -0,0 +1,54 @@
+package net.pterodactylus.sone.data.impl;
+
+import static org.mockito.Mockito.mock;
+
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.freenet.wot.Identity;
+import net.pterodactylus.sone.freenet.wot.OwnIdentity;
+
+import org.junit.Test;
+
+/**
+ * Unit test for {@link AbstractSoneBuilder}.
+ *
+ * @author <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();
+       }
+
+}