Merge branch 'next' into feature/album-and-image-links
authorDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Mon, 27 Jul 2015 18:30:21 +0000 (20:30 +0200)
committerDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Mon, 27 Jul 2015 18:30:21 +0000 (20:30 +0200)
16 files changed:
pom.xml
src/main/java/net/pterodactylus/sone/core/SoneParser.java
src/main/java/net/pterodactylus/sone/main/DebugLoaders.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/main/DefaultLoaders.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/main/Loaders.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/main/SonePlugin.java
src/main/java/net/pterodactylus/sone/template/FilesystemTemplate.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/template/ImageLinkFilter.java
src/main/java/net/pterodactylus/sone/web/ReloadingPage.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/web/WebInterface.java
src/test/java/net/pterodactylus/sone/main/DebugLoadersTest.java [new file with mode: 0644]
src/test/java/net/pterodactylus/sone/main/DefaultLoadersTest.java [new file with mode: 0644]
src/test/java/net/pterodactylus/sone/template/FilesystemTemplateTest.java [new file with mode: 0644]
src/test/java/net/pterodactylus/sone/template/ImageLinkFilterTest.java [new file with mode: 0644]
src/test/resources/net/pterodactylus/sone/main/template.txt [new file with mode: 0644]
template.txt [new file with mode: 0644]

diff --git a/pom.xml b/pom.xml
index a76b3b2..13937ce 100644 (file)
--- a/pom.xml
+++ b/pom.xml
                        <version>1.3</version>
                </dependency>
                <dependency>
+                       <groupId>org.jsoup</groupId>
+                       <artifactId>jsoup</artifactId>
+                       <version>1.7.1</version>
+                       <scope>test</scope>
+               </dependency>
+               <dependency>
                        <groupId>org.freenetproject</groupId>
                        <artifactId>fred</artifactId>
                        <version>0.7.5.1467.99.3</version>
index 24b856f..9122852 100644 (file)
@@ -326,16 +326,13 @@ public class SoneParser {
                }
 
                /* okay, apparently everything was parsed correctly. Now import. */
-               /* atomic setter operation on the Sone. */
-               synchronized (sone) {
-                       sone.setProfile(profile);
-                       sone.setPosts(posts);
-                       sone.setReplies(replies);
-                       sone.setLikePostIds(likedPostIds);
-                       sone.setLikeReplyIds(likedReplyIds);
-                       for (Album album : topLevelAlbums) {
-                               sone.getRootAlbum().addAlbum(album);
-                       }
+               sone.setProfile(profile);
+               sone.setPosts(posts);
+               sone.setReplies(replies);
+               sone.setLikePostIds(likedPostIds);
+               sone.setLikeReplyIds(likedReplyIds);
+               for (Album album : topLevelAlbums) {
+                       sone.getRootAlbum().addAlbum(album);
                }
 
                return sone;
diff --git a/src/main/java/net/pterodactylus/sone/main/DebugLoaders.java b/src/main/java/net/pterodactylus/sone/main/DebugLoaders.java
new file mode 100644 (file)
index 0000000..11755e0
--- /dev/null
@@ -0,0 +1,41 @@
+package net.pterodactylus.sone.main;
+
+import java.io.File;
+
+import net.pterodactylus.sone.template.FilesystemTemplate;
+import net.pterodactylus.sone.web.ReloadingPage;
+import net.pterodactylus.util.template.FilesystemTemplateProvider;
+import net.pterodactylus.util.template.Template;
+import net.pterodactylus.util.template.TemplateProvider;
+import net.pterodactylus.util.web.Page;
+import net.pterodactylus.util.web.Request;
+
+/**
+ * {@link Loaders} implementation that loads all resources from the filesystem.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class DebugLoaders implements Loaders {
+
+       private final String filesystemPath;
+
+       public DebugLoaders(String filesystemPath) {
+               this.filesystemPath = filesystemPath;
+       }
+
+       @Override
+       public Template loadTemplate(String path) {
+               return new FilesystemTemplate(new File(filesystemPath, path).getAbsolutePath());
+       }
+
+       @Override
+       public <REQ extends Request> Page<REQ> loadStaticPage(String basePath, String prefix, String mimeType) {
+               return new ReloadingPage<REQ>(basePath, new File(filesystemPath, prefix).getAbsolutePath(), mimeType);
+       }
+
+       @Override
+       public TemplateProvider getTemplateProvider() {
+               return new FilesystemTemplateProvider(new File(filesystemPath, "/templates/").getAbsolutePath());
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/sone/main/DefaultLoaders.java b/src/main/java/net/pterodactylus/sone/main/DefaultLoaders.java
new file mode 100644 (file)
index 0000000..0f73216
--- /dev/null
@@ -0,0 +1,52 @@
+package net.pterodactylus.sone.main;
+
+import static net.pterodactylus.util.template.TemplateParser.parse;
+
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.io.UnsupportedEncodingException;
+
+import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.util.io.Closer;
+import net.pterodactylus.util.template.ClassPathTemplateProvider;
+import net.pterodactylus.util.template.Template;
+import net.pterodactylus.util.template.TemplateProvider;
+import net.pterodactylus.util.web.Page;
+import net.pterodactylus.util.web.Request;
+import net.pterodactylus.util.web.StaticPage;
+
+/**
+ * Default {@link Loaders} implementation that loads resources from the classpath.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class DefaultLoaders implements Loaders {
+
+       @Override
+       public Template loadTemplate(String path) {
+               InputStream templateInputStream = null;
+               Reader reader = null;
+               try {
+                       templateInputStream = getClass().getResourceAsStream(path);
+                       reader = new InputStreamReader(templateInputStream, "UTF-8");
+                       return parse(reader);
+               } catch (UnsupportedEncodingException uee1) {
+                       throw new RuntimeException("UTF-8 not supported.");
+               } finally {
+                       Closer.close(reader);
+                       Closer.close(templateInputStream);
+               }
+       }
+
+       @Override
+       public <REQ extends Request> Page<REQ> loadStaticPage(String pathPrefix, String basePath, String mimeType) {
+               return new StaticPage<REQ>(pathPrefix, basePath, mimeType);
+       }
+
+       @Override
+       public TemplateProvider getTemplateProvider() {
+               return new ClassPathTemplateProvider(WebInterface.class, "/templates/");
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/sone/main/Loaders.java b/src/main/java/net/pterodactylus/sone/main/Loaders.java
new file mode 100644 (file)
index 0000000..34ee1b1
--- /dev/null
@@ -0,0 +1,22 @@
+package net.pterodactylus.sone.main;
+
+import net.pterodactylus.util.template.Template;
+import net.pterodactylus.util.template.TemplateProvider;
+import net.pterodactylus.util.web.Page;
+import net.pterodactylus.util.web.Request;
+
+import com.google.inject.ImplementedBy;
+
+/**
+ * Defines loaders for resources that can be loaded from various locations.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+@ImplementedBy(DefaultLoaders.class)
+public interface Loaders {
+
+       Template loadTemplate(String path);
+       <REQ extends Request> Page<REQ> loadStaticPage(String basePath, String prefix, String mimeType);
+       TemplateProvider getTemplateProvider();
+
+}
index 1df4a33..cbdca34 100644 (file)
@@ -250,6 +250,12 @@ public class SonePlugin implements FredPlugin, FredPluginFCP, FredPluginL10n, Fr
                                bind(Context.class).toInstance(context);
                                bind(getOptionalContextTypeLiteral()).toInstance(of(context));
                                bind(SonePlugin.class).toInstance(SonePlugin.this);
+                               if (startConfiguration.getBooleanValue("Developer.LoadFromFilesystem").getValue(false)) {
+                                       String path = startConfiguration.getStringValue("Developer.FilesystemPath").getValue(null);
+                                       if (path != null) {
+                                               bind(Loaders.class).toInstance(new DebugLoaders(path));
+                                       }
+                               }
                                bindListener(Matchers.any(), new TypeListener() {
 
                                        @Override
@@ -295,7 +301,6 @@ public class SonePlugin implements FredPlugin, FredPluginFCP, FredPluginL10n, Fr
         */
        @Override
        public void terminate() {
-               deregisterLoggerHandlers();
                try {
                        /* stop the web interface. */
                        webInterface.stop();
@@ -307,6 +312,8 @@ public class SonePlugin implements FredPlugin, FredPluginFCP, FredPluginL10n, Fr
                        webOfTrustConnector.stop();
                } catch (Throwable t1) {
                        logger.log(Level.SEVERE, "Error while shutting down!", t1);
+               } finally {
+                       deregisterLoggerHandlers();
                }
        }
 
diff --git a/src/main/java/net/pterodactylus/sone/template/FilesystemTemplate.java b/src/main/java/net/pterodactylus/sone/template/FilesystemTemplate.java
new file mode 100644 (file)
index 0000000..473c191
--- /dev/null
@@ -0,0 +1,145 @@
+package net.pterodactylus.sone.template;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.io.Writer;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
+
+import net.pterodactylus.util.template.Part;
+import net.pterodactylus.util.template.Template;
+import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.template.TemplateException;
+import net.pterodactylus.util.template.TemplateParser;
+
+import freenet.support.io.Closer;
+
+import com.google.common.base.Charsets;
+
+/**
+ * {@link Template} implementation that can be reloaded from the filesystem.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class FilesystemTemplate extends Template {
+
+       private final String filename;
+       private final AtomicReference<LastLoadedTemplate> lastTemplate = new AtomicReference<LastLoadedTemplate>();
+       private final TemplateContext initialContext = new TemplateContext();
+       private final List<Part> parts = new ArrayList<Part>();
+
+       public FilesystemTemplate(String filename) {
+               this.filename = filename;
+       }
+
+       @Override
+       public TemplateContext getInitialContext() {
+               loadTemplate();
+               return initialContext;
+       }
+
+       private void loadTemplate() {
+               File templateFile = new File(filename);
+               if (!templateFile.exists()) {
+                       throw new TemplateFileNotFoundException(filename);
+               }
+               if (templateWasLoaded() && !templateFileHasBeenModifiedAfterLoading(templateFile)) {
+                       return;
+               }
+               InputStream templateInputStream = null;
+               Reader templateReader = null;
+               try {
+                       templateInputStream = new FileInputStream(templateFile);
+                       templateReader = new InputStreamReader(templateInputStream, Charsets.UTF_8);
+                       Template template = TemplateParser.parse(templateReader);
+                       lastTemplate.set(new LastLoadedTemplate(template));
+                       template.getInitialContext().mergeContext(initialContext);
+                       for (Part part : parts) {
+                               template.add(part);
+                       }
+               } catch (FileNotFoundException e) {
+                       throw new TemplateFileNotFoundException(filename);
+               } finally {
+                       Closer.close(templateReader);
+                       Closer.close(templateInputStream);
+               }
+       }
+
+       private boolean templateWasLoaded() {
+               return lastTemplate.get() != null;
+       }
+
+       private boolean templateFileHasBeenModifiedAfterLoading(File templateFile) {
+               return templateFile.lastModified() > lastTemplate.get().getLoadTime();
+       }
+
+       @Override
+       public void add(Part part) {
+               loadTemplate();
+               parts.add(part);
+               lastTemplate.get().getTemplate().add(part);
+       }
+
+       @Override
+       public void render(TemplateContext templateContext, Writer writer) throws TemplateException {
+               loadTemplate();
+               lastTemplate.get().getTemplate().render(templateContext, writer);
+       }
+
+       @Override
+       public Iterator<Part> iterator() {
+               loadTemplate();
+               return lastTemplate.get().getTemplate().iterator();
+       }
+
+       @Override
+       public int getLine() {
+               loadTemplate();
+               return lastTemplate.get().getTemplate().getLine();
+       }
+
+       @Override
+       public int getColumn() {
+               loadTemplate();
+               return lastTemplate.get().getTemplate().getColumn();
+       }
+
+       private static class LastLoadedTemplate {
+
+               private final Template template;
+               private final long loadTime = System.currentTimeMillis();
+
+               private LastLoadedTemplate(Template template) {
+                       this.template = template;
+               }
+
+               public Template getTemplate() {
+                       return template;
+               }
+
+               public long getLoadTime() {
+                       return loadTime;
+               }
+
+       }
+
+       /**
+        * Exception that signals that a template file could not be found.
+        *
+        * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+        */
+       public static class TemplateFileNotFoundException extends RuntimeException {
+
+               public TemplateFileNotFoundException(String filename) {
+                       super(filename);
+               }
+
+       }
+
+}
index 9ede851..65e8cdb 100644 (file)
@@ -33,6 +33,7 @@ import net.pterodactylus.util.template.TemplateContext;
 import net.pterodactylus.util.template.TemplateContextFactory;
 import net.pterodactylus.util.template.TemplateParser;
 
+import com.google.common.base.Function;
 import com.google.common.base.Optional;
 
 /**
@@ -79,11 +80,11 @@ public class ImageLinkFilter implements Filter {
                if (image == null) {
                        return null;
                }
-               String imageClass = valueOf(parameters.get("class"));
+               String imageClass = Optional.fromNullable(parameters.get("class")).transform(getStringValue()).orNull();
                int maxWidth = parseInt(valueOf(parameters.get("max-width")), MAX_VALUE);
                int maxHeight = parseInt(valueOf(parameters.get("max-height")), MAX_VALUE);
                String mode = valueOf(parameters.get("mode"));
-               String title = valueOf(parameters.get("title"));
+               String title = Optional.fromNullable(parameters.get("title")).transform(getStringValue()).orNull();
 
                TemplateContext linkTemplateContext = templateContextFactory.createTemplateContext();
                linkTemplateContext.set("class", imageClass);
@@ -117,4 +118,13 @@ public class ImageLinkFilter implements Filter {
                return stringWriter.toString();
        }
 
+       private Function<Object, String> getStringValue() {
+               return new Function<Object, String>() {
+                       @Override
+                       public String apply(Object input) {
+                               return (input != null) ? input.toString() : null;
+                       }
+               };
+       }
+
 }
diff --git a/src/main/java/net/pterodactylus/sone/web/ReloadingPage.java b/src/main/java/net/pterodactylus/sone/web/ReloadingPage.java
new file mode 100644 (file)
index 0000000..0c8f516
--- /dev/null
@@ -0,0 +1,88 @@
+/*
+ * Sone - ReloadingPage.java - Copyright © 2010–2015 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.web;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import net.pterodactylus.util.io.Closer;
+import net.pterodactylus.util.io.StreamCopier;
+import net.pterodactylus.util.web.Page;
+import net.pterodactylus.util.web.Request;
+import net.pterodactylus.util.web.Response;
+
+/**
+ * {@link Page} implementation that delivers static files from the filesystem.
+ *
+ * @param <REQ>
+ *            The type of the request
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class ReloadingPage<REQ extends Request> implements Page<REQ> {
+
+       private final String pathPrefix;
+       private final String filesystemPath;
+       private final String mimeType;
+
+       public ReloadingPage(String pathPrefix, String filesystemPathPrefix, String mimeType) {
+               this.pathPrefix = pathPrefix;
+               this.filesystemPath = filesystemPathPrefix;
+               this.mimeType = mimeType;
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public String getPath() {
+               return pathPrefix;
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public boolean isPrefixPage() {
+               return true;
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public Response handleRequest(REQ request, Response response) throws IOException {
+               String path = request.getUri().getPath();
+               int lastSlash = path.lastIndexOf('/');
+               String filename = path.substring(lastSlash + 1);
+               InputStream fileInputStream = new FileInputStream(new File(filesystemPath, filename));
+               if (fileInputStream == null) {
+                       return response.setStatusCode(404).setStatusText("Not found.");
+               }
+               OutputStream contentOutputStream = response.getContent();
+               try {
+                       StreamCopier.copy(fileInputStream, contentOutputStream);
+               } finally {
+                       Closer.close(fileInputStream);
+                       Closer.close(contentOutputStream);
+               }
+               return response.setStatusCode(200).setStatusText("OK").setContentType(mimeType);
+       }
+}
index 974299d..06ebf8d 100644 (file)
@@ -21,11 +21,7 @@ import static java.util.logging.Logger.getLogger;
 import static net.pterodactylus.util.template.TemplateParser.parse;
 
 import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.Reader;
 import java.io.StringReader;
-import java.io.UnsupportedEncodingException;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
@@ -72,6 +68,7 @@ import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.freenet.L10nFilter;
 import net.pterodactylus.sone.freenet.wot.Identity;
 import net.pterodactylus.sone.freenet.wot.Trust;
+import net.pterodactylus.sone.main.Loaders;
 import net.pterodactylus.sone.main.ReparseFilter;
 import net.pterodactylus.sone.main.SonePlugin;
 import net.pterodactylus.sone.notify.ListNotification;
@@ -131,11 +128,9 @@ import net.pterodactylus.sone.web.ajax.UntrustAjaxPage;
 import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.sone.web.page.PageToadlet;
 import net.pterodactylus.sone.web.page.PageToadletFactory;
-import net.pterodactylus.util.io.Closer;
 import net.pterodactylus.util.notify.Notification;
 import net.pterodactylus.util.notify.NotificationManager;
 import net.pterodactylus.util.notify.TemplateNotification;
-import net.pterodactylus.util.template.ClassPathTemplateProvider;
 import net.pterodactylus.util.template.CollectionSortFilter;
 import net.pterodactylus.util.template.ContainsFilter;
 import net.pterodactylus.util.template.DateFilter;
@@ -152,7 +147,6 @@ import net.pterodactylus.util.template.TemplateContextFactory;
 import net.pterodactylus.util.template.TemplateProvider;
 import net.pterodactylus.util.template.XmlFilter;
 import net.pterodactylus.util.web.RedirectPage;
-import net.pterodactylus.util.web.StaticPage;
 import net.pterodactylus.util.web.TemplatePage;
 
 import com.google.common.collect.Collections2;
@@ -178,6 +172,9 @@ public class WebInterface {
        /** The logger. */
        private static final Logger logger = getLogger(WebInterface.class.getName());
 
+       /** The loaders for templates, pages, and classpath providers. */
+       private final Loaders loaders;
+
        /** The notification manager. */
        private final NotificationManager notificationManager = new NotificationManager();
 
@@ -249,8 +246,9 @@ public class WebInterface {
         *            The Sone plugin
         */
        @Inject
-       public WebInterface(SonePlugin sonePlugin) {
+       public WebInterface(SonePlugin sonePlugin, Loaders loaders) {
                this.sonePlugin = sonePlugin;
+               this.loaders = loaders;
                formPassword = sonePlugin.pluginRespirator().getToadletContainer().getFormPassword();
                soneTextParser = new SoneTextParser(getCore(), getCore(), getCore());
 
@@ -291,60 +289,45 @@ public class WebInterface {
                templateContextFactory.addFilter("paginate", new PaginationFilter());
                templateContextFactory.addFilter("build-id", new BuildIdFilter());
                templateContextFactory.addProvider(TemplateProvider.TEMPLATE_CONTEXT_PROVIDER);
-               templateContextFactory.addProvider(new ClassPathTemplateProvider(WebInterface.class, "/templates/"));
+               templateContextFactory.addProvider(loaders.getTemplateProvider());
                templateContextFactory.addTemplateObject("webInterface", this);
                templateContextFactory.addTemplateObject("formPassword", formPassword);
 
                /* create notifications. */
-               Template newSoneNotificationTemplate = parseTemplate("/templates/notify/newSoneNotification.html");
+               Template newSoneNotificationTemplate = loaders.loadTemplate("/templates/notify/newSoneNotification.html");
                newSoneNotification = new ListNotification<Sone>("new-sone-notification", "sones", newSoneNotificationTemplate, false);
 
-               Template newPostNotificationTemplate = parseTemplate("/templates/notify/newPostNotification.html");
+               Template newPostNotificationTemplate = loaders.loadTemplate("/templates/notify/newPostNotification.html");
                newPostNotification = new ListNotification<Post>("new-post-notification", "posts", newPostNotificationTemplate, false);
 
-               Template localPostNotificationTemplate = parseTemplate("/templates/notify/newPostNotification.html");
+               Template localPostNotificationTemplate = loaders.loadTemplate("/templates/notify/newPostNotification.html");
                localPostNotification = new ListNotification<Post>("local-post-notification", "posts", localPostNotificationTemplate, false);
 
-               Template newReplyNotificationTemplate = parseTemplate("/templates/notify/newReplyNotification.html");
+               Template newReplyNotificationTemplate = loaders.loadTemplate("/templates/notify/newReplyNotification.html");
                newReplyNotification = new ListNotification<PostReply>("new-reply-notification", "replies", newReplyNotificationTemplate, false);
 
-               Template localReplyNotificationTemplate = parseTemplate("/templates/notify/newReplyNotification.html");
+               Template localReplyNotificationTemplate = loaders.loadTemplate("/templates/notify/newReplyNotification.html");
                localReplyNotification = new ListNotification<PostReply>("local-reply-notification", "replies", localReplyNotificationTemplate, false);
 
-               Template mentionNotificationTemplate = parseTemplate("/templates/notify/mentionNotification.html");
+               Template mentionNotificationTemplate = loaders.loadTemplate("/templates/notify/mentionNotification.html");
                mentionNotification = new ListNotification<Post>("mention-notification", "posts", mentionNotificationTemplate, false);
 
-               Template lockedSonesTemplate = parseTemplate("/templates/notify/lockedSonesNotification.html");
+               Template lockedSonesTemplate = loaders.loadTemplate("/templates/notify/lockedSonesNotification.html");
                lockedSonesNotification = new ListNotification<Sone>("sones-locked-notification", "sones", lockedSonesTemplate);
 
-               Template newVersionTemplate = parseTemplate("/templates/notify/newVersionNotification.html");
+               Template newVersionTemplate = loaders.loadTemplate("/templates/notify/newVersionNotification.html");
                newVersionNotification = new TemplateNotification("new-version-notification", newVersionTemplate);
 
-               Template insertingImagesTemplate = parseTemplate("/templates/notify/inserting-images-notification.html");
+               Template insertingImagesTemplate = loaders.loadTemplate("/templates/notify/inserting-images-notification.html");
                insertingImagesNotification = new ListNotification<Image>("inserting-images-notification", "images", insertingImagesTemplate);
 
-               Template insertedImagesTemplate = parseTemplate("/templates/notify/inserted-images-notification.html");
+               Template insertedImagesTemplate = loaders.loadTemplate("/templates/notify/inserted-images-notification.html");
                insertedImagesNotification = new ListNotification<Image>("inserted-images-notification", "images", insertedImagesTemplate);
 
-               Template imageInsertFailedTemplate = parseTemplate("/templates/notify/image-insert-failed-notification.html");
+               Template imageInsertFailedTemplate = loaders.loadTemplate("/templates/notify/image-insert-failed-notification.html");
                imageInsertFailedNotification = new ListNotification<Image>("image-insert-failed-notification", "images", imageInsertFailedTemplate);
        }
 
-       private Template parseTemplate(String resourceName) {
-               InputStream templateInputStream = null;
-               Reader reader = null;
-               try {
-                       templateInputStream = getClass().getResourceAsStream(resourceName);
-                       reader = new InputStreamReader(templateInputStream, "UTF-8");
-                       return parse(reader);
-               } catch (UnsupportedEncodingException uee1) {
-                       throw new RuntimeException("UTF-8 not supported.");
-               } finally {
-                       Closer.close(reader);
-                       Closer.close(templateInputStream);
-               }
-       }
-
        //
        // ACCESSORS
        //
@@ -533,7 +516,7 @@ public class WebInterface {
         */
        public void setFirstStart(boolean firstStart) {
                if (firstStart) {
-                       Template firstStartNotificationTemplate = parseTemplate("/templates/notify/firstStartNotification.html");
+                       Template firstStartNotificationTemplate = loaders.loadTemplate("/templates/notify/firstStartNotification.html");
                        Notification firstStartNotification = new TemplateNotification("first-start-notification", firstStartNotificationTemplate);
                        notificationManager.addNotification(firstStartNotification);
                }
@@ -548,7 +531,7 @@ public class WebInterface {
         */
        public void setNewConfig(boolean newConfig) {
                if (newConfig && !hasFirstStartNotification()) {
-                       Template configNotReadNotificationTemplate = parseTemplate("/templates/notify/configNotReadNotification.html");
+                       Template configNotReadNotificationTemplate = loaders.loadTemplate("/templates/notify/configNotReadNotification.html");
                        Notification configNotReadNotification = new TemplateNotification("config-not-read-notification", configNotReadNotificationTemplate);
                        notificationManager.addNotification(configNotReadNotification);
                }
@@ -579,7 +562,7 @@ public class WebInterface {
                registerToadlets();
 
                /* notification templates. */
-               Template startupNotificationTemplate = parseTemplate("/templates/notify/startupNotification.html");
+               Template startupNotificationTemplate = loaders.loadTemplate("/templates/notify/startupNotification.html");
 
                final TemplateNotification startupNotification = new TemplateNotification("startup-notification", startupNotificationTemplate);
                notificationManager.addNotification(startupNotification);
@@ -592,7 +575,7 @@ public class WebInterface {
                        }
                }, 2, TimeUnit.MINUTES);
 
-               Template wotMissingNotificationTemplate = parseTemplate("/templates/notify/wotMissingNotification.html");
+               Template wotMissingNotificationTemplate = loaders.loadTemplate("/templates/notify/wotMissingNotification.html");
                final TemplateNotification wotMissingNotification = new TemplateNotification("wot-missing-notification", wotMissingNotificationTemplate);
                ticker.scheduleAtFixedRate(new Runnable() {
 
@@ -626,37 +609,37 @@ public class WebInterface {
         */
        private void registerToadlets() {
                Template emptyTemplate = parse(new StringReader(""));
-               Template loginTemplate = parseTemplate("/templates/login.html");
-               Template indexTemplate = parseTemplate("/templates/index.html");
-               Template newTemplate = parseTemplate("/templates/new.html");
-               Template knownSonesTemplate = parseTemplate("/templates/knownSones.html");
-               Template createSoneTemplate = parseTemplate("/templates/createSone.html");
-               Template createPostTemplate = parseTemplate("/templates/createPost.html");
-               Template createReplyTemplate = parseTemplate("/templates/createReply.html");
-               Template bookmarksTemplate = parseTemplate("/templates/bookmarks.html");
-               Template searchTemplate = parseTemplate("/templates/search.html");
-               Template editProfileTemplate = parseTemplate("/templates/editProfile.html");
-               Template editProfileFieldTemplate = parseTemplate("/templates/editProfileField.html");
-               Template deleteProfileFieldTemplate = parseTemplate("/templates/deleteProfileField.html");
-               Template viewSoneTemplate = parseTemplate("/templates/viewSone.html");
-               Template viewPostTemplate = parseTemplate("/templates/viewPost.html");
-               Template deletePostTemplate = parseTemplate("/templates/deletePost.html");
-               Template deleteReplyTemplate = parseTemplate("/templates/deleteReply.html");
-               Template deleteSoneTemplate = parseTemplate("/templates/deleteSone.html");
-               Template imageBrowserTemplate = parseTemplate("/templates/imageBrowser.html");
-               Template createAlbumTemplate = parseTemplate("/templates/createAlbum.html");
-               Template deleteAlbumTemplate = parseTemplate("/templates/deleteAlbum.html");
-               Template deleteImageTemplate = parseTemplate("/templates/deleteImage.html");
-               Template noPermissionTemplate = parseTemplate("/templates/noPermission.html");
-               Template emptyImageTitleTemplate = parseTemplate("/templates/emptyImageTitle.html");
-               Template emptyAlbumTitleTemplate = parseTemplate("/templates/emptyAlbumTitle.html");
-               Template optionsTemplate = parseTemplate("/templates/options.html");
-               Template rescueTemplate = parseTemplate("/templates/rescue.html");
-               Template aboutTemplate = parseTemplate("/templates/about.html");
-               Template invalidTemplate = parseTemplate("/templates/invalid.html");
-               Template postTemplate = parseTemplate("/templates/include/viewPost.html");
-               Template replyTemplate = parseTemplate("/templates/include/viewReply.html");
-               Template openSearchTemplate = parseTemplate("/templates/xml/OpenSearch.xml");
+               Template loginTemplate = loaders.loadTemplate("/templates/login.html");
+               Template indexTemplate = loaders.loadTemplate("/templates/index.html");
+               Template newTemplate = loaders.loadTemplate("/templates/new.html");
+               Template knownSonesTemplate = loaders.loadTemplate("/templates/knownSones.html");
+               Template createSoneTemplate = loaders.loadTemplate("/templates/createSone.html");
+               Template createPostTemplate = loaders.loadTemplate("/templates/createPost.html");
+               Template createReplyTemplate = loaders.loadTemplate("/templates/createReply.html");
+               Template bookmarksTemplate = loaders.loadTemplate("/templates/bookmarks.html");
+               Template searchTemplate = loaders.loadTemplate("/templates/search.html");
+               Template editProfileTemplate = loaders.loadTemplate("/templates/editProfile.html");
+               Template editProfileFieldTemplate = loaders.loadTemplate("/templates/editProfileField.html");
+               Template deleteProfileFieldTemplate = loaders.loadTemplate("/templates/deleteProfileField.html");
+               Template viewSoneTemplate = loaders.loadTemplate("/templates/viewSone.html");
+               Template viewPostTemplate = loaders.loadTemplate("/templates/viewPost.html");
+               Template deletePostTemplate = loaders.loadTemplate("/templates/deletePost.html");
+               Template deleteReplyTemplate = loaders.loadTemplate("/templates/deleteReply.html");
+               Template deleteSoneTemplate = loaders.loadTemplate("/templates/deleteSone.html");
+               Template imageBrowserTemplate = loaders.loadTemplate("/templates/imageBrowser.html");
+               Template createAlbumTemplate = loaders.loadTemplate("/templates/createAlbum.html");
+               Template deleteAlbumTemplate = loaders.loadTemplate("/templates/deleteAlbum.html");
+               Template deleteImageTemplate = loaders.loadTemplate("/templates/deleteImage.html");
+               Template noPermissionTemplate = loaders.loadTemplate("/templates/noPermission.html");
+               Template emptyImageTitleTemplate = loaders.loadTemplate("/templates/emptyImageTitle.html");
+               Template emptyAlbumTitleTemplate = loaders.loadTemplate("/templates/emptyAlbumTitle.html");
+               Template optionsTemplate = loaders.loadTemplate("/templates/options.html");
+               Template rescueTemplate = loaders.loadTemplate("/templates/rescue.html");
+               Template aboutTemplate = loaders.loadTemplate("/templates/about.html");
+               Template invalidTemplate = loaders.loadTemplate("/templates/invalid.html");
+               Template postTemplate = loaders.loadTemplate("/templates/include/viewPost.html");
+               Template replyTemplate = loaders.loadTemplate("/templates/include/viewReply.html");
+               Template openSearchTemplate = loaders.loadTemplate("/templates/xml/OpenSearch.xml");
 
                PageToadletFactory pageToadletFactory = new PageToadletFactory(sonePlugin.pluginRespirator().getHLSimpleClient(), "/Sone/");
                pageToadlets.add(pageToadletFactory.createPageToadlet(new RedirectPage<FreenetRequest>("", "index.html")));
@@ -705,9 +688,9 @@ public class WebInterface {
                pageToadlets.add(pageToadletFactory.createPageToadlet(new SoneTemplatePage("emptyAlbumTitle.html", emptyAlbumTitleTemplate, "Page.EmptyAlbumTitle.Title", this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new DismissNotificationPage(emptyTemplate, this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new SoneTemplatePage("invalid.html", invalidTemplate, "Page.Invalid.Title", this)));
-               pageToadlets.add(pageToadletFactory.createPageToadlet(new StaticPage<FreenetRequest>("css/", "/static/css/", "text/css")));
-               pageToadlets.add(pageToadletFactory.createPageToadlet(new StaticPage<FreenetRequest>("javascript/", "/static/javascript/", "text/javascript")));
-               pageToadlets.add(pageToadletFactory.createPageToadlet(new StaticPage<FreenetRequest>("images/", "/static/images/", "image/png")));
+               pageToadlets.add(pageToadletFactory.createPageToadlet(loaders.<FreenetRequest>loadStaticPage("css/", "/static/css/", "text/css")));
+               pageToadlets.add(pageToadletFactory.createPageToadlet(loaders.<FreenetRequest>loadStaticPage("javascript/", "/static/javascript/", "text/javascript")));
+               pageToadlets.add(pageToadletFactory.createPageToadlet(loaders.<FreenetRequest>loadStaticPage("images/", "/static/images/", "image/png")));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new TemplatePage<FreenetRequest>("OpenSearch.xml", "application/opensearchdescription+xml", templateContextFactory, openSearchTemplate)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new GetImagePage(this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new GetTranslationPage(this)));
@@ -800,7 +783,7 @@ public class WebInterface {
                synchronized (soneInsertNotifications) {
                        TemplateNotification templateNotification = soneInsertNotifications.get(sone);
                        if (templateNotification == null) {
-                               templateNotification = new TemplateNotification(parseTemplate("/templates/notify/soneInsertNotification.html"));
+                               templateNotification = new TemplateNotification(loaders.loadTemplate("/templates/notify/soneInsertNotification.html"));
                                templateNotification.set("insertSone", sone);
                                soneInsertNotifications.put(sone, templateNotification);
                        }
diff --git a/src/test/java/net/pterodactylus/sone/main/DebugLoadersTest.java b/src/test/java/net/pterodactylus/sone/main/DebugLoadersTest.java
new file mode 100644 (file)
index 0000000..c3fa7e9
--- /dev/null
@@ -0,0 +1,89 @@
+package net.pterodactylus.sone.main;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.startsWith;
+import static org.mockito.Mockito.mock;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.StringWriter;
+import java.net.URI;
+import java.net.URISyntaxException;
+
+import net.pterodactylus.sone.web.page.FreenetRequest;
+import net.pterodactylus.util.template.Template;
+import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.template.TemplateProvider;
+import net.pterodactylus.util.web.Method;
+import net.pterodactylus.util.web.Page;
+import net.pterodactylus.util.web.Response;
+
+import freenet.clients.http.ToadletContext;
+import freenet.support.api.HTTPRequest;
+
+import com.google.common.base.Charsets;
+import com.google.common.io.Files;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+/**
+ * Unit test for {@link DebugLoaders}.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class DebugLoadersTest {
+
+       @Rule
+       public final TemporaryFolder temporaryFolder = new TemporaryFolder();
+
+       private final StringWriter stringWriter = new StringWriter();
+       private final TemplateContext templateContext = new TemplateContext();
+       private Loaders loaders;
+
+       @Before
+       public void setupLoader() throws IOException {
+               String templatePath = temporaryFolder.newFolder("temps").getPath();
+               loaders = new DebugLoaders(templatePath);
+               File templateFile = new File(templatePath, "template.txt");
+               Files.write("<%if foo>foo<%else>bar<%/if>", templateFile, Charsets.UTF_8);
+               new File(templatePath, "templates").mkdir();
+               File secondTemplateFile = new File(templatePath, "templates/template.txt");
+               Files.write("<%if foo>foo<%else>bar<%/if>", secondTemplateFile, Charsets.UTF_8);
+       }
+
+       @Test
+       public void debugLoaderCanLoadTemplatesFromFilesystem() throws IOException {
+               Template template = loaders.loadTemplate("/template.txt");
+               template.render(templateContext, stringWriter);
+               assertThat(stringWriter.toString(), is("bar"));
+       }
+
+       @Test
+       public void staticPageIsServedFromFilesystem() throws URISyntaxException, IOException {
+               Page<FreenetRequest> page = loaders.loadStaticPage("text/", "", "text/plain");
+               URI uri = new URI("http://some.host/text/template.txt");
+               Method method = Method.GET;
+               HTTPRequest httpRequest = mock(HTTPRequest.class);
+               ToadletContext toadletContext = mock(ToadletContext.class);
+               FreenetRequest request = new FreenetRequest(uri, method, httpRequest, toadletContext);
+               OutputStream outputStream = new ByteArrayOutputStream();
+               Response response = new Response(outputStream);
+               page.handleRequest(request, response);
+               assertThat(response.getContentType(), startsWith("text/plain"));
+               assertThat(response.getStatusCode(), is(200));
+       }
+
+       @Test
+       public void templateProviderLocatesTemplatesInFileSystem() {
+               TemplateProvider templateProvider = loaders.getTemplateProvider();
+               Template template = templateProvider.getTemplate(templateContext, "template.txt");
+               template.render(templateContext, stringWriter);
+               assertThat(stringWriter.toString(), is("bar"));
+       }
+
+}
diff --git a/src/test/java/net/pterodactylus/sone/main/DefaultLoadersTest.java b/src/test/java/net/pterodactylus/sone/main/DefaultLoadersTest.java
new file mode 100644 (file)
index 0000000..0f0a9f1
--- /dev/null
@@ -0,0 +1,69 @@
+package net.pterodactylus.sone.main;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.notNullValue;
+import static org.hamcrest.Matchers.startsWith;
+import static org.mockito.Mockito.mock;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.StringWriter;
+import java.net.URI;
+import java.net.URISyntaxException;
+
+import net.pterodactylus.sone.web.page.FreenetRequest;
+import net.pterodactylus.util.template.Template;
+import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.template.TemplateProvider;
+import net.pterodactylus.util.web.Method;
+import net.pterodactylus.util.web.Page;
+import net.pterodactylus.util.web.Response;
+
+import freenet.clients.http.ToadletContext;
+import freenet.support.api.HTTPRequest;
+
+import org.junit.Test;
+
+/**
+ * Unit test for {@link DefaultLoaders}.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class DefaultLoadersTest {
+
+       private final Loaders loaders = new DefaultLoaders();
+       private final StringWriter stringWriter = new StringWriter();
+       private final TemplateContext templateContext = new TemplateContext();
+
+       @Test
+       public void templateCanBeLoadedFromTheClasspath() {
+               Template template = loaders.loadTemplate("/net/pterodactylus/sone/main/template.txt");
+               template.render(templateContext, stringWriter);
+               assertThat(stringWriter.toString(), is("Template. bar\n"));
+       }
+
+       @Test
+       public void staticPageIsServedFromClasspath() throws IOException, URISyntaxException {
+               Page<FreenetRequest> staticPage = loaders.loadStaticPage("text/", "/net/pterodactylus/sone/main/", "text/plain");
+               URI uri = new URI("http://some.host/text/template.txt");
+               Method method = Method.GET;
+               HTTPRequest httpRequest = mock(HTTPRequest.class);
+               ToadletContext toadletContext = mock(ToadletContext.class);
+               FreenetRequest request = new FreenetRequest(uri, method, httpRequest, toadletContext);
+               OutputStream outputStream = new ByteArrayOutputStream();
+               Response response = new Response(outputStream);
+               staticPage.handleRequest(request, response);
+               assertThat(response.getContentType(), startsWith("text/plain"));
+               assertThat(response.getStatusCode(), is(200));
+       }
+
+       @Test
+       public void templateIsLocatedInClasspath() {
+               TemplateProvider templateProvider = loaders.getTemplateProvider();
+               Template template = templateProvider.getTemplate(templateContext, "about.html");
+               assertThat(template, notNullValue());
+       }
+
+}
diff --git a/src/test/java/net/pterodactylus/sone/template/FilesystemTemplateTest.java b/src/test/java/net/pterodactylus/sone/template/FilesystemTemplateTest.java
new file mode 100644 (file)
index 0000000..cd989eb
--- /dev/null
@@ -0,0 +1,111 @@
+package net.pterodactylus.sone.template;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.util.Arrays;
+import java.util.concurrent.atomic.AtomicReference;
+
+import net.pterodactylus.util.template.Part;
+import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.template.TemplateException;
+
+import com.google.common.base.Charsets;
+import com.google.common.io.Files;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Unit test for {@link FilesystemTemplate}.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class FilesystemTemplateTest {
+
+       private final File tempFile;
+       private final FilesystemTemplate filesystemTemplate;
+       private final AtomicReference<StringWriter> stringWriter = new AtomicReference<StringWriter>(new StringWriter());
+       private final TemplateContext templateContext = new TemplateContext();
+
+       public FilesystemTemplateTest() throws IOException {
+               tempFile = File.createTempFile("template-", ".dat");
+               writeTemplate("Text");
+               filesystemTemplate = new FilesystemTemplate(tempFile.getAbsolutePath());
+       }
+
+       private void writeTemplate(String text) throws IOException {
+               Files.write(text + ".<%foreach values value><% value><%/foreach>", tempFile, Charsets.UTF_8);
+       }
+
+       @Before
+       public void setupTemplateContext() {
+               templateContext.set("values", Arrays.asList("a", 1));
+       }
+
+       @Test(expected = FilesystemTemplate.TemplateFileNotFoundException.class)
+       public void loadingTemplateFromNonExistingFileThrowsException() throws IOException {
+               FilesystemTemplate filesystemTemplate = new FilesystemTemplate("/a/b/c.dat");
+               filesystemTemplate.getInitialContext();
+       }
+
+       @Test
+       public void templateCanBeLoadedFromTheFilesystem() {
+               filesystemTemplate.render(templateContext, stringWriter.get());
+               assertThat(getRenderedString(), is("Text.a1"));
+       }
+
+       @Test
+       public void templateCanBeReloaded() throws IOException, InterruptedException {
+               filesystemTemplate.render(templateContext, stringWriter.get());
+               assertThat(getRenderedString(), is("Text.a1"));
+               Thread.sleep(1000);
+               writeTemplate("New");
+               filesystemTemplate.render(templateContext, stringWriter.get());
+               assertThat(getRenderedString(), is("New.a1"));
+       }
+
+       @Test
+       public void templateIsNotReloadedIfNotChanged() {
+               filesystemTemplate.render(templateContext, stringWriter.get());
+               assertThat(getRenderedString(), is("Text.a1"));
+               filesystemTemplate.render(templateContext, stringWriter.get());
+               assertThat(getRenderedString(), is("Text.a1"));
+       }
+
+       private String getRenderedString() {
+               String renderedString = stringWriter.get().toString();
+               stringWriter.set(new StringWriter());
+               return renderedString;
+       }
+
+       @Test
+       public void initialContextIsCopiedToReloadedTemplates() throws IOException, InterruptedException {
+               filesystemTemplate.getInitialContext().set("values", "test");
+               Thread.sleep(1000);
+               writeTemplate("New");
+               assertThat(filesystemTemplate.getInitialContext().get("values"), is((Object) "test"));
+       }
+
+       @Test
+       public void partsAreCopiedToReloadedTemplates() throws InterruptedException, IOException {
+               filesystemTemplate.add(new Part() {
+                       @Override
+                       public void render(TemplateContext templateContext, Writer writer) throws TemplateException {
+                               try {
+                                       writer.write(".Test");
+                               } catch (IOException e) {
+                                       throw new TemplateException(e);
+                               }
+                       }
+               });
+               Thread.sleep(1000);
+               writeTemplate("New");
+               filesystemTemplate.render(templateContext, stringWriter.get());
+               assertThat(getRenderedString(), is("New.a1.Test"));
+       }
+
+}
diff --git a/src/test/java/net/pterodactylus/sone/template/ImageLinkFilterTest.java b/src/test/java/net/pterodactylus/sone/template/ImageLinkFilterTest.java
new file mode 100644 (file)
index 0000000..20d2362
--- /dev/null
@@ -0,0 +1,162 @@
+package net.pterodactylus.sone.template;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.nullValue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import net.pterodactylus.sone.core.Core;
+import net.pterodactylus.sone.data.Image;
+import net.pterodactylus.util.template.HtmlFilter;
+import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.template.TemplateContextFactory;
+
+import com.google.common.collect.ImmutableMap;
+import org.hamcrest.Matchers;
+import org.jsoup.Jsoup;
+import org.jsoup.nodes.Document;
+import org.jsoup.nodes.Element;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Unit test for {@link ImageLinkFilterTest}.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class ImageLinkFilterTest {
+
+       private final Core core = mock(Core.class);
+       private final TemplateContextFactory templateContextFactory = new TemplateContextFactory();
+       private final ImageLinkFilter imageLinkFilter = new ImageLinkFilter(core, templateContextFactory);
+       private final TemplateContext templateContext = null;
+       private final Image image = mock(Image.class);
+
+       @Before
+       public void setupTemplateContextFactory() {
+               templateContextFactory.addFilter("html", new HtmlFilter());
+       }
+
+       @Before
+       public void setupCore() {
+               when(core.getImage("image-id", false)).thenReturn(image);
+       }
+
+       @Before
+       public void setupImage() {
+               when(image.getId()).thenReturn("image-id");
+               when(image.getKey()).thenReturn("image-key");
+               when(image.isInserted()).thenReturn(true);
+               when(image.getWidth()).thenReturn(640);
+               when(image.getHeight()).thenReturn(270);
+               when(image.getTitle()).thenReturn("image title");
+               when(image.getDescription()).thenReturn("image description");
+       }
+
+       @Test
+       public void imageLinkIsGeneratedCorrectlyForNotInsertedImages() {
+               when(image.isInserted()).thenReturn(false);
+               String result = String.valueOf(imageLinkFilter.format(templateContext, image, ImmutableMap.<String, Object>of()));
+               Element imageElement = getSingleElement(result);
+               assertThat(imageElement.attr("class"), is(""));
+               assertThat(imageElement.attr("src"), is("getImage.html?image=image-id"));
+               assertThat(imageElement.attr("title"), is("image title"));
+               assertThat(imageElement.attr("alt"), is("image description"));
+               assertThat(imageElement.attr("width"), is("640"));
+               assertThat(imageElement.attr("height"), is("270"));
+       }
+
+       @Test
+       public void imageLinkIsGeneratedCorrectlyForInsertedImages() {
+               String result = String.valueOf(imageLinkFilter.format(templateContext, image, ImmutableMap.<String, Object>of()));
+               Element imageElement = getSingleElement(result);
+               assertThat(imageElement.attr("class"), is(""));
+               assertThat(imageElement.attr("src"), is("/image-key?forcedownload=true"));
+               assertThat(imageElement.attr("title"), is("image title"));
+               assertThat(imageElement.attr("alt"), is("image description"));
+               assertThat(imageElement.attr("width"), is("640"));
+               assertThat(imageElement.attr("height"), is("270"));
+       }
+
+       @Test
+       public void imageTitleAndDescriptionAreOverriddenCorrectly() {
+               String result = String.valueOf(imageLinkFilter.format(templateContext, image, ImmutableMap.<String, Object>of("title", "Test Title")));
+               Element imageElement = getSingleElement(result);
+               assertThat(imageElement.attr("title"), is("Test Title"));
+               assertThat(imageElement.attr("alt"), is("Test Title"));
+       }
+
+       @Test
+       public void imageIsScaledByWidthCorrectly() {
+               String result = String.valueOf(imageLinkFilter.format(templateContext, image, ImmutableMap.<String, Object>of("max-width", "320")));
+               Element imageElement = getSingleElement(result);
+               assertThat(imageElement.attr("width"), is("320"));
+               assertThat(imageElement.attr("height"), is("135"));
+       }
+
+       @Test
+       public void imageIsScaledByHeightCorrectly() {
+               String result = String.valueOf(imageLinkFilter.format(templateContext, image, ImmutableMap.<String, Object>of("max-height", "135")));
+               Element imageElement = getSingleElement(result);
+               assertThat(imageElement.attr("width"), is("320"));
+               assertThat(imageElement.attr("height"), is("135"));
+       }
+
+       @Test
+       public void wideImageIsEnlargedCorrectly() {
+               String result = String.valueOf(imageLinkFilter.format(templateContext, image,
+                               ImmutableMap.<String, Object>of("mode", "enlarge", "max-width", "100", "max-height", "100")));
+               Element imageElement = getSingleElement(result);
+               assertThat(imageElement.attr("width"), is("237"));
+               assertThat(imageElement.attr("height"), is("100"));
+               assertThat(imageElement.attr("style"), containsString("left: -68px"));
+               assertThat(imageElement.attr("style"), containsString("top: 0px"));
+       }
+
+       @Test
+       public void highImageIsEnlargedCorrectly() {
+               when(image.getWidth()).thenReturn(270);
+               when(image.getHeight()).thenReturn(640);
+               String result = String.valueOf(imageLinkFilter.format(templateContext, image,
+                               ImmutableMap.<String, Object>of("mode", "enlarge", "max-width", "100", "max-height", "100")));
+               Element imageElement = getSingleElement(result);
+               assertThat(imageElement.attr("width"), is("100"));
+               assertThat(imageElement.attr("height"), is("237"));
+               assertThat(imageElement.attr("style"), containsString("left: 0px"));
+               assertThat(imageElement.attr("style"), containsString("top: -68px"));
+       }
+
+       @Test
+       public void nullImageIsReturnedAsNull() {
+               assertThat(imageLinkFilter.format(templateContext, null, null), nullValue());
+       }
+
+       @Test
+       public void stringIsUsedToLoadImageFromCore() {
+               String result = String.valueOf(imageLinkFilter.format(templateContext, "image-id", ImmutableMap.<String, Object>of()));
+               Element imageElement = getSingleElement(result);
+               assertThat(imageElement.attr("class"), is(""));
+               assertThat(imageElement.attr("src"), is("/image-key?forcedownload=true"));
+               assertThat(imageElement.attr("title"), is("image title"));
+               assertThat(imageElement.attr("alt"), is("image description"));
+               assertThat(imageElement.attr("width"), is("640"));
+               assertThat(imageElement.attr("height"), is("270"));
+       }
+
+       private Element getSingleElement(String result) {
+               Document document = Jsoup.parseBodyFragment(result);
+               assertThatBodyHasASingleElement(document);
+               return getSingleElement(document);
+       }
+
+       private void assertThatBodyHasASingleElement(Document document) {
+               assertThat(document.body().select("> *"), Matchers.hasSize(1));
+       }
+
+       private Element getSingleElement(Document document) {
+               return document.body().select("> *").get(0);
+       }
+
+}
diff --git a/src/test/resources/net/pterodactylus/sone/main/template.txt b/src/test/resources/net/pterodactylus/sone/main/template.txt
new file mode 100644 (file)
index 0000000..c2377d2
--- /dev/null
@@ -0,0 +1 @@
+Template. <%if foo>foo<%else>bar<%/if>
diff --git a/template.txt b/template.txt
new file mode 100644 (file)
index 0000000..a26591c
--- /dev/null
@@ -0,0 +1 @@
+<%if foo>foo<%else>bar<%/if>
\ No newline at end of file