From: David ‘Bombe’ Roden Date: Fri, 12 Jun 2015 18:47:55 +0000 (+0200) Subject: Merge branch 'last-working' into next X-Git-Tag: 0.9-rc1^2~3 X-Git-Url: https://git.pterodactylus.net/?p=Sone.git;a=commitdiff_plain;h=f229fe41f708d2b275c20ceb9aba5993761218a3;hp=-c Merge branch 'last-working' into next This is the current state with lots and lots of changes since the last release. I have been using this state during the last year and it should work pretty much the same as before (minus a couple of bugs). Conflicts: src/main/java/net/pterodactylus/sone/core/FreenetInterface.java src/main/java/net/pterodactylus/sone/core/SoneInserter.java src/main/java/net/pterodactylus/sone/freenet/PluginStoreConfigurationBackend.java --- f229fe41f708d2b275c20ceb9aba5993761218a3 diff --combined pom.xml index 4309f05,0dfc383..67e29c9 --- a/pom.xml +++ b/pom.xml @@@ -22,15 -22,20 +22,20 @@@ test + org.hamcrest + hamcrest-all + 1.3 + + org.freenetproject fred - 0.7.5.1405 + 0.7.5.1467.99.3 provided org.freenetproject freenet-ext - 26 + 29 provided @@@ -41,7 -46,7 +46,7 @@@ com.google.guava guava - 14.0-rc1 + 14.0.1 commons-lang @@@ -140,6 -145,48 +145,48 @@@
© 2010–2013 David ‘Bombe’ Roden
+ + org.jacoco + jacoco-maven-plugin + 0.7.1.201405082137 + + + default-prepare-agent + + prepare-agent + + + + default-report + prepare-package + + report + + + + default-check + + check + + + + + + BUNDLE + + + + COMPLEXITY + COVEREDRATIO + 0.60 + + + + + + + + diff --combined src/main/java/net/pterodactylus/sone/core/FreenetInterface.java index 4ee3926,dc0e3ea..e802ba2 --- a/src/main/java/net/pterodactylus/sone/core/FreenetInterface.java +++ b/src/main/java/net/pterodactylus/sone/core/FreenetInterface.java @@@ -17,11 -17,16 +17,16 @@@ package net.pterodactylus.sone.core; + import static freenet.keys.USK.create; + import static java.lang.String.format; + import static java.util.logging.Level.WARNING; + import static java.util.logging.Logger.getLogger; + import static net.pterodactylus.sone.freenet.Key.routingKey; + import java.net.MalformedURLException; import java.util.Collections; import java.util.HashMap; import java.util.Map; - import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; @@@ -32,17 -37,18 +37,17 @@@ import net.pterodactylus.sone.core.even 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; @@@ -58,19 -64,18 +63,20 @@@ import freenet.node.Node import freenet.node.RequestClient; import freenet.node.RequestStarter; import freenet.support.api.Bucket; +import freenet.support.api.RandomAccessBucket; import freenet.support.io.ArrayBucket; +import freenet.support.io.ResumeFailedException; /** * Contains all necessary functionality for interacting with the Freenet node. * * @author David ‘Bombe’ Roden */ + @Singleton public class FreenetInterface { /** The logger. */ - private static final Logger logger = Logging.getLogger(FreenetInterface.class); + private static final Logger logger = getLogger("Sone.FreenetInterface"); /** The event bus. */ private final EventBus eventBus; @@@ -87,18 -92,6 +93,18 @@@ /** The not-Sone-related USK callbacks. */ private final Map uriUskCallbacks = Collections.synchronizedMap(new HashMap()); + private final RequestClient imageInserts = new RequestClient() { + @Override + public boolean persistent() { + return false; + } + + @Override + public boolean realTimeFlag() { + return true; + } + }; + /** * Creates a new Freenet interface. * @@@ -126,14 -119,13 +132,13 @@@ * @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; } @@@ -144,16 -136,6 +149,6 @@@ } /** - * 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. @@@ -172,12 -154,11 +167,12 @@@ 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); @@@ -205,51 -186,30 +200,30 @@@ } } - /** - * 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); } } @@@ -285,7 -245,7 +259,7 @@@ 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); } @@@ -301,7 -261,7 +275,7 @@@ }; 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); @@@ -417,7 -377,6 +391,7 @@@ /** The client putter. */ private ClientPutter clientPutter; + private Bucket bucket; /** The final URI. */ private volatile FreenetURI resultingUri; @@@ -449,10 -408,6 +423,10 @@@ eventBus.post(new ImageInsertStartedEvent(image)); } + public void setBucket(Bucket bucket) { + this.bucket = bucket; + } + // // ACTIONS // @@@ -462,22 -417,20 +436,23 @@@ */ @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. */ } /** @@@ -485,20 -438,19 +460,20 @@@ */ @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. */ } @@@ -506,7 -458,7 +481,7 @@@ * {@inheritDoc} */ @Override - public void onGeneratedMetadata(Bucket metadata, BaseClientPutter clientPutter, ObjectContainer objectContainer) { + public void onGeneratedMetadata(Bucket metadata, BaseClientPutter clientPutter) { /* ignore, we don’t care. */ } @@@ -514,7 -466,7 +489,7 @@@ * {@inheritDoc} */ @Override - public void onGeneratedURI(FreenetURI generatedUri, BaseClientPutter clientPutter, ObjectContainer objectContainer) { + public void onGeneratedURI(FreenetURI generatedUri, BaseClientPutter clientPutter) { resultingUri = generatedUri; } @@@ -523,11 -475,19 +498,20 @@@ */ @Override @SuppressWarnings("synthetic-access") - public void onSuccess(BaseClientPutter clientPutter, ObjectContainer objectContainer) { + public void onSuccess(BaseClientPutter clientPutter) { eventBus.post(new ImageInsertFinishedEvent(image, resultingUri)); + bucket.free(); } } + public class InsertTokenSupplier implements Function { + + @Override + public InsertToken apply(Image image) { + return new InsertToken(image); + } + + } + } diff --combined src/main/java/net/pterodactylus/sone/core/SoneDownloaderImpl.java index 0000000,ff0eaf7..b36c2d3 mode 000000,100644..100644 --- a/src/main/java/net/pterodactylus/sone/core/SoneDownloaderImpl.java +++ b/src/main/java/net/pterodactylus/sone/core/SoneDownloaderImpl.java @@@ -1,0 -1,275 +1,274 @@@ + /* + * Sone - SoneDownloader.java - Copyright © 2010–2013 David Roden + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + + package net.pterodactylus.sone.core; + + import static freenet.support.io.Closer.close; + import static java.lang.String.format; + import static java.lang.System.currentTimeMillis; + import static java.util.concurrent.TimeUnit.DAYS; + import static java.util.logging.Logger.getLogger; + + import java.io.InputStream; + import java.util.HashSet; + import java.util.Set; + import java.util.logging.Level; + import java.util.logging.Logger; + + import net.pterodactylus.sone.core.FreenetInterface.Fetched; + import net.pterodactylus.sone.data.Sone; + import net.pterodactylus.sone.data.Sone.SoneStatus; + import net.pterodactylus.util.service.AbstractService; + + import freenet.client.FetchResult; + import freenet.client.async.ClientContext; + import freenet.client.async.USKCallback; + import freenet.keys.FreenetURI; + import freenet.keys.USK; + import freenet.node.RequestStarter; + import freenet.support.api.Bucket; + import freenet.support.io.Closer; + import com.db4o.ObjectContainer; + + import com.google.common.annotations.VisibleForTesting; + + /** + * The Sone downloader is responsible for download Sones as they are updated. + * + * @author David ‘Bombe’ Roden + */ + public class SoneDownloaderImpl extends AbstractService implements SoneDownloader { + + /** The logger. */ + private static final Logger logger = getLogger("Sone.Downloader"); + + /** The maximum protocol version. */ + private static final int MAX_PROTOCOL_VERSION = 0; + + /** The core. */ + private final Core core; + private final SoneParser soneParser; + + /** The Freenet interface. */ + private final FreenetInterface freenetInterface; + + /** The sones to update. */ + private final Set sones = new HashSet(); + + /** + * Creates a new Sone downloader. + * + * @param core + * The core + * @param freenetInterface + * The Freenet interface + */ + public SoneDownloaderImpl(Core core, FreenetInterface freenetInterface) { + this(core, freenetInterface, new SoneParser(core)); + } + + /** + * Creates a new Sone downloader. + * + * @param core + * The core + * @param freenetInterface + * The Freenet interface + * @param soneParser + */ + @VisibleForTesting + SoneDownloaderImpl(Core core, FreenetInterface freenetInterface, SoneParser soneParser) { + super("Sone Downloader", false); + this.core = core; + this.freenetInterface = freenetInterface; + this.soneParser = soneParser; + } + + // + // ACTIONS + // + + /** + * Adds the given Sone to the set of Sones that will be watched for updates. + * + * @param sone + * The Sone to add + */ + @Override + public void addSone(final Sone sone) { + if (!sones.add(sone)) { + freenetInterface.unregisterUsk(sone); + } + final USKCallback uskCallback = new USKCallback() { + + @Override + @SuppressWarnings("synthetic-access") + public void onFoundEdition(long edition, USK key, - ObjectContainer objectContainer, + ClientContext clientContext, boolean metadata, + short codec, byte[] data, boolean newKnownGood, + boolean newSlotToo) { + logger.log(Level.FINE, format( + "Found USK update for Sone “%s” at %s, new known good: %s, new slot too: %s.", + sone, key, newKnownGood, newSlotToo)); + if (edition > sone.getLatestEdition()) { + sone.setLatestEdition(edition); + new Thread(fetchSoneAction(sone), + "Sone Downloader").start(); + } + } + + @Override + public short getPollingPriorityProgress() { + return RequestStarter.INTERACTIVE_PRIORITY_CLASS; + } + + @Override + public short getPollingPriorityNormal() { + return RequestStarter.INTERACTIVE_PRIORITY_CLASS; + } + }; + if (soneHasBeenActiveRecently(sone)) { + freenetInterface.registerActiveUsk(sone.getRequestUri(), + uskCallback); + } else { + freenetInterface.registerPassiveUsk(sone.getRequestUri(), + uskCallback); + } + } + + private boolean soneHasBeenActiveRecently(Sone sone) { + return (currentTimeMillis() - sone.getTime()) < DAYS.toMillis(7); + } + + private void fetchSone(Sone sone) { + fetchSone(sone, sone.getRequestUri().sskForUSK()); + } + + /** + * Fetches the updated Sone. This method can be used to fetch a Sone from a + * specific URI. + * + * @param sone + * The Sone to fetch + * @param soneUri + * The URI to fetch the Sone from + */ + @Override + public void fetchSone(Sone sone, FreenetURI soneUri) { + fetchSone(sone, soneUri, false); + } + + /** + * Fetches the Sone from the given URI. + * + * @param sone + * The Sone to fetch + * @param soneUri + * The URI of the Sone to fetch + * @param fetchOnly + * {@code true} to only fetch and parse the Sone, {@code false} + * to {@link Core#updateSone(Sone) update} it in the core + * @return The downloaded Sone, or {@code null} if the Sone could not be + * downloaded + */ + @Override + public Sone fetchSone(Sone sone, FreenetURI soneUri, boolean fetchOnly) { + logger.log(Level.FINE, String.format("Starting fetch for Sone “%s” from %s…", sone, soneUri)); + FreenetURI requestUri = soneUri.setMetaString(new String[] { "sone.xml" }); + sone.setStatus(SoneStatus.downloading); + try { + Fetched fetchResults = freenetInterface.fetchUri(requestUri); + if (fetchResults == null) { + /* TODO - mark Sone as bad. */ + return null; + } + logger.log(Level.FINEST, String.format("Got %d bytes back.", fetchResults.getFetchResult().size())); + Sone parsedSone = parseSone(sone, fetchResults.getFetchResult(), fetchResults.getFreenetUri()); + if (parsedSone != null) { + if (!fetchOnly) { + parsedSone.setStatus((parsedSone.getTime() == 0) ? SoneStatus.unknown : SoneStatus.idle); + core.updateSone(parsedSone); + addSone(parsedSone); + } + } + return parsedSone; + } finally { + sone.setStatus((sone.getTime() == 0) ? SoneStatus.unknown : SoneStatus.idle); + } + } + + /** + * Parses a Sone from a fetch result. + * + * @param originalSone + * The sone to parse, or {@code null} if the Sone is yet unknown + * @param fetchResult + * The fetch result + * @param requestUri + * The requested URI + * @return The parsed Sone, or {@code null} if the Sone could not be parsed + */ + private Sone parseSone(Sone originalSone, FetchResult fetchResult, FreenetURI requestUri) { + logger.log(Level.FINEST, String.format("Parsing FetchResult (%d bytes, %s) for %s…", fetchResult.size(), fetchResult.getMimeType(), originalSone)); + Bucket soneBucket = fetchResult.asBucket(); + InputStream soneInputStream = null; + try { + soneInputStream = soneBucket.getInputStream(); + Sone parsedSone = soneParser.parseSone(originalSone, + soneInputStream); + if (parsedSone != null) { + parsedSone.setLatestEdition(requestUri.getEdition()); + } + return parsedSone; + } catch (Exception e1) { + logger.log(Level.WARNING, String.format("Could not parse Sone from %s!", requestUri), e1); + } finally { + close(soneInputStream); + close(soneBucket); + } + return null; + } + + @Override + public Runnable fetchSoneWithUriAction(final Sone sone) { + return new Runnable() { + @Override + public void run() { + fetchSone(sone, sone.getRequestUri()); + } + }; + } + + @Override + public Runnable fetchSoneAction(final Sone sone) { + return new Runnable() { + @Override + public void run() { + fetchSone(sone); + } + }; + } + + /** {@inheritDoc} */ + @Override + protected void serviceStop() { + for (Sone sone : sones) { + freenetInterface.unregisterUsk(sone); + } + } + + } diff --combined src/main/java/net/pterodactylus/sone/core/SoneInserter.java index c5b9e40,33bc240..86bf049 --- a/src/main/java/net/pterodactylus/sone/core/SoneInserter.java +++ b/src/main/java/net/pterodactylus/sone/core/SoneInserter.java @@@ -17,19 -17,24 +17,26 @@@ package net.pterodactylus.sone.core; - import static com.google.common.base.Preconditions.checkArgument; + import static java.lang.String.format; + import static java.lang.System.currentTimeMillis; + import static java.util.logging.Logger.getLogger; import static net.pterodactylus.sone.data.Album.NOT_EMPTY; ++import java.io.Closeable; + import java.io.InputStream; import java.io.InputStreamReader; import java.io.StringWriter; import java.nio.charset.Charset; import java.util.HashMap; import java.util.HashSet; import java.util.Map; +import java.util.Set; + import java.util.concurrent.atomic.AtomicInteger; import java.util.logging.Level; import java.util.logging.Logger; + import net.pterodactylus.sone.core.SoneModificationDetector.LockableFingerprintProvider; + import net.pterodactylus.sone.core.event.InsertionDelayChangedEvent; import net.pterodactylus.sone.core.event.SoneInsertAbortedEvent; import net.pterodactylus.sone.core.event.SoneInsertedEvent; import net.pterodactylus.sone.core.event.SoneInsertingEvent; @@@ -38,9 -43,9 +45,8 @@@ import net.pterodactylus.sone.data.Post import net.pterodactylus.sone.data.Reply; import net.pterodactylus.sone.data.Sone; import net.pterodactylus.sone.data.Sone.SoneStatus; -import net.pterodactylus.sone.freenet.StringBucket; import net.pterodactylus.sone.main.SonePlugin; import net.pterodactylus.util.io.Closer; - import net.pterodactylus.util.logging.Logging; import net.pterodactylus.util.service.AbstractService; import net.pterodactylus.util.template.HtmlFilter; import net.pterodactylus.util.template.ReflectionAccessor; @@@ -51,16 -56,15 +57,19 @@@ import net.pterodactylus.util.template. 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. @@@ -70,10 -74,10 +79,10 @@@ 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(); @@@ -96,14 -100,9 +105,9 @@@ /** 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. @@@ -114,32 -113,49 +118,49 @@@ * The event bus * @param freenetInterface * The freenet interface - * @param sone - * The Sone to insert + * @param soneId + * The ID of the Sone to insert */ - public SoneInserter(Core core, EventBus eventBus, FreenetInterface freenetInterface, Sone sone) { - super("Sone Inserter for “" + sone.getName() + "”", false); + public SoneInserter(final Core core, EventBus eventBus, FreenetInterface freenetInterface, final String soneId) { + this(core, eventBus, freenetInterface, soneId, new SoneModificationDetector(new LockableFingerprintProvider() { + @Override + public boolean isLocked() { + final Optional sone = core.getSone(soneId); + if (!sone.isPresent()) { + return false; + } + return core.isLocked(sone.get()); + } + + @Override + public String getFingerprint() { + final Optional sone = core.getSone(soneId); + if (!sone.isPresent()) { + return null; + } + return sone.get().getFingerprint(); + } + }, insertionDelay), 1000); + } + + @VisibleForTesting + SoneInserter(Core core, EventBus eventBus, FreenetInterface freenetInterface, String soneId, SoneModificationDetector soneModificationDetector, long delay) { + super("Sone Inserter for “" + soneId + "”", false); this.core = core; this.eventBus = eventBus; this.freenetInterface = freenetInterface; - this.sone = sone; + this.soneId = soneId; + this.soneModificationDetector = soneModificationDetector; + this.delay = delay; } // // ACCESSORS // - /** - * Sets the Sone to insert. - * - * @param sone - * The Sone to insert - * @return This Sone inserter - */ - public SoneInserter setSone(Sone sone) { - checkArgument((this.sone == null) || sone.equals(this.sone), "Sone to insert can not be set to a different Sone"); - this.sone = sone; - return this; + @VisibleForTesting + static AtomicInteger getInsertionDelay() { + return insertionDelay; } /** @@@ -149,8 -165,8 +170,8 @@@ * @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); } /** @@@ -159,7 -175,7 +180,7 @@@ * @return The fingerprint of the last insert */ public String getLastInsertFingerprint() { - return lastInsertFingerprint; + return soneModificationDetector.getOriginalFingerprint(); } /** @@@ -169,7 -185,7 +190,7 @@@ * The fingerprint of the last insert */ public void setLastInsertFingerprint(String lastInsertFingerprint) { - this.lastInsertFingerprint = lastInsertFingerprint; + soneModificationDetector.setFingerprint(lastInsertFingerprint); } /** @@@ -180,7 -196,7 +201,7 @@@ * otherwise */ public boolean isModified() { - return modified; + return soneModificationDetector.isModified(); } // @@@ -192,59 -208,28 +213,28 @@@ */ @Override protected void serviceRun() { - long lastModificationTime = 0; - String lastInsertedFingerprint = lastInsertFingerprint; - String lastFingerprint = ""; - Sone sone; while (!shouldStop()) { try { - /* check every seconds. */ - sleep(1000); - - /* don’t insert locked Sones. */ - sone = this.sone; - if (core.isLocked(sone)) { - /* trigger redetection when the Sone is unlocked. */ - synchronized (sone) { - modified = !sone.getFingerprint().equals(lastInsertedFingerprint); + /* check every second. */ + sleep(delay); + + if (soneModificationDetector.isEligibleForInsert()) { + Optional soneOptional = core.getSone(soneId); + if (!soneOptional.isPresent()) { + logger.log(Level.WARNING, format("Sone %s has disappeared, exiting inserter.", soneId)); + return; } - lastFingerprint = ""; - lastModificationTime = 0; - continue; - } - - InsertInformation insertInformation = null; - synchronized (sone) { - String fingerprint = sone.getFingerprint(); - if (!fingerprint.equals(lastFingerprint)) { - if (fingerprint.equals(lastInsertedFingerprint)) { - modified = false; - lastModificationTime = 0; - logger.log(Level.FINE, String.format("Sone %s has been reverted to last insert state.", sone)); - } else { - lastModificationTime = System.currentTimeMillis(); - modified = true; - logger.log(Level.FINE, String.format("Sone %s has been modified, waiting %d seconds before inserting.", sone.getName(), insertionDelay)); - } - lastFingerprint = fingerprint; - } - if (modified && (lastModificationTime > 0) && ((System.currentTimeMillis() - lastModificationTime) > (insertionDelay * 1000))) { - lastInsertedFingerprint = fingerprint; - insertInformation = new InsertInformation(sone); - } - } - - if (insertInformation != null) { + Sone sone = soneOptional.get(); + InsertInformation insertInformation = new InsertInformation(sone); logger.log(Level.INFO, String.format("Inserting Sone “%s”…", sone.getName())); boolean success = false; try { sone.setStatus(SoneStatus.inserting); - long insertTime = System.currentTimeMillis(); - insertInformation.setTime(insertTime); + long insertTime = currentTimeMillis(); eventBus.post(new SoneInsertingEvent(sone)); - FreenetURI finalUri = freenetInterface.insertDirectory(insertInformation.getInsertUri(), insertInformation.generateManifestEntries(), "index.html"); - eventBus.post(new SoneInsertedEvent(sone, System.currentTimeMillis() - insertTime)); + FreenetURI finalUri = freenetInterface.insertDirectory(sone.getInsertUri(), insertInformation.generateManifestEntries(), "index.html"); + eventBus.post(new SoneInsertedEvent(sone, currentTimeMillis() - insertTime, insertInformation.getFingerprint())); /* at this point we might already be stopped. */ if (shouldStop()) { /* if so, bail out, don’t change anything. */ @@@ -259,7 -244,6 +249,7 @@@ 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); } @@@ -269,12 -253,10 +259,10 @@@ */ 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; } } } @@@ -285,6 -267,11 +273,11 @@@ } } + @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 @@@ -292,11 -279,11 +285,13 @@@ * * @author David ‘Bombe’ Roden */ - private class InsertInformation { + @VisibleForTesting - class InsertInformation { ++ class InsertInformation implements Closeable { + /** All properties of the Sone, copied for thread safety. */ + private final Map soneProperties = new HashMap(); - private final Set buckets = new HashSet(); + private final String fingerprint; + private final ManifestCreator manifestCreator; /** * Creates a new insert information container. @@@ -305,40 -292,28 +300,28 @@@ * The sone to insert */ public InsertInformation(Sone sone) { + this.fingerprint = sone.getFingerprint(); + Map soneProperties = new HashMap(); soneProperties.put("id", sone.getId()); soneProperties.put("name", sone.getName()); - soneProperties.put("time", sone.getTime()); + soneProperties.put("time", currentTimeMillis()); soneProperties.put("requestUri", sone.getRequestUri()); - soneProperties.put("insertUri", sone.getInsertUri()); soneProperties.put("profile", sone.getProfile()); soneProperties.put("posts", Ordering.from(Post.TIME_COMPARATOR).sortedCopy(sone.getPosts())); soneProperties.put("replies", Ordering.from(Reply.TIME_COMPARATOR).reverse().sortedCopy(sone.getReplies())); soneProperties.put("likedPostIds", new HashSet(sone.getLikedPostIds())); soneProperties.put("likedReplyIds", new HashSet(sone.getLikedReplyIds())); soneProperties.put("albums", FluentIterable.from(sone.getRootAlbum().getAlbums()).transformAndConcat(Album.FLATTENER).filter(NOT_EMPTY).toList()); + manifestCreator = new ManifestCreator(core, soneProperties); } // // ACCESSORS // - /** - * Returns the insert URI of the Sone. - * - * @return The insert URI of the Sone - */ - public FreenetURI getInsertUri() { - return (FreenetURI) soneProperties.get("insertUri"); - } - - /** - * Sets the time of the Sone at the time of the insert. - * - * @param time - * The time of the Sone - */ - public void setTime(long time) { - soneProperties.put("time", time); + @VisibleForTesting + String getFingerprint() { + return fingerprint; } // @@@ -354,41 -329,50 +337,56 @@@ HashMap manifestEntries = new HashMap(); /* first, create an index.html. */ - manifestEntries.put("index.html", createManifestElement("index.html", "text/html; charset=utf-8", "/templates/insert/index.html")); + manifestEntries.put("index.html", manifestCreator.createManifestElement( + "index.html", "text/html; charset=utf-8", + "/templates/insert/index.html")); /* now, store the sone. */ - manifestEntries.put("sone.xml", createManifestElement("sone.xml", "text/xml; charset=utf-8", "/templates/insert/sone.xml")); + manifestEntries.put("sone.xml", manifestCreator.createManifestElement( + "sone.xml", "text/xml; charset=utf-8", + "/templates/insert/sone.xml")); return manifestEntries; } - // - // PRIVATE METHODS - // ++ @Override ++ public void close() { ++ manifestCreator.close(); ++ } + - /** - * Creates a new manifest element. - * - * @param name - * The name of the file - * @param contentType - * The content type of the file - * @param templateName - * The name of the template to render - * @return The manifest element - */ - @SuppressWarnings("synthetic-access") - private ManifestElement createManifestElement(String name, String contentType, String templateName) { + } + + /** + * Creates manifest elements for an insert by rendering a template. + * + * @author David ‘Bombe’ Roden + */ + @VisibleForTesting - static class ManifestCreator { ++ static class ManifestCreator implements Closeable { + + private final Core core; + private final Map soneProperties; ++ private final Set buckets = new HashSet(); + + ManifestCreator(Core core, Map soneProperties) { + this.core = core; + this.soneProperties = soneProperties; + } + + public ManifestElement createManifestElement(String name, String contentType, String templateName) { InputStreamReader templateInputStreamReader = null; + InputStream templateInputStream = null; Template template; try { - templateInputStreamReader = new InputStreamReader(getClass().getResourceAsStream(templateName), utf8Charset); + templateInputStream = getClass().getResourceAsStream(templateName); + templateInputStreamReader = new InputStreamReader(templateInputStream, utf8Charset); template = TemplateParser.parse(templateInputStreamReader); } catch (TemplateException te1) { logger.log(Level.SEVERE, String.format("Could not parse template “%s”!", templateName), te1); return null; } finally { Closer.close(templateInputStreamReader); + Closer.close(templateInputStream); } TemplateContext templateContext = templateContextFactory.createTemplateContext(); @@@ -397,22 -381,19 +395,22 @@@ templateContext.set("currentEdition", core.getUpdateChecker().getLatestEdition()); templateContext.set("version", SonePlugin.VERSION); StringWriter writer = new StringWriter(); - StringBucket bucket = null; try { template.render(templateContext, writer); - bucket = new StringBucket(writer.toString(), utf8Charset); + RandomAccessBucket bucket = new ArrayBucket(writer.toString().getBytes(Charsets.UTF_8)); + buckets.add(bucket); return new ManifestElement(name, bucket, contentType, bucket.size()); } catch (TemplateException te1) { logger.log(Level.SEVERE, String.format("Could not render template “%s”!", templateName), te1); return null; } finally { Closer.close(writer); - if (bucket != null) { - bucket.free(); - } + } + } + + public void close() { + for (Bucket bucket : buckets) { + bucket.free(); } } diff --combined src/main/java/net/pterodactylus/sone/freenet/PluginStoreConfigurationBackend.java index 5bde0b0,0ed6dd5..eb16a09 --- a/src/main/java/net/pterodactylus/sone/freenet/PluginStoreConfigurationBackend.java +++ b/src/main/java/net/pterodactylus/sone/freenet/PluginStoreConfigurationBackend.java @@@ -17,14 -17,15 +17,15 @@@ package net.pterodactylus.sone.freenet; + import static java.util.logging.Logger.getLogger; + import java.util.logging.Logger; import net.pterodactylus.util.config.AttributeNotFoundException; import net.pterodactylus.util.config.Configuration; import net.pterodactylus.util.config.ConfigurationException; import net.pterodactylus.util.config.ExtendedConfigurationBackend; - import net.pterodactylus.util.logging.Logging; -import freenet.client.async.DatabaseDisabledException; +import freenet.client.async.PersistenceDisabledException; import freenet.pluginmanager.PluginRespirator; import freenet.pluginmanager.PluginStore; @@@ -37,7 -38,7 +38,7 @@@ public class PluginStoreConfigurationBa /** The logger. */ @SuppressWarnings("unused") - private static final Logger logger = Logging.getLogger(PluginStoreConfigurationBackend.class); + private static final Logger logger = getLogger("Sone.Fred"); /** The plugin respirator. */ private final PluginRespirator pluginRespirator; @@@ -50,12 -51,15 +51,12 @@@ * * @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(); - } } /** @@@ -173,8 -177,8 +174,8 @@@ public void save() throws ConfigurationException { try { pluginRespirator.putStore(pluginStore); - } catch (DatabaseDisabledException dde1) { - throw new ConfigurationException("Could not store plugin store, database is disabled.", dde1); + } catch (PersistenceDisabledException pde1) { + throw new ConfigurationException("Could not store plugin store, persistence is disabled.", pde1); } } diff --combined src/main/java/net/pterodactylus/sone/main/SonePlugin.java index 1bb7b5b,f6fc60a..2970269 --- a/src/main/java/net/pterodactylus/sone/main/SonePlugin.java +++ b/src/main/java/net/pterodactylus/sone/main/SonePlugin.java @@@ -17,7 -17,11 +17,11 @@@ package net.pterodactylus.sone.main; + import static com.google.common.base.Optional.of; + import static java.util.logging.Logger.getLogger; + import java.io.File; + import java.util.logging.Handler; import java.util.logging.Level; import java.util.logging.LogRecord; import java.util.logging.Logger; @@@ -25,6 -29,7 +29,7 @@@ import net.pterodactylus.sone.core.Core; import net.pterodactylus.sone.core.FreenetInterface; import net.pterodactylus.sone.core.WebOfTrustUpdater; + import net.pterodactylus.sone.core.WebOfTrustUpdaterImpl; import net.pterodactylus.sone.database.Database; import net.pterodactylus.sone.database.PostBuilderFactory; import net.pterodactylus.sone.database.PostProvider; @@@ -34,16 -39,17 +39,17 @@@ import net.pterodactylus.sone.database. import net.pterodactylus.sone.fcp.FcpInterface; import net.pterodactylus.sone.freenet.PluginStoreConfigurationBackend; import net.pterodactylus.sone.freenet.plugin.PluginConnector; + import net.pterodactylus.sone.freenet.wot.Context; import net.pterodactylus.sone.freenet.wot.IdentityManager; + import net.pterodactylus.sone.freenet.wot.IdentityManagerImpl; import net.pterodactylus.sone.freenet.wot.WebOfTrustConnector; import net.pterodactylus.sone.web.WebInterface; import net.pterodactylus.util.config.Configuration; import net.pterodactylus.util.config.ConfigurationException; import net.pterodactylus.util.config.MapConfigurationBackend; - import net.pterodactylus.util.logging.Logging; - import net.pterodactylus.util.logging.LoggingListener; import net.pterodactylus.util.version.Version; + import com.google.common.base.Optional; import com.google.common.eventbus.EventBus; import com.google.inject.AbstractModule; import com.google.inject.Guice; @@@ -51,12 -57,11 +57,11 @@@ import com.google.inject.Injector import com.google.inject.Singleton; import com.google.inject.TypeLiteral; import com.google.inject.matcher.Matchers; - import com.google.inject.name.Names; import com.google.inject.spi.InjectionListener; import com.google.inject.spi.TypeEncounter; import com.google.inject.spi.TypeListener; -import freenet.client.async.DatabaseDisabledException; +import freenet.client.async.PersistenceDisabledException; import freenet.l10n.BaseL10n.LANGUAGE; import freenet.l10n.PluginL10n; import freenet.node.Node; @@@ -81,25 -86,32 +86,32 @@@ public class SonePlugin implements Fred 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() { + } }); } @@@ -107,7 -119,7 +119,7 @@@ 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; @@@ -189,13 -201,19 +201,19 @@@ try { oldConfiguration = new Configuration(new PluginStoreConfigurationBackend(pluginRespirator)); logger.log(Level.INFO, "Plugin store loaded."); - } catch (DatabaseDisabledException dde1) { + } catch (PersistenceDisabledException pde1) { logger.log(Level.SEVERE, "Could not load any configuration, using empty configuration!"); oldConfiguration = new Configuration(new MapConfigurationBackend()); } } - final Configuration startConfiguration = oldConfiguration; + final Configuration startConfiguration; + if ((newConfiguration != null) && (oldConfiguration != newConfiguration)) { + logger.log(Level.INFO, "Setting configuration to file-based configuration."); + startConfiguration = newConfiguration; + } else { + startConfiguration = oldConfiguration; + } final EventBus eventBus = new EventBus(); /* Freenet injector configuration. */ @@@ -213,23 -231,12 +231,12 @@@ @Override protected void configure() { - bind(Core.class).in(Singleton.class); - bind(MemoryDatabase.class).in(Singleton.class); bind(EventBus.class).toInstance(eventBus); bind(Configuration.class).toInstance(startConfiguration); - bind(FreenetInterface.class).in(Singleton.class); - bind(PluginConnector.class).in(Singleton.class); - bind(WebOfTrustConnector.class).in(Singleton.class); - bind(WebOfTrustUpdater.class).in(Singleton.class); - bind(IdentityManager.class).in(Singleton.class); - bind(String.class).annotatedWith(Names.named("WebOfTrustContext")).toInstance("Sone"); + Context context = new Context("Sone"); + bind(Context.class).toInstance(context); + bind(getOptionalContextTypeLiteral()).toInstance(of(context)); bind(SonePlugin.class).toInstance(SonePlugin.this); - bind(FcpInterface.class).in(Singleton.class); - bind(Database.class).to(MemoryDatabase.class); - bind(PostBuilderFactory.class).to(MemoryDatabase.class); - bind(PostReplyBuilderFactory.class).to(MemoryDatabase.class); - bind(SoneProvider.class).to(Core.class).in(Singleton.class); - bind(PostProvider.class).to(MemoryDatabase.class); bindListener(Matchers.any(), new TypeListener() { @Override @@@ -245,6 -252,11 +252,11 @@@ }); } + private TypeLiteral> getOptionalContextTypeLiteral() { + return new TypeLiteral>() { + }; + } + }; Injector injector = Guice.createInjector(freenetModule, soneModule); core = injector.getInstance(Core.class); @@@ -254,34 -266,15 +266,15 @@@ /* create FCP interface. */ fcpInterface = injector.getInstance(FcpInterface.class); - core.setFcpInterface(fcpInterface); /* create the web interface. */ webInterface = injector.getInstance(WebInterface.class); - boolean startupFailed = true; - try { - - /* start core! */ - core.start(); - if ((newConfiguration != null) && (oldConfiguration != newConfiguration)) { - logger.log(Level.INFO, "Setting configuration to file-based configuration."); - core.setConfiguration(newConfiguration); - } - webInterface.start(); - webInterface.setFirstStart(firstStart); - webInterface.setNewConfig(newConfig); - startupFailed = false; - } finally { - if (startupFailed) { - /* - * we let the exception bubble up but shut the logging down so - * that the logfile is not swamped by the installed logging - * handlers of the failed instances. - */ - Logging.shutdown(); - } - } + /* start core! */ + core.start(); + webInterface.start(); + webInterface.setFirstStart(firstStart); + webInterface.setNewConfig(newConfig); } /** @@@ -300,9 -293,6 +293,6 @@@ webOfTrustConnector.stop(); } catch (Throwable t1) { logger.log(Level.SEVERE, "Error while shutting down!", t1); - } finally { - /* shutdown logger. */ - Logging.shutdown(); } } diff --combined src/test/java/net/pterodactylus/sone/core/FreenetInterfaceTest.java index 0000000,c753740..091c93f mode 000000,100644..100644 --- a/src/test/java/net/pterodactylus/sone/core/FreenetInterfaceTest.java +++ b/src/test/java/net/pterodactylus/sone/core/FreenetInterfaceTest.java @@@ -1,0 -1,393 +1,401 @@@ + package net.pterodactylus.sone.core; + -import static freenet.client.InsertException.CANCELLED; -import static freenet.client.InsertException.INTERNAL_ERROR; + import static freenet.keys.InsertableClientSSK.createRandom; + import static freenet.node.RequestStarter.INTERACTIVE_PRIORITY_CLASS; + import static freenet.node.RequestStarter.PREFETCH_PRIORITY_CLASS; + import static net.pterodactylus.sone.Matchers.delivers; + import static net.pterodactylus.sone.TestUtil.setFinalField; + import static org.hamcrest.MatcherAssert.assertThat; + import static org.hamcrest.Matchers.is; + import static org.hamcrest.Matchers.notNullValue; + import static org.hamcrest.Matchers.nullValue; + import static org.mockito.ArgumentCaptor.forClass; + import static org.mockito.Matchers.any; + import static org.mockito.Matchers.anyBoolean; + import static org.mockito.Matchers.anyShort; + import static org.mockito.Matchers.eq; + import static org.mockito.Mockito.doNothing; + import static org.mockito.Mockito.mock; + import static org.mockito.Mockito.never; + import static org.mockito.Mockito.times; + import static org.mockito.Mockito.verify; + import static org.mockito.Mockito.when; + import static org.mockito.Mockito.withSettings; + + import java.io.IOException; + import java.net.MalformedURLException; + import java.util.HashMap; + + import net.pterodactylus.sone.TestUtil; + import net.pterodactylus.sone.core.FreenetInterface.Callback; + import net.pterodactylus.sone.core.FreenetInterface.Fetched; + import net.pterodactylus.sone.core.FreenetInterface.InsertToken; + import net.pterodactylus.sone.core.FreenetInterface.InsertTokenSupplier; + import net.pterodactylus.sone.core.event.ImageInsertAbortedEvent; + import net.pterodactylus.sone.core.event.ImageInsertFailedEvent; + import net.pterodactylus.sone.core.event.ImageInsertFinishedEvent; + import net.pterodactylus.sone.core.event.ImageInsertStartedEvent; + import net.pterodactylus.sone.data.Image; + import net.pterodactylus.sone.data.impl.ImageImpl; + import net.pterodactylus.sone.data.Sone; + import net.pterodactylus.sone.data.TemporaryImage; -import net.pterodactylus.sone.freenet.StringBucket; + + import freenet.client.ClientMetadata; + import freenet.client.FetchException; ++import freenet.client.FetchException.FetchExceptionMode; + import freenet.client.FetchResult; + import freenet.client.HighLevelSimpleClient; + import freenet.client.InsertBlock; + import freenet.client.InsertContext; + import freenet.client.InsertException; ++import freenet.client.InsertException.InsertExceptionMode; + import freenet.client.async.ClientPutter; + import freenet.client.async.USKCallback; + import freenet.client.async.USKManager; + import freenet.crypt.DummyRandomSource; + import freenet.crypt.RandomSource; + import freenet.keys.FreenetURI; + import freenet.keys.InsertableClientSSK; + import freenet.keys.USK; + import freenet.node.Node; + import freenet.node.NodeClientCore; + import freenet.node.RequestClient; + import freenet.support.Base64; + import freenet.support.api.Bucket; ++import freenet.support.io.ArrayBucket; ++import freenet.support.io.ResumeFailedException; + + import com.google.common.eventbus.EventBus; + import org.junit.Before; + import org.junit.Test; + import org.mockito.ArgumentCaptor; + + /** + * Unit test for {@link FreenetInterface}. + * + * @author David ‘Bombe’ Roden + */ + public class FreenetInterfaceTest { + + private final EventBus eventBus = mock(EventBus.class); + private final Node node = mock(Node.class); + private final NodeClientCore nodeClientCore = mock(NodeClientCore.class); + private final HighLevelSimpleClient highLevelSimpleClient = mock(HighLevelSimpleClient.class, withSettings().extraInterfaces(RequestClient.class)); + private final RandomSource randomSource = new DummyRandomSource(); + private final USKManager uskManager = mock(USKManager.class); + private FreenetInterface freenetInterface; + private final Sone sone = mock(Sone.class); + private final ArgumentCaptor callbackCaptor = forClass(USKCallback.class); + private final Image image = mock(Image.class); + private InsertToken insertToken; ++ private final Bucket bucket = mock(Bucket.class); + + @Before + public void setupFreenetInterface() { + when(nodeClientCore.makeClient(anyShort(), anyBoolean(), anyBoolean())).thenReturn(highLevelSimpleClient); + setFinalField(node, "clientCore", nodeClientCore); + setFinalField(node, "random", randomSource); + setFinalField(nodeClientCore, "uskManager", uskManager); + freenetInterface = new FreenetInterface(eventBus, node); + insertToken = freenetInterface.new InsertToken(image); ++ insertToken.setBucket(bucket); + } + + @Before + public void setupSone() { + InsertableClientSSK insertSsk = createRandom(randomSource, "test-0"); + when(sone.getId()).thenReturn(Base64.encode(insertSsk.getURI().getRoutingKey())); + when(sone.getRequestUri()).thenReturn(insertSsk.getURI().uskForSSK()); + } + + @Before + public void setupCallbackCaptorAndUskManager() { + doNothing().when(uskManager).subscribe(any(USK.class), callbackCaptor.capture(), anyBoolean(), any(RequestClient.class)); + } + + @Test + public void canFetchUri() throws MalformedURLException, FetchException { + FreenetURI freenetUri = new FreenetURI("KSK@GPLv3.txt"); + FetchResult fetchResult = createFetchResult(); + when(highLevelSimpleClient.fetch(freenetUri)).thenReturn(fetchResult); + Fetched fetched = freenetInterface.fetchUri(freenetUri); + assertThat(fetched, notNullValue()); + assertThat(fetched.getFetchResult(), is(fetchResult)); + assertThat(fetched.getFreenetUri(), is(freenetUri)); + } + + @Test + public void fetchFollowsRedirect() throws MalformedURLException, FetchException { + FreenetURI freenetUri = new FreenetURI("KSK@GPLv2.txt"); + FreenetURI newFreenetUri = new FreenetURI("KSK@GPLv3.txt"); + FetchResult fetchResult = createFetchResult(); - FetchException fetchException = new FetchException(FetchException.PERMANENT_REDIRECT, newFreenetUri); ++ FetchException fetchException = new FetchException(FetchExceptionMode.PERMANENT_REDIRECT, newFreenetUri); + when(highLevelSimpleClient.fetch(freenetUri)).thenThrow(fetchException); + when(highLevelSimpleClient.fetch(newFreenetUri)).thenReturn(fetchResult); + Fetched fetched = freenetInterface.fetchUri(freenetUri); + assertThat(fetched.getFetchResult(), is(fetchResult)); + assertThat(fetched.getFreenetUri(), is(newFreenetUri)); + } + + @Test + public void fetchReturnsNullOnFetchExceptions() throws MalformedURLException, FetchException { + FreenetURI freenetUri = new FreenetURI("KSK@GPLv2.txt"); - FetchException fetchException = new FetchException(FetchException.ALL_DATA_NOT_FOUND); ++ FetchException fetchException = new FetchException(FetchExceptionMode.ALL_DATA_NOT_FOUND); + when(highLevelSimpleClient.fetch(freenetUri)).thenThrow(fetchException); + Fetched fetched = freenetInterface.fetchUri(freenetUri); + assertThat(fetched, nullValue()); + } + + private FetchResult createFetchResult() { + ClientMetadata clientMetadata = new ClientMetadata("text/plain"); - Bucket bucket = new StringBucket("Some Data."); ++ Bucket bucket = new ArrayBucket("Some Data.".getBytes()); + return new FetchResult(clientMetadata, bucket); + } + + @Test + public void insertingAnImage() throws SoneException, InsertException, IOException { + TemporaryImage temporaryImage = new TemporaryImage("image-id"); + temporaryImage.setMimeType("image/png"); + byte[] imageData = new byte[] { 1, 2, 3, 4 }; + temporaryImage.setImageData(imageData); + Image image = new ImageImpl("image-id"); + InsertToken insertToken = freenetInterface.new InsertToken(image); + InsertContext insertContext = mock(InsertContext.class); + when(highLevelSimpleClient.getInsertContext(anyBoolean())).thenReturn(insertContext); + ClientPutter clientPutter = mock(ClientPutter.class); + ArgumentCaptor insertBlockCaptor = forClass(InsertBlock.class); - when(highLevelSimpleClient.insert(insertBlockCaptor.capture(), eq(false), eq((String) null), eq(false), eq(insertContext), eq(insertToken), anyShort())).thenReturn(clientPutter); ++ when(highLevelSimpleClient.insert(insertBlockCaptor.capture(), eq((String) null), eq(false), eq(insertContext), eq(insertToken), anyShort())).thenReturn(clientPutter); + freenetInterface.insertImage(temporaryImage, image, insertToken); + assertThat(insertBlockCaptor.getValue().getData().getInputStream(), delivers(new byte[] { 1, 2, 3, 4 })); + assertThat(TestUtil.getPrivateField(insertToken, "clientPutter"), is(clientPutter)); + verify(eventBus).post(any(ImageInsertStartedEvent.class)); + } + + @Test(expected = SoneInsertException.class) + public void insertExceptionCausesASoneException() throws InsertException, SoneException, IOException { + TemporaryImage temporaryImage = new TemporaryImage("image-id"); + temporaryImage.setMimeType("image/png"); + byte[] imageData = new byte[] { 1, 2, 3, 4 }; + temporaryImage.setImageData(imageData); + Image image = new ImageImpl("image-id"); + InsertToken insertToken = freenetInterface.new InsertToken(image); + InsertContext insertContext = mock(InsertContext.class); + when(highLevelSimpleClient.getInsertContext(anyBoolean())).thenReturn(insertContext); + ArgumentCaptor insertBlockCaptor = forClass(InsertBlock.class); - when(highLevelSimpleClient.insert(insertBlockCaptor.capture(), eq(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())).thenThrow(InsertException.class); + freenetInterface.insertImage(temporaryImage, image, insertToken); + } + + @Test + public void insertingADirectory() throws InsertException, SoneException { + FreenetURI freenetUri = mock(FreenetURI.class); + HashMap manifestEntries = new HashMap(); + String defaultFile = "index.html"; + FreenetURI resultingUri = mock(FreenetURI.class); + when(highLevelSimpleClient.insertManifest(eq(freenetUri), eq(manifestEntries), eq(defaultFile))).thenReturn(resultingUri); + assertThat(freenetInterface.insertDirectory(freenetUri, manifestEntries, defaultFile), is(resultingUri)); + } + + @Test(expected = SoneException.class) + public void insertExceptionIsForwardedAsSoneException() throws InsertException, SoneException { + when(highLevelSimpleClient.insertManifest(any(FreenetURI.class), any(HashMap.class), any(String.class))).thenThrow(InsertException.class); + freenetInterface.insertDirectory(null, null, null); + } + + @Test + public void soneWithWrongRequestUriWillNotBeSubscribed() throws MalformedURLException { + when(sone.getRequestUri()).thenReturn(new FreenetURI("KSK@GPLv3.txt")); + freenetInterface.registerUsk(new FreenetURI("KSK@GPLv3.txt"), null); + verify(uskManager, never()).subscribe(any(USK.class), any(USKCallback.class), anyBoolean(), any(RequestClient.class)); + } + + @Test + public void registeringAUsk() { + FreenetURI freenetUri = createRandom(randomSource, "test-0").getURI().uskForSSK(); + Callback callback = mock(Callback.class); + freenetInterface.registerUsk(freenetUri, callback); + verify(uskManager).subscribe(any(USK.class), any(USKCallback.class), anyBoolean(), eq((RequestClient) highLevelSimpleClient)); + } + + @Test + public void registeringANonUskKeyWillNotBeSubscribed() throws MalformedURLException { + FreenetURI freenetUri = new FreenetURI("KSK@GPLv3.txt"); + Callback callback = mock(Callback.class); + freenetInterface.registerUsk(freenetUri, callback); + verify(uskManager, never()).subscribe(any(USK.class), any(USKCallback.class), anyBoolean(), eq((RequestClient) highLevelSimpleClient)); + } + + @Test + public void registeringAnActiveUskWillSubscribeToItCorrectly() { + FreenetURI freenetUri = createRandom(randomSource, "test-0").getURI().uskForSSK(); + final USKCallback uskCallback = mock(USKCallback.class); + freenetInterface.registerActiveUsk(freenetUri, uskCallback); + verify(uskManager).subscribe(any(USK.class), eq(uskCallback), eq(true), any(RequestClient.class)); + } + + @Test + public void registeringAnInactiveUskWillSubscribeToItCorrectly() { + FreenetURI freenetUri = createRandom(randomSource, "test-0").getURI().uskForSSK(); + final USKCallback uskCallback = mock(USKCallback.class); + freenetInterface.registerPassiveUsk(freenetUri, uskCallback); + verify(uskManager).subscribe(any(USK.class), eq(uskCallback), eq(false), any(RequestClient.class)); + } + + @Test + public void registeringAnActiveNonUskWillNotSubscribeToAUsk() + throws MalformedURLException { + FreenetURI freenetUri = createRandom(randomSource, "test-0").getURI(); + freenetInterface.registerActiveUsk(freenetUri, null); + verify(uskManager, never()).subscribe(any(USK.class), + any(USKCallback.class), anyBoolean(), + eq((RequestClient) highLevelSimpleClient)); + } + + @Test + public void registeringAnInactiveNonUskWillNotSubscribeToAUsk() + throws MalformedURLException { + FreenetURI freenetUri = createRandom(randomSource, "test-0").getURI(); + freenetInterface.registerPassiveUsk(freenetUri, null); + verify(uskManager, never()).subscribe(any(USK.class), + any(USKCallback.class), anyBoolean(), + eq((RequestClient) highLevelSimpleClient)); + } + + @Test + public void unregisteringANotRegisteredUskDoesNothing() { + FreenetURI freenetURI = createRandom(randomSource, "test-0").getURI().uskForSSK(); + freenetInterface.unregisterUsk(freenetURI); + verify(uskManager, never()).unsubscribe(any(USK.class), any(USKCallback.class)); + } + + @Test + public void unregisteringARegisteredUsk() { + FreenetURI freenetURI = createRandom(randomSource, "test-0").getURI().uskForSSK(); + Callback callback = mock(Callback.class); + freenetInterface.registerUsk(freenetURI, callback); + freenetInterface.unregisterUsk(freenetURI); + verify(uskManager).unsubscribe(any(USK.class), any(USKCallback.class)); + } + + @Test + public void unregisteringANotRegisteredSoneDoesNothing() { + freenetInterface.unregisterUsk(sone); + verify(uskManager, never()).unsubscribe(any(USK.class), any(USKCallback.class)); + } + + @Test + public void unregisteringARegisteredSoneUnregistersTheSone() + throws MalformedURLException { + freenetInterface.registerActiveUsk(sone.getRequestUri(), mock(USKCallback.class)); + freenetInterface.unregisterUsk(sone); + verify(uskManager).unsubscribe(any(USK.class), any(USKCallback.class)); + } + + @Test + public void unregisteringASoneWithAWrongRequestKeyWillNotUnsubscribe() throws MalformedURLException { + when(sone.getRequestUri()).thenReturn(new FreenetURI("KSK@GPLv3.txt")); + freenetInterface.registerUsk(sone.getRequestUri(), null); + freenetInterface.unregisterUsk(sone); + verify(uskManager, never()).unsubscribe(any(USK.class), any(USKCallback.class)); + } + + @Test + public void callbackForNormalUskUsesDifferentPriorities() { + Callback callback = mock(Callback.class); + FreenetURI soneUri = createRandom(randomSource, "test-0").getURI().uskForSSK(); + freenetInterface.registerUsk(soneUri, callback); + assertThat(callbackCaptor.getValue().getPollingPriorityNormal(), is(PREFETCH_PRIORITY_CLASS)); + assertThat(callbackCaptor.getValue().getPollingPriorityProgress(), is(INTERACTIVE_PRIORITY_CLASS)); + } + + @Test + public void callbackForNormalUskForwardsImportantParameters() throws MalformedURLException { + Callback callback = mock(Callback.class); + FreenetURI uri = createRandom(randomSource, "test-0").getURI().uskForSSK(); + freenetInterface.registerUsk(uri, callback); + USK key = mock(USK.class); + when(key.getURI()).thenReturn(uri); - callbackCaptor.getValue().onFoundEdition(3, key, null, null, false, (short) 0, null, true, true); ++ callbackCaptor.getValue().onFoundEdition(3, key, null, false, (short) 0, null, true, true); + verify(callback).editionFound(eq(uri), eq(3L), eq(true), eq(true)); + } + + @Test + public void fetchedRetainsUriAndFetchResult() { + FreenetURI freenetUri = mock(FreenetURI.class); + FetchResult fetchResult = mock(FetchResult.class); + Fetched fetched = new Fetched(freenetUri, fetchResult); + assertThat(fetched.getFreenetUri(), is(freenetUri)); + assertThat(fetched.getFetchResult(), is(fetchResult)); + } + + @Test + public void cancellingAnInsertWillFireImageInsertAbortedEvent() { + ClientPutter clientPutter = mock(ClientPutter.class); + insertToken.setClientPutter(clientPutter); + ArgumentCaptor imageInsertStartedEvent = forClass(ImageInsertStartedEvent.class); + verify(eventBus).post(imageInsertStartedEvent.capture()); + assertThat(imageInsertStartedEvent.getValue().image(), is(image)); + insertToken.cancel(); + ArgumentCaptor imageInsertAbortedEvent = forClass(ImageInsertAbortedEvent.class); + verify(eventBus, times(2)).post(imageInsertAbortedEvent.capture()); ++ verify(bucket).free(); + assertThat(imageInsertAbortedEvent.getValue().image(), is(image)); + } + + @Test + public void failureWithoutExceptionSendsFailedEvent() { - insertToken.onFailure(null, null, null); ++ insertToken.onFailure(null, null); + ArgumentCaptor imageInsertFailedEvent = forClass(ImageInsertFailedEvent.class); + verify(eventBus).post(imageInsertFailedEvent.capture()); ++ verify(bucket).free(); + assertThat(imageInsertFailedEvent.getValue().image(), is(image)); + assertThat(imageInsertFailedEvent.getValue().cause(), nullValue()); + } + + @Test + public void failureSendsFailedEventWithException() { - InsertException insertException = new InsertException(INTERNAL_ERROR, "Internal error", null); - insertToken.onFailure(insertException, null, null); ++ InsertException insertException = new InsertException(InsertExceptionMode.INTERNAL_ERROR, "Internal error", null); ++ insertToken.onFailure(insertException, null); + ArgumentCaptor imageInsertFailedEvent = forClass(ImageInsertFailedEvent.class); + verify(eventBus).post(imageInsertFailedEvent.capture()); ++ verify(bucket).free(); + assertThat(imageInsertFailedEvent.getValue().image(), is(image)); + assertThat(imageInsertFailedEvent.getValue().cause(), is((Throwable) insertException)); + } + + @Test + public void failureBecauseCancelledByUserSendsAbortedEvent() { - InsertException insertException = new InsertException(CANCELLED, null); - insertToken.onFailure(insertException, null, null); ++ InsertException insertException = new InsertException(InsertExceptionMode.CANCELLED, null); ++ insertToken.onFailure(insertException, null); + ArgumentCaptor imageInsertAbortedEvent = forClass(ImageInsertAbortedEvent.class); + verify(eventBus).post(imageInsertAbortedEvent.capture()); ++ verify(bucket).free(); + assertThat(imageInsertAbortedEvent.getValue().image(), is(image)); + } + + @Test - public void ignoredMethodsDoNotThrowExceptions() { - insertToken.onMajorProgress(null); - insertToken.onFetchable(null, null); - insertToken.onGeneratedMetadata(null, 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, null); - insertToken.onSuccess(null, null); ++ insertToken.onGeneratedURI(generatedUri, null); ++ insertToken.onSuccess(null); + ArgumentCaptor imageInsertFinishedEvent = forClass(ImageInsertFinishedEvent.class); + verify(eventBus).post(imageInsertFinishedEvent.capture()); ++ verify(bucket).free(); + assertThat(imageInsertFinishedEvent.getValue().image(), is(image)); + assertThat(imageInsertFinishedEvent.getValue().resultingUri(), is(generatedUri)); + } + + @Test + public void insertTokenSupplierSuppliesInsertTokens() { + InsertTokenSupplier insertTokenSupplier = freenetInterface.new InsertTokenSupplier(); + assertThat(insertTokenSupplier.apply(image), notNullValue()); + } + + } diff --combined src/test/java/net/pterodactylus/sone/core/SoneInserterTest.java index 0000000,e0ff3a5..552746e mode 000000,100644..100644 --- a/src/test/java/net/pterodactylus/sone/core/SoneInserterTest.java +++ b/src/test/java/net/pterodactylus/sone/core/SoneInserterTest.java @@@ -1,0 -1,291 +1,289 @@@ + package net.pterodactylus.sone.core; + + import static com.google.common.base.Optional.of; + import static com.google.common.io.ByteStreams.toByteArray; + import static com.google.common.util.concurrent.MoreExecutors.sameThreadExecutor; + import static java.lang.System.currentTimeMillis; + import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.containsInAnyOrder; + import static org.hamcrest.Matchers.containsString; + import static org.hamcrest.Matchers.instanceOf; + import static org.hamcrest.Matchers.is; + import static org.hamcrest.Matchers.nullValue; + import static org.mockito.Matchers.any; + import static org.mockito.Matchers.anyString; + import static org.mockito.Matchers.argThat; + import static org.mockito.Matchers.eq; + import static org.mockito.Mockito.doAnswer; + import static org.mockito.Mockito.mock; + import static org.mockito.Mockito.never; + import static org.mockito.Mockito.times; + import static org.mockito.Mockito.verify; + import static org.mockito.Mockito.when; + + import java.io.IOException; + import java.util.HashMap; + import java.util.Map; + -import net.pterodactylus.sone.core.SoneInserter.InsertInformation; + 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.client.async.ManifestElement; + import freenet.keys.FreenetURI; ++import freenet.support.api.ManifestElement; + + import com.google.common.base.Charsets; + import com.google.common.base.Optional; + import com.google.common.eventbus.AsyncEventBus; + import com.google.common.eventbus.EventBus; + import org.junit.Before; + import org.junit.Test; + import org.mockito.ArgumentCaptor; + import org.mockito.invocation.InvocationOnMock; + import org.mockito.stubbing.Answer; + + /** + * Unit test for {@link SoneInserter} and its subclasses. + * + * @author David ‘Bombe’ Roden + */ + public class SoneInserterTest { + + private final Core core = mock(Core.class); + private final EventBus eventBus = mock(EventBus.class); + private final FreenetInterface freenetInterface = mock(FreenetInterface.class); + + @Before + public void setupCore() { + UpdateChecker updateChecker = mock(UpdateChecker.class); + when(core.getUpdateChecker()).thenReturn(updateChecker); + when(core.getSone(anyString())).thenReturn(Optional.absent()); + } + + @Test + public void insertionDelayIsForwardedToSoneInserter() { + EventBus eventBus = new AsyncEventBus(sameThreadExecutor()); + eventBus.register(new SoneInserter(core, eventBus, freenetInterface, "SoneId")); + eventBus.post(new InsertionDelayChangedEvent(15)); + assertThat(SoneInserter.getInsertionDelay().get(), is(15)); + } + + private Sone createSone(FreenetURI insertUri, String fingerprint) { + Sone sone = mock(Sone.class); + when(sone.getInsertUri()).thenReturn(insertUri); + when(sone.getFingerprint()).thenReturn(fingerprint); + when(sone.getRootAlbum()).thenReturn(mock(Album.class)); + when(core.getSone(anyString())).thenReturn(of(sone)); + return sone; + } + + @Test + public void isModifiedIsTrueIfModificationDetectorSaysSo() { + SoneModificationDetector soneModificationDetector = mock(SoneModificationDetector.class); + when(soneModificationDetector.isModified()).thenReturn(true); + SoneInserter soneInserter = new SoneInserter(core, eventBus, freenetInterface, "SoneId", soneModificationDetector, 1); + assertThat(soneInserter.isModified(), is(true)); + } + + @Test + public void isModifiedIsFalseIfModificationDetectorSaysSo() { + SoneModificationDetector soneModificationDetector = mock(SoneModificationDetector.class); + SoneInserter soneInserter = new SoneInserter(core, eventBus, freenetInterface, "SoneId", soneModificationDetector, 1); + assertThat(soneInserter.isModified(), is(false)); + } + + @Test + public void lastFingerprintIsStoredCorrectly() { + SoneInserter soneInserter = new SoneInserter(core, eventBus, freenetInterface, "SoneId"); + soneInserter.setLastInsertFingerprint("last-fingerprint"); + assertThat(soneInserter.getLastInsertFingerprint(), is("last-fingerprint")); + } + + @Test + public void soneInserterStopsWhenItShould() { + SoneInserter soneInserter = new SoneInserter(core, eventBus, freenetInterface, "SoneId"); + soneInserter.stop(); + soneInserter.serviceRun(); + } + + @Test + public void soneInserterInsertsASoneIfItIsEligible() throws SoneException { + FreenetURI insertUri = mock(FreenetURI.class); + final FreenetURI finalUri = mock(FreenetURI.class); + String fingerprint = "fingerprint"; + Sone sone = createSone(insertUri, fingerprint); + SoneModificationDetector soneModificationDetector = mock(SoneModificationDetector.class); + when(soneModificationDetector.isEligibleForInsert()).thenReturn(true); + when(freenetInterface.insertDirectory(eq(insertUri), any(HashMap.class), eq("index.html"))).thenReturn(finalUri); + final SoneInserter soneInserter = new SoneInserter(core, eventBus, freenetInterface, "SoneId", soneModificationDetector, 1); + doAnswer(new Answer() { + @Override + public Void answer(InvocationOnMock invocation) throws Throwable { + soneInserter.stop(); + return null; + } + }).when(core).touchConfiguration(); + soneInserter.serviceRun(); + ArgumentCaptor soneEvents = ArgumentCaptor.forClass(SoneEvent.class); + verify(freenetInterface).insertDirectory(eq(insertUri), any(HashMap.class), eq("index.html")); + verify(eventBus, times(2)).post(soneEvents.capture()); + assertThat(soneEvents.getAllValues().get(0), instanceOf(SoneInsertingEvent.class)); + assertThat(soneEvents.getAllValues().get(0).sone(), is(sone)); + assertThat(soneEvents.getAllValues().get(1), instanceOf(SoneInsertedEvent.class)); + assertThat(soneEvents.getAllValues().get(1).sone(), is(sone)); + } + + @Test + public void soneInserterBailsOutIfItIsStoppedWhileInserting() throws SoneException { + FreenetURI insertUri = mock(FreenetURI.class); + final FreenetURI finalUri = mock(FreenetURI.class); + String fingerprint = "fingerprint"; + Sone sone = createSone(insertUri, fingerprint); + SoneModificationDetector soneModificationDetector = mock(SoneModificationDetector.class); + when(soneModificationDetector.isEligibleForInsert()).thenReturn(true); + final SoneInserter soneInserter = new SoneInserter(core, eventBus, freenetInterface, "SoneId", soneModificationDetector, 1); + when(freenetInterface.insertDirectory(eq(insertUri), any(HashMap.class), eq("index.html"))).thenAnswer(new Answer() { + @Override + public FreenetURI answer(InvocationOnMock invocation) throws Throwable { + soneInserter.stop(); + return finalUri; + } + }); + soneInserter.serviceRun(); + ArgumentCaptor soneEvents = ArgumentCaptor.forClass(SoneEvent.class); + verify(freenetInterface).insertDirectory(eq(insertUri), any(HashMap.class), eq("index.html")); + verify(eventBus, times(2)).post(soneEvents.capture()); + assertThat(soneEvents.getAllValues().get(0), instanceOf(SoneInsertingEvent.class)); + assertThat(soneEvents.getAllValues().get(0).sone(), is(sone)); + assertThat(soneEvents.getAllValues().get(1), instanceOf(SoneInsertedEvent.class)); + assertThat(soneEvents.getAllValues().get(1).sone(), is(sone)); + verify(core, never()).touchConfiguration(); + } + + @Test + public void soneInserterDoesNotInsertSoneIfItIsNotEligible() throws SoneException { + FreenetURI insertUri = mock(FreenetURI.class); + String fingerprint = "fingerprint"; + Sone sone = createSone(insertUri, fingerprint); + SoneModificationDetector soneModificationDetector = mock(SoneModificationDetector.class); + final SoneInserter soneInserter = new SoneInserter(core, eventBus, freenetInterface, "SoneId", soneModificationDetector, 1); + new Thread(new Runnable() { + @Override + public void run() { + try { + Thread.sleep(500); + } catch (InterruptedException ie1) { + throw new RuntimeException(ie1); + } + soneInserter.stop(); + } + }).start(); + soneInserter.serviceRun(); + verify(freenetInterface, never()).insertDirectory(eq(insertUri), any(HashMap.class), eq("index.html")); + verify(eventBus, never()).post(argThat(org.hamcrest.Matchers.any(SoneEvent.class))); + } + + @Test + public void soneInserterPostsAbortedEventIfAnExceptionOccurs() throws SoneException { + FreenetURI insertUri = mock(FreenetURI.class); + String fingerprint = "fingerprint"; + Sone sone = createSone(insertUri, fingerprint); + SoneModificationDetector soneModificationDetector = mock(SoneModificationDetector.class); + when(soneModificationDetector.isEligibleForInsert()).thenReturn(true); + final SoneInserter soneInserter = new SoneInserter(core, eventBus, freenetInterface, "SoneId", soneModificationDetector, 1); + final SoneException soneException = new SoneException(new Exception()); + when(freenetInterface.insertDirectory(eq(insertUri), any(HashMap.class), eq("index.html"))).thenAnswer(new Answer() { + @Override + public FreenetURI answer(InvocationOnMock invocation) throws Throwable { + soneInserter.stop(); + throw soneException; + } + }); + soneInserter.serviceRun(); + ArgumentCaptor soneEvents = ArgumentCaptor.forClass(SoneEvent.class); + verify(freenetInterface).insertDirectory(eq(insertUri), any(HashMap.class), eq("index.html")); + verify(eventBus, times(2)).post(soneEvents.capture()); + assertThat(soneEvents.getAllValues().get(0), instanceOf(SoneInsertingEvent.class)); + assertThat(soneEvents.getAllValues().get(0).sone(), is(sone)); + assertThat(soneEvents.getAllValues().get(1), instanceOf(SoneInsertAbortedEvent.class)); + assertThat(soneEvents.getAllValues().get(1).sone(), is(sone)); + verify(core, never()).touchConfiguration(); + } + + @Test + public void soneInserterExitsIfSoneIsUnknown() { + SoneModificationDetector soneModificationDetector = + mock(SoneModificationDetector.class); + SoneInserter soneInserter = + new SoneInserter(core, eventBus, freenetInterface, "SoneId", + soneModificationDetector, 1); + when(soneModificationDetector.isEligibleForInsert()).thenReturn(true); + when(core.getSone("SoneId")).thenReturn(Optional.absent()); + soneInserter.serviceRun(); + } + + @Test + public void soneInserterCatchesExceptionAndContinues() { + SoneModificationDetector soneModificationDetector = + mock(SoneModificationDetector.class); + final SoneInserter soneInserter = + new SoneInserter(core, eventBus, freenetInterface, "SoneId", + soneModificationDetector, 1); + Answer> stopInserterAndThrowException = + new Answer>() { + @Override + public Optional answer( + InvocationOnMock invocation) { + soneInserter.stop(); + throw new NullPointerException(); + } + }; + when(soneModificationDetector.isEligibleForInsert()).thenAnswer( + stopInserterAndThrowException); + soneInserter.serviceRun(); + } + + @Test + public void templateIsRenderedCorrectlyForManifestElement() + throws IOException { + Map soneProperties = new HashMap(); + soneProperties.put("id", "SoneId"); + ManifestCreator manifestCreator = new ManifestCreator(core, soneProperties); + long now = currentTimeMillis(); + when(core.getStartupTime()).thenReturn(now); + ManifestElement manifestElement = manifestCreator.createManifestElement("test.txt", "plain/text; charset=utf-8", "sone-inserter-manifest.txt"); + assertThat(manifestElement.getName(), is("test.txt")); + assertThat(manifestElement.getMimeTypeOverride(), is("plain/text; charset=utf-8")); + String templateContent = new String(toByteArray(manifestElement.getData().getInputStream()), Charsets.UTF_8); + assertThat(templateContent, containsString("Sone Version: " + SonePlugin.VERSION.toString() + "\n")); + assertThat(templateContent, containsString("Core Startup: " + now + "\n")); + assertThat(templateContent, containsString("Sone ID: " + "SoneId" + "\n")); + } + + @Test + public void invalidTemplateReturnsANullManifestElement() { + Map soneProperties = new HashMap(); + ManifestCreator manifestCreator = new ManifestCreator(core, soneProperties); + assertThat(manifestCreator.createManifestElement("test.txt", + "plain/text; charset=utf-8", + "sone-inserter-invalid-manifest.txt"), + nullValue()); + } + + @Test + public void errorWhileRenderingTemplateReturnsANullManifestElement() { + Map soneProperties = new HashMap(); + ManifestCreator manifestCreator = new ManifestCreator(core, soneProperties); + when(core.toString()).thenThrow(NullPointerException.class); + assertThat(manifestCreator.createManifestElement("test.txt", + "plain/text; charset=utf-8", + "sone-inserter-faulty-manifest.txt"), + nullValue()); + } + + } diff --combined src/test/java/net/pterodactylus/sone/core/UpdateCheckerTest.java index 0000000,b3f8c08..a5e3b2a mode 000000,100644..100644 --- a/src/test/java/net/pterodactylus/sone/core/UpdateCheckerTest.java +++ b/src/test/java/net/pterodactylus/sone/core/UpdateCheckerTest.java @@@ -1,0 -1,233 +1,233 @@@ + package net.pterodactylus.sone.core; + + import static java.lang.Long.MAX_VALUE; + import static net.pterodactylus.sone.main.SonePlugin.VERSION; + import static org.hamcrest.MatcherAssert.assertThat; + import static org.hamcrest.Matchers.instanceOf; + import static org.hamcrest.Matchers.is; + import static org.mockito.ArgumentCaptor.forClass; + import static org.mockito.Matchers.any; + import static org.mockito.Matchers.argThat; + import static org.mockito.Mockito.mock; + import static org.mockito.Mockito.never; + import static org.mockito.Mockito.times; + import static org.mockito.Mockito.verify; + import static org.mockito.Mockito.when; + + import java.io.IOException; + import java.io.InputStream; + + import net.pterodactylus.sone.core.FreenetInterface.Callback; + import net.pterodactylus.sone.core.FreenetInterface.Fetched; + import net.pterodactylus.sone.core.event.UpdateFoundEvent; -import net.pterodactylus.sone.freenet.StringBucket; + import net.pterodactylus.util.version.Version; + + import freenet.client.ClientMetadata; + import freenet.client.FetchResult; + import freenet.keys.FreenetURI; + import freenet.support.api.Bucket; ++import freenet.support.io.ArrayBucket; + + import com.google.common.eventbus.EventBus; + import org.junit.Before; + import org.junit.Test; + import org.mockito.ArgumentCaptor; + import org.mockito.invocation.InvocationOnMock; + import org.mockito.stubbing.Answer; + + /** + * Unit test for {@link UpdateChecker}. + * + * @author David ‘Bombe’ Roden + */ + public class UpdateCheckerTest { + + private final EventBus eventBus = mock(EventBus.class); + private final FreenetInterface freenetInterface = mock(FreenetInterface.class); + private final UpdateChecker updateChecker = new UpdateChecker(eventBus, freenetInterface); + + @Before + public void startUpdateChecker() { + updateChecker.start(); + } + + @Test + public void newUpdateCheckerDoesNotHaveALatestVersion() { + assertThat(updateChecker.hasLatestVersion(), is(false)); + assertThat(updateChecker.getLatestVersion(), is(VERSION)); + } + + @Test + public void startingAnUpdateCheckerRegisterAUsk() { + verify(freenetInterface).registerUsk(any(FreenetURI.class), any(Callback.class)); + } + + @Test + public void stoppingAnUpdateCheckerUnregistersAUsk() { + updateChecker.stop(); + verify(freenetInterface).unregisterUsk(any(FreenetURI.class)); + } + + @Test + public void callbackDoesNotDownloadIfNewEditionIsNotFound() { + setupCallbackWithEdition(MAX_VALUE, false, false); + verify(freenetInterface, never()).fetchUri(any(FreenetURI.class)); + verify(eventBus, never()).post(argThat(instanceOf(UpdateFoundEvent.class))); + } + + private void setupCallbackWithEdition(long edition, boolean newKnownGood, boolean newSlot) { + ArgumentCaptor uri = forClass(FreenetURI.class); + ArgumentCaptor callback = forClass(Callback.class); + verify(freenetInterface).registerUsk(uri.capture(), callback.capture()); + callback.getValue().editionFound(uri.getValue(), edition, newKnownGood, newSlot); + } + + @Test + public void callbackStartsIfNewEditionIsFound() { + setupFetchResult(createFutureFetchResult()); + setupCallbackWithEdition(MAX_VALUE, true, false); + verifyAFreenetUriIsFetched(); + ArgumentCaptor updateFoundEvent = forClass(UpdateFoundEvent.class); + verify(eventBus, times(1)).post(updateFoundEvent.capture()); + assertThat(updateFoundEvent.getValue().version(), is(new Version(99, 0, 0))); + assertThat(updateFoundEvent.getValue().releaseTime(), is(11865368297000L)); + assertThat(updateChecker.getLatestVersion(), is(new Version(99, 0, 0))); + assertThat(updateChecker.getLatestVersionDate(), is(11865368297000L)); + assertThat(updateChecker.hasLatestVersion(), is(true)); + } + + private FetchResult createFutureFetchResult() { + ClientMetadata clientMetadata = new ClientMetadata("application/xml"); - Bucket fetched = new StringBucket("# MapConfigurationBackendVersion=1\n" + ++ Bucket fetched = new ArrayBucket(("# MapConfigurationBackendVersion=1\n" + + "CurrentVersion/Version: 99.0.0\n" + - "CurrentVersion/ReleaseTime: 11865368297000"); ++ "CurrentVersion/ReleaseTime: 11865368297000").getBytes()); + return new FetchResult(clientMetadata, fetched); + } + + @Test + public void callbackDoesNotStartIfNoNewEditionIsFound() { + setupFetchResult(createPastFetchResult()); + setupCallbackWithEdition(updateChecker.getLatestEdition(), true, false); + verifyAFreenetUriIsFetched(); + verifyNoUpdateFoundEventIsFired(); + } + + private void setupFetchResult(final FetchResult pastFetchResult) { + when(freenetInterface.fetchUri(any(FreenetURI.class))).thenAnswer(new Answer() { + @Override + public Fetched answer(InvocationOnMock invocation) throws Throwable { + FreenetURI freenetUri = (FreenetURI) invocation.getArguments()[0]; + return new Fetched(freenetUri, pastFetchResult); + } + }); + } + + private FetchResult createPastFetchResult() { + ClientMetadata clientMetadata = new ClientMetadata("application/xml"); - Bucket fetched = new StringBucket("# MapConfigurationBackendVersion=1\n" + ++ Bucket fetched = new ArrayBucket(("# MapConfigurationBackendVersion=1\n" + + "CurrentVersion/Version: 0.2\n" + - "CurrentVersion/ReleaseTime: 1289417883000"); ++ "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("Some other data.") { ++ Bucket fetched = new ArrayBucket("Some other data.".getBytes()) { + @Override + public InputStream getInputStream() { + try { + return when(mock(InputStream.class).read()).thenThrow(IOException.class).getMock(); + } catch (IOException ioe1) { + /* won’t throw here. */ + return null; + } + } + }; + return new FetchResult(clientMetadata, fetched); + } + + @Test + public void invalidTimeDoesNotCauseAnUpdateToBeFound() { + setupFetchResult(createInvalidTimeFetchResult()); + setupCallbackWithEdition(MAX_VALUE, true, false); + verifyAFreenetUriIsFetched(); + verifyNoUpdateFoundEventIsFired(); + } + + private FetchResult createInvalidTimeFetchResult() { + ClientMetadata clientMetadata = new ClientMetadata("application/xml"); - Bucket fetched = new StringBucket("# MapConfigurationBackendVersion=1\n" + ++ Bucket fetched = new ArrayBucket(("# MapConfigurationBackendVersion=1\n" + + "CurrentVersion/Version: 0.2\n" + - "CurrentVersion/ReleaseTime: invalid"); ++ "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" + - "CurrentVersion/Version: 0.2\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"); - Bucket fetched = new StringBucket("# MapConfigurationBackendVersion=1\n" + ++ Bucket fetched = new ArrayBucket(("# MapConfigurationBackendVersion=1\n" + + "CurrentVersion/Version: foo\n" + - "CurrentVersion/ReleaseTime: 1289417883000"); ++ "CurrentVersion/ReleaseTime: 1289417883000").getBytes()); + return new FetchResult(clientMetadata, fetched); + } + + }