<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>
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.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, 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);
+ }
+
+ }
+
}
--- /dev/null
- ObjectContainer objectContainer,
+ /*
+ * 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);
+ }
+ }
+
+ }
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 {
++ class InsertInformation implements Closeable {
- private final Set<Bucket> buckets = new HashSet<Bucket>();
+ /** 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 {
++ 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();
}
}
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);
}
}
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() {
+ }
});
}
public static final Version VERSION = new Version(0, 8, 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();
}
}
--- /dev/null
-import static freenet.client.InsertException.CANCELLED;
-import static freenet.client.InsertException.INTERNAL_ERROR;
+ package net.pterodactylus.sone.core;
+
-import net.pterodactylus.sone.freenet.StringBucket;
+ 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;
- FetchException fetchException = new FetchException(FetchException.PERMANENT_REDIRECT, newFreenetUri);
+
+ 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(FetchException.ALL_DATA_NOT_FOUND);
++ 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");
- Bucket bucket = new StringBucket("Some Data.");
++ 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");
- when(highLevelSimpleClient.insert(insertBlockCaptor.capture(), eq(false), eq((String) null), eq(false), eq(insertContext), eq(insertToken), anyShort())).thenReturn(clientPutter);
++ 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(false), eq((String) null), eq(false), eq(insertContext), eq(insertToken), anyShort())).thenThrow(InsertException.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);
- callbackCaptor.getValue().onFoundEdition(3, key, null, null, false, (short) 0, null, true, true);
++ 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);
- insertToken.onFailure(null, null, null);
++ 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() {
- InsertException insertException = new InsertException(INTERNAL_ERROR, "Internal error", null);
- insertToken.onFailure(insertException, null, null);
++ 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(CANCELLED, null);
- insertToken.onFailure(insertException, null, null);
++ 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() {
- public void ignoredMethodsDoNotThrowExceptions() {
- insertToken.onMajorProgress(null);
- insertToken.onFetchable(null, null);
- insertToken.onGeneratedMetadata(null, null, null);
++ 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
- insertToken.onGeneratedURI(generatedUri, null, null);
- insertToken.onSuccess(null, null);
++ 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
-import static org.hamcrest.Matchers.containsInAnyOrder;
+ 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 net.pterodactylus.sone.core.SoneInserter.InsertInformation;
+ 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 freenet.client.async.ManifestElement;
+ 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
-import net.pterodactylus.sone.freenet.StringBucket;
+ 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;
- Bucket fetched = new StringBucket("# MapConfigurationBackendVersion=1\n" +
+ 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");
- "CurrentVersion/ReleaseTime: 11865368297000");
++ Bucket fetched = new ArrayBucket(("# MapConfigurationBackendVersion=1\n" +
+ "CurrentVersion/Version: 99.0.0\n" +
- Bucket fetched = new StringBucket("# MapConfigurationBackendVersion=1\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");
- "CurrentVersion/ReleaseTime: 1289417883000");
++ Bucket fetched = new ArrayBucket(("# MapConfigurationBackendVersion=1\n" +
+ "CurrentVersion/Version: 0.2\n" +
- Bucket fetched = new StringBucket("Some other data.");
++ "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 StringBucket("Some other data.") {
++ 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 StringBucket("# MapConfigurationBackendVersion=1\n" +
++ 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");
- "CurrentVersion/ReleaseTime: invalid");
++ Bucket fetched = new ArrayBucket(("# MapConfigurationBackendVersion=1\n" +
+ "CurrentVersion/Version: 0.2\n" +
- Bucket fetched = new StringBucket("# 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 StringBucket("# MapConfigurationBackendVersion=1\n" +
++ 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");
- "CurrentVersion/ReleaseTime: 1289417883000");
++ Bucket fetched = new ArrayBucket(("# MapConfigurationBackendVersion=1\n" +
+ "CurrentVersion/Version: foo\n" +
++ "CurrentVersion/ReleaseTime: 1289417883000").getBytes());
+ return new FetchResult(clientMetadata, fetched);
+ }
+
+ }