Merge branch 'release-0.7' 0.7
authorDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Mon, 26 Sep 2011 20:17:49 +0000 (22:17 +0200)
committerDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Mon, 26 Sep 2011 20:17:49 +0000 (22:17 +0200)
126 files changed:
pom.xml
src/main/java/net/pterodactylus/sone/core/Core.java
src/main/java/net/pterodactylus/sone/core/CoreListener.java
src/main/java/net/pterodactylus/sone/core/CoreListenerManager.java
src/main/java/net/pterodactylus/sone/core/FreenetInterface.java
src/main/java/net/pterodactylus/sone/core/ImageInsertListener.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/core/ImageInserter.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/core/SoneDownloader.java
src/main/java/net/pterodactylus/sone/core/SoneException.java
src/main/java/net/pterodactylus/sone/core/SoneInserter.java
src/main/java/net/pterodactylus/sone/data/Album.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/data/Image.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/data/Sone.java
src/main/java/net/pterodactylus/sone/data/TemporaryImage.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/freenet/wot/WebOfTrustConnector.java
src/main/java/net/pterodactylus/sone/main/SonePlugin.java
src/main/java/net/pterodactylus/sone/template/AlbumAccessor.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/template/GetPagePlugin.java
src/main/java/net/pterodactylus/sone/template/ImageLinkFilter.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/template/ParserFilter.java
src/main/java/net/pterodactylus/sone/template/RequestChangeFilter.java
src/main/java/net/pterodactylus/sone/text/SoneTextParser.java
src/main/java/net/pterodactylus/sone/text/SoneTextParserContext.java
src/main/java/net/pterodactylus/sone/web/AboutPage.java
src/main/java/net/pterodactylus/sone/web/BookmarkPage.java
src/main/java/net/pterodactylus/sone/web/BookmarksPage.java
src/main/java/net/pterodactylus/sone/web/CreateAlbumPage.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/web/CreatePostPage.java
src/main/java/net/pterodactylus/sone/web/CreateReplyPage.java
src/main/java/net/pterodactylus/sone/web/CreateSonePage.java
src/main/java/net/pterodactylus/sone/web/DeleteAlbumPage.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/web/DeleteImagePage.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/web/DeletePostPage.java
src/main/java/net/pterodactylus/sone/web/DeleteProfileFieldPage.java
src/main/java/net/pterodactylus/sone/web/DeleteReplyPage.java
src/main/java/net/pterodactylus/sone/web/DeleteSonePage.java
src/main/java/net/pterodactylus/sone/web/DismissNotificationPage.java
src/main/java/net/pterodactylus/sone/web/DistrustPage.java
src/main/java/net/pterodactylus/sone/web/EditAlbumPage.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/web/EditImagePage.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/web/EditProfileFieldPage.java
src/main/java/net/pterodactylus/sone/web/EditProfilePage.java
src/main/java/net/pterodactylus/sone/web/FollowSonePage.java
src/main/java/net/pterodactylus/sone/web/GetImagePage.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/web/ImageBrowserPage.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/web/IndexPage.java
src/main/java/net/pterodactylus/sone/web/KnownSonesPage.java
src/main/java/net/pterodactylus/sone/web/LikePage.java
src/main/java/net/pterodactylus/sone/web/LockSonePage.java
src/main/java/net/pterodactylus/sone/web/LoginPage.java
src/main/java/net/pterodactylus/sone/web/LogoutPage.java
src/main/java/net/pterodactylus/sone/web/MarkAsKnownPage.java
src/main/java/net/pterodactylus/sone/web/OptionsPage.java
src/main/java/net/pterodactylus/sone/web/RescuePage.java
src/main/java/net/pterodactylus/sone/web/SearchPage.java
src/main/java/net/pterodactylus/sone/web/SoneTemplatePage.java
src/main/java/net/pterodactylus/sone/web/TrustPage.java
src/main/java/net/pterodactylus/sone/web/UnbookmarkPage.java
src/main/java/net/pterodactylus/sone/web/UnfollowSonePage.java
src/main/java/net/pterodactylus/sone/web/UnlikePage.java
src/main/java/net/pterodactylus/sone/web/UnlockSonePage.java
src/main/java/net/pterodactylus/sone/web/UntrustPage.java
src/main/java/net/pterodactylus/sone/web/UploadImagePage.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/web/ViewPostPage.java
src/main/java/net/pterodactylus/sone/web/ViewSonePage.java
src/main/java/net/pterodactylus/sone/web/WebInterface.java
src/main/java/net/pterodactylus/sone/web/ajax/BookmarkAjaxPage.java
src/main/java/net/pterodactylus/sone/web/ajax/CreatePostAjaxPage.java
src/main/java/net/pterodactylus/sone/web/ajax/CreateReplyAjaxPage.java
src/main/java/net/pterodactylus/sone/web/ajax/DeletePostAjaxPage.java
src/main/java/net/pterodactylus/sone/web/ajax/DeleteProfileFieldAjaxPage.java
src/main/java/net/pterodactylus/sone/web/ajax/DeleteReplyAjaxPage.java
src/main/java/net/pterodactylus/sone/web/ajax/DismissNotificationAjaxPage.java
src/main/java/net/pterodactylus/sone/web/ajax/DistrustAjaxPage.java
src/main/java/net/pterodactylus/sone/web/ajax/EditAlbumAjaxPage.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/web/ajax/EditImageAjaxPage.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/web/ajax/EditProfileFieldAjaxPage.java
src/main/java/net/pterodactylus/sone/web/ajax/FollowSoneAjaxPage.java
src/main/java/net/pterodactylus/sone/web/ajax/GetLikesAjaxPage.java
src/main/java/net/pterodactylus/sone/web/ajax/GetNotificationAjaxPage.java
src/main/java/net/pterodactylus/sone/web/ajax/GetPostAjaxPage.java
src/main/java/net/pterodactylus/sone/web/ajax/GetReplyAjaxPage.java
src/main/java/net/pterodactylus/sone/web/ajax/GetStatusAjaxPage.java
src/main/java/net/pterodactylus/sone/web/ajax/GetTimesAjaxPage.java
src/main/java/net/pterodactylus/sone/web/ajax/GetTranslationPage.java
src/main/java/net/pterodactylus/sone/web/ajax/JsonPage.java
src/main/java/net/pterodactylus/sone/web/ajax/LikeAjaxPage.java
src/main/java/net/pterodactylus/sone/web/ajax/LockSoneAjaxPage.java
src/main/java/net/pterodactylus/sone/web/ajax/MarkAsKnownAjaxPage.java
src/main/java/net/pterodactylus/sone/web/ajax/MoveProfileFieldAjaxPage.java
src/main/java/net/pterodactylus/sone/web/ajax/TrustAjaxPage.java
src/main/java/net/pterodactylus/sone/web/ajax/UnbookmarkAjaxPage.java
src/main/java/net/pterodactylus/sone/web/ajax/UnfollowSoneAjaxPage.java
src/main/java/net/pterodactylus/sone/web/ajax/UnlikeAjaxPage.java
src/main/java/net/pterodactylus/sone/web/ajax/UnlockSoneAjaxPage.java
src/main/java/net/pterodactylus/sone/web/ajax/UntrustAjaxPage.java
src/main/java/net/pterodactylus/sone/web/page/FreenetRequest.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/web/page/FreenetTemplatePage.java
src/main/java/net/pterodactylus/sone/web/page/Page.java [deleted file]
src/main/java/net/pterodactylus/sone/web/page/PageToadlet.java
src/main/java/net/pterodactylus/sone/web/page/PageToadletFactory.java
src/main/java/net/pterodactylus/sone/web/page/RedirectPage.java [deleted file]
src/main/java/net/pterodactylus/sone/web/page/StaticPage.java [deleted file]
src/main/java/net/pterodactylus/sone/web/page/TemplatePage.java [deleted file]
src/main/resources/i18n/sone.en.properties
src/main/resources/static/css/sone.css
src/main/resources/static/images/unknown-image-0.png [new file with mode: 0644]
src/main/resources/static/javascript/sone.js
src/main/resources/templates/createAlbum.html [new file with mode: 0644]
src/main/resources/templates/deleteAlbum.html [new file with mode: 0644]
src/main/resources/templates/deleteImage.html [new file with mode: 0644]
src/main/resources/templates/imageBrowser.html [new file with mode: 0644]
src/main/resources/templates/include/browseAlbums.html [new file with mode: 0644]
src/main/resources/templates/include/createAlbum.html [new file with mode: 0644]
src/main/resources/templates/include/soneMenu.html
src/main/resources/templates/include/uploadImage.html [new file with mode: 0644]
src/main/resources/templates/include/viewPost.html
src/main/resources/templates/include/viewReply.html
src/main/resources/templates/insert/include/album.xml [new file with mode: 0644]
src/main/resources/templates/insert/sone.xml
src/main/resources/templates/invalid.html
src/main/resources/templates/notify/image-insert-failed-notification.html [new file with mode: 0644]
src/main/resources/templates/notify/inserted-images-notification.html [new file with mode: 0644]
src/main/resources/templates/notify/inserting-images-notification.html [new file with mode: 0644]
src/main/resources/templates/viewSone.html
src/test/java/net/pterodactylus/sone/text/SoneTextParserTest.java

diff --git a/pom.xml b/pom.xml
index 037d3be..f494135 100644 (file)
--- a/pom.xml
+++ b/pom.xml
@@ -2,12 +2,12 @@
        <modelVersion>4.0.0</modelVersion>
        <groupId>net.pterodactylus</groupId>
        <artifactId>sone</artifactId>
-       <version>0.6.7</version>
+       <version>0.7</version>
        <dependencies>
                <dependency>
                        <groupId>net.pterodactylus</groupId>
                        <artifactId>utils</artifactId>
-                       <version>0.10.0</version>
+                       <version>0.11</version>
                </dependency>
                <dependency>
                        <groupId>junit</groupId>
index c8d7588..8aac5d7 100644 (file)
@@ -34,12 +34,15 @@ import java.util.logging.Logger;
 import net.pterodactylus.sone.core.Options.DefaultOption;
 import net.pterodactylus.sone.core.Options.Option;
 import net.pterodactylus.sone.core.Options.OptionWatcher;
+import net.pterodactylus.sone.data.Album;
 import net.pterodactylus.sone.data.Client;
+import net.pterodactylus.sone.data.Image;
 import net.pterodactylus.sone.data.Post;
 import net.pterodactylus.sone.data.Profile;
-import net.pterodactylus.sone.data.Profile.Field;
 import net.pterodactylus.sone.data.Reply;
 import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.data.TemporaryImage;
+import net.pterodactylus.sone.data.Profile.Field;
 import net.pterodactylus.sone.fcp.FcpInterface;
 import net.pterodactylus.sone.fcp.FcpInterface.FullAccessRequired;
 import net.pterodactylus.sone.freenet.wot.Identity;
@@ -67,7 +70,7 @@ import freenet.keys.FreenetURI;
  *
  * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
  */
-public class Core extends AbstractService implements IdentityListener, UpdateListener, SoneProvider, PostProvider, SoneInsertListener {
+public class Core extends AbstractService implements IdentityListener, UpdateListener, SoneProvider, PostProvider, SoneInsertListener, ImageInsertListener {
 
        /**
         * Enumeration for the possible states of a {@link Sone}.
@@ -116,6 +119,9 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis
        /** The Sone downloader. */
        private final SoneDownloader soneDownloader;
 
+       /** The image inserter. */
+       private final ImageInserter imageInserter;
+
        /** Sone downloader thread-pool. */
        private final ExecutorService soneDownloaders = Executors.newFixedThreadPool(10);
 
@@ -182,6 +188,15 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis
        /** Trusted identities, sorted by own identities. */
        private Map<OwnIdentity, Set<Identity>> trustedIdentities = Collections.synchronizedMap(new HashMap<OwnIdentity, Set<Identity>>());
 
+       /** All known albums. */
+       private Map<String, Album> albums = new HashMap<String, Album>();
+
+       /** All known images. */
+       private Map<String, Image> images = new HashMap<String, Image>();
+
+       /** All temporary images. */
+       private Map<String, TemporaryImage> temporaryImages = new HashMap<String, TemporaryImage>();
+
        /** Ticker for threads that mark own elements as known. */
        private Ticker localElementTicker = new Ticker();
 
@@ -204,6 +219,7 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis
                this.freenetInterface = freenetInterface;
                this.identityManager = identityManager;
                this.soneDownloader = new SoneDownloader(this, freenetInterface);
+               this.imageInserter = new ImageInserter(this, freenetInterface);
                this.updateChecker = new UpdateChecker(freenetInterface);
        }
 
@@ -800,6 +816,89 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis
                return posts;
        }
 
+       /**
+        * Returns the album with the given ID, creating a new album if no album
+        * with the given ID can be found.
+        *
+        * @param albumId
+        *            The ID of the album
+        * @return The album with the given ID
+        */
+       public Album getAlbum(String albumId) {
+               return getAlbum(albumId, true);
+       }
+
+       /**
+        * Returns the album with the given ID, optionally creating a new album if
+        * an album with the given ID can not be found.
+        *
+        * @param albumId
+        *            The ID of the album
+        * @param create
+        *            {@code true} to create a new album if none exists for the
+        *            given ID
+        * @return The album with the given ID, or {@code null} if no album with the
+        *         given ID exists and {@code create} is {@code false}
+        */
+       public Album getAlbum(String albumId, boolean create) {
+               synchronized (albums) {
+                       Album album = albums.get(albumId);
+                       if (create && (album == null)) {
+                               album = new Album(albumId);
+                               albums.put(albumId, album);
+                       }
+                       return album;
+               }
+       }
+
+       /**
+        * Returns the image with the given ID, creating it if necessary.
+        *
+        * @param imageId
+        *            The ID of the image
+        * @return The image with the given ID
+        */
+       public Image getImage(String imageId) {
+               return getImage(imageId, true);
+       }
+
+       /**
+        * Returns the image with the given ID, optionally creating it if it does
+        * not exist.
+        *
+        * @param imageId
+        *            The ID of the image
+        * @param create
+        *            {@code true} to create an image if none exists with the given
+        *            ID
+        * @return The image with the given ID, or {@code null} if none exists and
+        *         none was created
+        */
+       public Image getImage(String imageId, boolean create) {
+               synchronized (images) {
+                       Image image = images.get(imageId);
+                       if (create && (image == null)) {
+                               image = new Image(imageId);
+                               images.put(imageId, image);
+                       }
+                       return image;
+               }
+       }
+
+       /**
+        * Returns the temporary image with the given ID.
+        *
+        * @param imageId
+        *            The ID of the temporary image
+        * @return The temporary image, or {@code null} if there is no temporary
+        *         image with the given ID
+        */
+       public TemporaryImage getTemporaryImage(String imageId) {
+               synchronized (temporaryImages) {
+                       return temporaryImages.get(imageId);
+               }
+       }
+
        //
        // ACTIONS
        //
@@ -1124,6 +1223,22 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis
                                        }
                                }
                        }
+                       synchronized (albums) {
+                               synchronized (images) {
+                                       for (Album album : storedSone.getAlbums()) {
+                                               albums.remove(album.getId());
+                                               for (Image image : album.getImages()) {
+                                                       images.remove(image.getId());
+                                               }
+                                       }
+                                       for (Album album : sone.getAlbums()) {
+                                               albums.put(album.getId(), album);
+                                               for (Image image : album.getImages()) {
+                                                       images.put(image.getId(), image);
+                                               }
+                                       }
+                               }
+                       }
                        synchronized (storedSone) {
                                if (!soneRescueMode || (sone.getTime() > storedSone.getTime())) {
                                        storedSone.setTime(sone.getTime());
@@ -1143,11 +1258,15 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis
                                        for (String likedReplyId : sone.getLikedReplyIds()) {
                                                storedSone.addLikedReplyId(likedReplyId);
                                        }
+                                       for (Album album : sone.getAlbums()) {
+                                               storedSone.addAlbum(album);
+                                       }
                                } else {
                                        storedSone.setPosts(sone.getPosts());
                                        storedSone.setReplies(sone.getReplies());
                                        storedSone.setLikePostIds(sone.getLikedPostIds());
                                        storedSone.setLikeReplyIds(sone.getLikedReplyIds());
+                                       storedSone.setAlbums(sone.getAlbums());
                                }
                                storedSone.setLatestEdition(sone.getLatestEdition());
                        }
@@ -1323,6 +1442,65 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis
                        friends.add(friendId);
                }
 
+               /* load albums. */
+               List<Album> topLevelAlbums = new ArrayList<Album>();
+               int albumCounter = 0;
+               while (true) {
+                       String albumPrefix = sonePrefix + "/Albums/" + albumCounter++;
+                       String albumId = configuration.getStringValue(albumPrefix + "/ID").getValue(null);
+                       if (albumId == null) {
+                               break;
+                       }
+                       String albumTitle = configuration.getStringValue(albumPrefix + "/Title").getValue(null);
+                       String albumDescription = configuration.getStringValue(albumPrefix + "/Description").getValue(null);
+                       String albumParentId = configuration.getStringValue(albumPrefix + "/Parent").getValue(null);
+                       String albumImageId = configuration.getStringValue(albumPrefix + "/AlbumImage").getValue(null);
+                       if ((albumTitle == null) || (albumDescription == null)) {
+                               logger.log(Level.WARNING, "Invalid album found, aborting load!");
+                               return;
+                       }
+                       Album album = getAlbum(albumId).setSone(sone).setTitle(albumTitle).setDescription(albumDescription).setAlbumImage(albumImageId);
+                       if (albumParentId != null) {
+                               Album parentAlbum = getAlbum(albumParentId, false);
+                               if (parentAlbum == null) {
+                                       logger.log(Level.WARNING, "Invalid parent album ID: " + albumParentId);
+                                       return;
+                               }
+                               parentAlbum.addAlbum(album);
+                       } else {
+                               topLevelAlbums.add(album);
+                       }
+               }
+
+               /* load images. */
+               int imageCounter = 0;
+               while (true) {
+                       String imagePrefix = sonePrefix + "/Images/" + imageCounter++;
+                       String imageId = configuration.getStringValue(imagePrefix + "/ID").getValue(null);
+                       if (imageId == null) {
+                               break;
+                       }
+                       String albumId = configuration.getStringValue(imagePrefix + "/Album").getValue(null);
+                       String key = configuration.getStringValue(imagePrefix + "/Key").getValue(null);
+                       String title = configuration.getStringValue(imagePrefix + "/Title").getValue(null);
+                       String description = configuration.getStringValue(imagePrefix + "/Description").getValue(null);
+                       Long creationTime = configuration.getLongValue(imagePrefix + "/CreationTime").getValue(null);
+                       Integer width = configuration.getIntValue(imagePrefix + "/Width").getValue(null);
+                       Integer height = configuration.getIntValue(imagePrefix + "/Height").getValue(null);
+                       if ((albumId == null) || (key == null) || (title == null) || (description == null) || (creationTime == null) || (width == null) || (height == null)) {
+                               logger.log(Level.WARNING, "Invalid image found, aborting load!");
+                               return;
+                       }
+                       Album album = getAlbum(albumId, false);
+                       if (album == null) {
+                               logger.log(Level.WARNING, "Invalid album image encountered, aborting load!");
+                               return;
+                       }
+                       Image image = getImage(imageId).setSone(sone).setCreationTime(creationTime).setKey(key);
+                       image.setTitle(title).setDescription(description).setWidth(width).setHeight(height);
+                       album.addImage(image);
+               }
+
                /* load options. */
                sone.getOptions().getBooleanOption("AutoFollow").set(configuration.getBooleanValue(sonePrefix + "/Options/AutoFollow").getValue(null));
                sone.getOptions().getBooleanOption("EnableSoneInsertNotifications").set(configuration.getBooleanValue(sonePrefix + "/Options/EnableSoneInsertNotifications").getValue(null));
@@ -1336,6 +1514,7 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis
                        sone.setLikePostIds(likedPostIds);
                        sone.setLikeReplyIds(likedReplyIds);
                        sone.setFriends(friends);
+                       sone.setAlbums(topLevelAlbums);
                        soneInserters.get(sone).setLastInsertFingerprint(lastInsertFingerprint);
                }
                synchronized (newSones) {
@@ -1625,6 +1804,150 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis
        }
 
        /**
+        * Creates a new top-level album for the given Sone.
+        *
+        * @param sone
+        *            The Sone to create the album for
+        * @return The new album
+        */
+       public Album createAlbum(Sone sone) {
+               return createAlbum(sone, null);
+       }
+
+       /**
+        * Creates a new album for the given Sone.
+        *
+        * @param sone
+        *            The Sone to create the album for
+        * @param parent
+        *            The parent of the album (may be {@code null} to create a
+        *            top-level album)
+        * @return The new album
+        */
+       public Album createAlbum(Sone sone, Album parent) {
+               Album album = new Album();
+               synchronized (albums) {
+                       albums.put(album.getId(), album);
+               }
+               album.setSone(sone);
+               if (parent != null) {
+                       parent.addAlbum(album);
+               } else {
+                       sone.addAlbum(album);
+               }
+               return album;
+       }
+
+       /**
+        * Deletes the given album. The owner of the album has to be a local Sone,
+        * and the album has to be {@link Album#isEmpty() empty} to be deleted.
+        *
+        * @param album
+        *            The album to remove
+        */
+       public void deleteAlbum(Album album) {
+               Validation.begin().isNotNull("Album", album).check().is("Local Sone", isLocalSone(album.getSone())).check();
+               if (!album.isEmpty()) {
+                       return;
+               }
+               if (album.getParent() == null) {
+                       album.getSone().removeAlbum(album);
+               } else {
+                       album.getParent().removeAlbum(album);
+               }
+               synchronized (albums) {
+                       albums.remove(album.getId());
+               }
+               saveSone(album.getSone());
+       }
+
+       /**
+        * Creates a new image.
+        *
+        * @param sone
+        *            The Sone creating the image
+        * @param album
+        *            The album the image will be inserted into
+        * @param temporaryImage
+        *            The temporary image to create the image from
+        * @return The newly created image
+        */
+       public Image createImage(Sone sone, Album album, TemporaryImage temporaryImage) {
+               Validation.begin().isNotNull("Sone", sone).isNotNull("Album", album).isNotNull("Temporary Image", temporaryImage).check().is("Local Sone", isLocalSone(sone)).check().isEqual("Owner and Album Owner", sone, album.getSone()).check();
+               Image image = new Image(temporaryImage.getId()).setSone(sone).setCreationTime(System.currentTimeMillis());
+               album.addImage(image);
+               synchronized (images) {
+                       images.put(image.getId(), image);
+               }
+               imageInserter.insertImage(temporaryImage, image);
+               return image;
+       }
+
+       /**
+        * Deletes the given image. This method will also delete a matching
+        * temporary image.
+        *
+        * @see #deleteTemporaryImage(TemporaryImage)
+        * @param image
+        *            The image to delete
+        */
+       public void deleteImage(Image image) {
+               Validation.begin().isNotNull("Image", image).check().is("Local Sone", isLocalSone(image.getSone())).check();
+               deleteTemporaryImage(image.getId());
+               image.getAlbum().removeImage(image);
+               synchronized (images) {
+                       images.remove(image.getId());
+               }
+               saveSone(image.getSone());
+       }
+
+       /**
+        * Creates a new temporary image.
+        *
+        * @param mimeType
+        *            The MIME type of the temporary image
+        * @param imageData
+        *            The encoded data of the image
+        * @return The temporary image
+        */
+       public TemporaryImage createTemporaryImage(String mimeType, byte[] imageData) {
+               TemporaryImage temporaryImage = new TemporaryImage();
+               temporaryImage.setMimeType(mimeType).setImageData(imageData);
+               synchronized (temporaryImages) {
+                       temporaryImages.put(temporaryImage.getId(), temporaryImage);
+               }
+               return temporaryImage;
+       }
+
+       /**
+        * Deletes the given temporary image.
+        *
+        * @param temporaryImage
+        *            The temporary image to delete
+        */
+       public void deleteTemporaryImage(TemporaryImage temporaryImage) {
+               Validation.begin().isNotNull("Temporary Image", temporaryImage).check();
+               deleteTemporaryImage(temporaryImage.getId());
+       }
+
+       /**
+        * Deletes the temporary image with the given ID.
+        *
+        * @param imageId
+        *            The ID of the temporary image to delete
+        */
+       public void deleteTemporaryImage(String imageId) {
+               Validation.begin().isNotNull("Temporary Image ID", imageId).check();
+               synchronized (temporaryImages) {
+                       temporaryImages.remove(imageId);
+               }
+               Image image = getImage(imageId, false);
+               if (image != null) {
+                       imageInserter.cancelImageInsert(image);
+               }
+       }
+
+       /**
         * Notifies the core that the configuration, either of the core or of a
         * single local Sone, has changed, and that the configuration should be
         * saved.
@@ -1773,6 +2096,40 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis
                        }
                        configuration.getStringValue(sonePrefix + "/Friends/" + friendCounter + "/ID").setValue(null);
 
+                       /* save albums. first, collect in a flat structure, top-level first. */
+                       List<Album> albums = Sone.flattenAlbums(sone.getAlbums());
+
+                       int albumCounter = 0;
+                       for (Album album : albums) {
+                               String albumPrefix = sonePrefix + "/Albums/" + albumCounter++;
+                               configuration.getStringValue(albumPrefix + "/ID").setValue(album.getId());
+                               configuration.getStringValue(albumPrefix + "/Title").setValue(album.getTitle());
+                               configuration.getStringValue(albumPrefix + "/Description").setValue(album.getDescription());
+                               configuration.getStringValue(albumPrefix + "/Parent").setValue(album.getParent() == null ? null : album.getParent().getId());
+                               configuration.getStringValue(albumPrefix + "/AlbumImage").setValue(album.getAlbumImage() == null ? null : album.getAlbumImage().getId());
+                       }
+                       configuration.getStringValue(sonePrefix + "/Albums/" + albumCounter + "/ID").setValue(null);
+
+                       /* save images. */
+                       int imageCounter = 0;
+                       for (Album album : albums) {
+                               for (Image image : album.getImages()) {
+                                       if (!image.isInserted()) {
+                                               continue;
+                                       }
+                                       String imagePrefix = sonePrefix + "/Images/" + imageCounter++;
+                                       configuration.getStringValue(imagePrefix + "/ID").setValue(image.getId());
+                                       configuration.getStringValue(imagePrefix + "/Album").setValue(album.getId());
+                                       configuration.getStringValue(imagePrefix + "/Key").setValue(image.getKey());
+                                       configuration.getStringValue(imagePrefix + "/Title").setValue(image.getTitle());
+                                       configuration.getStringValue(imagePrefix + "/Description").setValue(image.getDescription());
+                                       configuration.getLongValue(imagePrefix + "/CreationTime").setValue(image.getCreationTime());
+                                       configuration.getIntValue(imagePrefix + "/Width").setValue(image.getWidth());
+                                       configuration.getIntValue(imagePrefix + "/Height").setValue(image.getHeight());
+                               }
+                       }
+                       configuration.getStringValue(sonePrefix + "/Images/" + imageCounter + "/ID").setValue(null);
+
                        /* save options. */
                        configuration.getBooleanValue(sonePrefix + "/Options/AutoFollow").setValue(sone.getOptions().getBooleanOption("AutoFollow").getReal());
                        configuration.getBooleanValue(sonePrefix + "/Options/EnableSoneInsertNotifications").setValue(sone.getOptions().getBooleanOption("EnableSoneInsertNotifications").getReal());
@@ -2125,12 +2482,13 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis
        }
 
        //
-       // SONEINSERTLISTENER METHODS
+       // INTERFACE ImageInsertListener
        //
 
        /**
         * {@inheritDoc}
         */
+       @Override
        public void insertStarted(Sone sone) {
                coreListenerManager.fireSoneInserting(sone);
        }
@@ -2151,6 +2509,49 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis
                coreListenerManager.fireSoneInsertAborted(sone, cause);
        }
 
+       //
+       // SONEINSERTLISTENER METHODS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public void imageInsertStarted(Image image) {
+               logger.log(Level.WARNING, "Image insert started for " + image);
+               coreListenerManager.fireImageInsertStarted(image);
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public void imageInsertAborted(Image image) {
+               logger.log(Level.WARNING, "Image insert aborted for " + image);
+               coreListenerManager.fireImageInsertAborted(image);
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public void imageInsertFinished(Image image, FreenetURI key) {
+               logger.log(Level.WARNING, "Image insert finished for " + image + ": " + key);
+               image.setKey(key.toString());
+               deleteTemporaryImage(image.getId());
+               saveSone(image.getSone());
+               coreListenerManager.fireImageInsertFinished(image);
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public void imageInsertFailed(Image image, Throwable cause) {
+               logger.log(Level.WARNING, "Image insert failed for " + image, cause);
+               coreListenerManager.fireImageInsertFailed(image, cause);
+       }
+
        /**
         * Convenience interface for external classes that want to access the core’s
         * configuration.
index d5120ac..1658745 100644 (file)
@@ -19,6 +19,7 @@ package net.pterodactylus.sone.core;
 
 import java.util.EventListener;
 
+import net.pterodactylus.sone.data.Image;
 import net.pterodactylus.sone.data.Post;
 import net.pterodactylus.sone.data.Reply;
 import net.pterodactylus.sone.data.Sone;
@@ -164,4 +165,38 @@ public interface CoreListener extends EventListener {
         */
        public void updateFound(Version version, long releaseTime, long latestEdition);
 
+       /**
+        * Notifies a listener that an image has started being inserted.
+        *
+        * @param image
+        *            The image that is now inserted
+        */
+       public void imageInsertStarted(Image image);
+
+       /**
+        * Notifies a listener that an image insert was aborted by the user.
+        *
+        * @param image
+        *            The image that is not inserted anymore
+        */
+       public void imageInsertAborted(Image image);
+
+       /**
+        * Notifies a listener that an image was successfully inserted.
+        *
+        * @param image
+        *            The image that was inserted
+        */
+       public void imageInsertFinished(Image image);
+
+       /**
+        * Notifies a listener that an image failed to be inserted.
+        *
+        * @param image
+        *            The image that could not be inserted
+        * @param cause
+        *            The reason for the failed insert
+        */
+       public void imageInsertFailed(Image image, Throwable cause);
+
 }
index 875a2b6..5748ffc 100644 (file)
@@ -17,6 +17,7 @@
 
 package net.pterodactylus.sone.core;
 
+import net.pterodactylus.sone.data.Image;
 import net.pterodactylus.sone.data.Post;
 import net.pterodactylus.sone.data.Reply;
 import net.pterodactylus.sone.data.Sone;
@@ -246,4 +247,58 @@ public class CoreListenerManager extends AbstractListenerManager<Core, CoreListe
                }
        }
 
+       /**
+        * Notifies all listeners that an image has started being inserted.
+        *
+        * @see CoreListener#imageInsertStarted(Image)
+        * @param image
+        *            The image that is now inserted
+        */
+       void fireImageInsertStarted(Image image) {
+               for (CoreListener coreListener : getListeners()) {
+                       coreListener.imageInsertStarted(image);
+               }
+       }
+
+       /**
+        * Notifies all listeners that an image insert was aborted by the user.
+        *
+        * @see CoreListener#imageInsertAborted(Image)
+        * @param image
+        *            The image that is not inserted anymore
+        */
+       void fireImageInsertAborted(Image image) {
+               for (CoreListener coreListener : getListeners()) {
+                       coreListener.imageInsertAborted(image);
+               }
+       }
+
+       /**
+        * Notifies all listeners that an image was successfully inserted.
+        *
+        * @see CoreListener#imageInsertFinished(Image)
+        * @param image
+        *            The image that was inserted
+        */
+       void fireImageInsertFinished(Image image) {
+               for (CoreListener coreListener : getListeners()) {
+                       coreListener.imageInsertFinished(image);
+               }
+       }
+
+       /**
+        * Notifies all listeners that an image failed to be inserted.
+        *
+        * @see CoreListener#imageInsertFailed(Image, Throwable)
+        * @param image
+        *            The image that could not be inserted
+        * @param cause
+        *            The cause of the failure
+        */
+       void fireImageInsertFailed(Image image, Throwable cause) {
+               for (CoreListener coreListener : getListeners()) {
+                       coreListener.imageInsertFailed(image, cause);
+               }
+       }
+
 }
index 20c8da7..39f342c 100644 (file)
 package net.pterodactylus.sone.core;
 
 import java.net.MalformedURLException;
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 
+import net.pterodactylus.sone.core.SoneException.Type;
+import net.pterodactylus.sone.data.Image;
 import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.data.TemporaryImage;
 import net.pterodactylus.util.collection.Pair;
 import net.pterodactylus.util.logging.Logging;
 
 import com.db4o.ObjectContainer;
 
+import freenet.client.ClientMetadata;
 import freenet.client.FetchException;
 import freenet.client.FetchResult;
 import freenet.client.HighLevelSimpleClient;
 import freenet.client.HighLevelSimpleClientImpl;
+import freenet.client.InsertBlock;
+import freenet.client.InsertContext;
 import freenet.client.InsertException;
+import freenet.client.async.BaseClientPutter;
 import freenet.client.async.ClientContext;
+import freenet.client.async.ClientPutCallback;
+import freenet.client.async.ClientPutter;
 import freenet.client.async.USKCallback;
 import freenet.keys.FreenetURI;
+import freenet.keys.InsertableClientSSK;
 import freenet.keys.USK;
 import freenet.node.Node;
 import freenet.node.RequestStarter;
+import freenet.support.api.Bucket;
+import freenet.support.io.ArrayBucket;
 
 /**
  * Contains all necessary functionality for interacting with the Freenet node.
@@ -115,6 +129,36 @@ public class FreenetInterface {
        }
 
        /**
+        * 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.
+        *
+        * @param temporaryImage
+        *            The temporary image data
+        * @param image
+        *            The image
+        * @param insertToken
+        *            The insert token
+        * @throws SoneException
+        *             if the insert could not be started
+        */
+       public void insertImage(TemporaryImage temporaryImage, Image image, InsertToken insertToken) throws SoneException {
+               String filenameHint = image.getId() + "." + temporaryImage.getMimeType().substring(temporaryImage.getMimeType().lastIndexOf("/") + 1);
+               InsertableClientSSK key = InsertableClientSSK.createRandom(node.random, "");
+               FreenetURI targetUri = key.getInsertURI().setDocName(filenameHint);
+               InsertContext insertContext = client.getInsertContext(true);
+               Bucket bucket = new ArrayBucket(temporaryImage.getImageData());
+               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);
+                       insertToken.setClientPutter(clientPutter);
+               } catch (InsertException ie1) {
+                       throw new SoneException(Type.INSERT_FAILED, "Could not start image insert.", ie1);
+               }
+       }
+
+       /**
         * Inserts a directory into Freenet.
         *
         * @param insertUri
@@ -280,4 +324,148 @@ public class FreenetInterface {
 
        }
 
+       /**
+        * Insert token that can be used to add {@link ImageInsertListener}s and
+        * cancel a running insert.
+        *
+        * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+        */
+       public class InsertToken implements ClientPutCallback {
+
+               /** The image being inserted. */
+               private final Image image;
+
+               /** The list of registered image insert listeners. */
+               private final List<ImageInsertListener> imageInsertListeners = Collections.synchronizedList(new ArrayList<ImageInsertListener>());
+
+               /** The client putter. */
+               private ClientPutter clientPutter;
+
+               /** The final URI. */
+               private volatile FreenetURI resultingUri;
+
+               /**
+                * Creates a new insert token for the given image.
+                *
+                * @param image
+                *            The image being inserted
+                */
+               public InsertToken(Image image) {
+                       this.image = image;
+               }
+
+               //
+               // LISTENER MANAGEMENT
+               //
+
+               /**
+                * Adds the given listener to the list of registered listener.
+                *
+                * @param imageInsertListener
+                *            The listener to add
+                */
+               public void addImageInsertListener(ImageInsertListener imageInsertListener) {
+                       imageInsertListeners.add(imageInsertListener);
+               }
+
+               /**
+                * Removes the given listener from the list of registered listener.
+                *
+                * @param imageInsertListener
+                *            The listener to remove
+                */
+               public void removeImageInsertListener(ImageInsertListener imageInsertListener) {
+                       imageInsertListeners.remove(imageInsertListener);
+               }
+
+               //
+               // ACCESSORS
+               //
+
+               /**
+                * Sets the client putter that is inserting the image. This will also
+                * signal all registered listeners that the image has started.
+                *
+                * @see ImageInsertListener#imageInsertStarted(Image)
+                * @param clientPutter
+                *            The client putter
+                */
+               public void setClientPutter(ClientPutter clientPutter) {
+                       this.clientPutter = clientPutter;
+                       for (ImageInsertListener imageInsertListener : imageInsertListeners) {
+                               imageInsertListener.imageInsertStarted(image);
+                       }
+               }
+
+               //
+               // ACTIONS
+               //
+
+               /**
+                * Cancels the running insert.
+                *
+                * @see ImageInsertListener#imageInsertAborted(Image)
+                */
+               @SuppressWarnings("synthetic-access")
+               public void cancel() {
+                       clientPutter.cancel(null, node.clientCore.clientContext);
+                       for (ImageInsertListener imageInsertListener : imageInsertListeners) {
+                               imageInsertListener.imageInsertAborted(image);
+                       }
+               }
+
+               //
+               // INTERFACE ClientPutCallback
+               //
+
+               /**
+                * {@inheritDoc}
+                */
+               @Override
+               public void onMajorProgress(ObjectContainer objectContainer) {
+                       /* ignore, we don’t care. */
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               @Override
+               public void onFailure(InsertException insertException, BaseClientPutter clientPutter, ObjectContainer objectContainer) {
+                       for (ImageInsertListener imageInsertListener : imageInsertListeners) {
+                               if ((insertException != null) && ("Cancelled by user".equals(insertException.getMessage()))) {
+                                       imageInsertListener.imageInsertAborted(image);
+                               } else {
+                                       imageInsertListener.imageInsertFailed(image, insertException);
+                               }
+                       }
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               @Override
+               public void onFetchable(BaseClientPutter clientPutter, ObjectContainer objectContainer) {
+                       /* ignore, we don’t care. */
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               @Override
+               public void onGeneratedURI(FreenetURI generatedUri, BaseClientPutter clientPutter, ObjectContainer objectContainer) {
+                       resultingUri = generatedUri;
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               @Override
+               public void onSuccess(BaseClientPutter clientPutter, ObjectContainer objectContainer) {
+                       for (ImageInsertListener imageInsertListener : imageInsertListeners) {
+                               imageInsertListener.imageInsertFinished(image, resultingUri);
+                       }
+               }
+
+       }
+
 }
diff --git a/src/main/java/net/pterodactylus/sone/core/ImageInsertListener.java b/src/main/java/net/pterodactylus/sone/core/ImageInsertListener.java
new file mode 100644 (file)
index 0000000..a0e8a56
--- /dev/null
@@ -0,0 +1,76 @@
+/*
+ * Sone - ImageInsertListener.java - Copyright © 2011 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.core;
+
+import java.util.EventListener;
+
+import net.pterodactylus.sone.core.FreenetInterface.InsertToken;
+import net.pterodactylus.sone.data.Image;
+import freenet.keys.FreenetURI;
+
+/**
+ * Listener interface for objects that want to be notified about the status of
+ * an image insert.
+ *
+ * @see ImageInserter#insertImage(net.pterodactylus.sone.data.TemporaryImage,
+ *      Image)
+ * @see FreenetInterface#insertImage(net.pterodactylus.sone.data.TemporaryImage,
+ *      Image, net.pterodactylus.sone.core.FreenetInterface.InsertToken)
+ * @see InsertToken
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public interface ImageInsertListener extends EventListener {
+
+       /**
+        * Notifies a listener that the insert of the given image started.
+        *
+        * @param image
+        *            The image that is being inserted
+        */
+       public void imageInsertStarted(Image image);
+
+       /**
+        * Notifies a listener that the insert of the given image was aborted by the
+        * user.
+        *
+        * @param image
+        *            The image that is no longer being inserted
+        */
+       public void imageInsertAborted(Image image);
+
+       /**
+        * Notifies a listener that the given image was inserted successfully.
+        *
+        * @param image
+        *            The image that was inserted
+        * @param key
+        *            The final key of the image
+        */
+       public void imageInsertFinished(Image image, FreenetURI key);
+
+       /**
+        * Notifies a listener that the given image could not be inserted.
+        *
+        * @param image
+        *            The image that could not be inserted
+        * @param cause
+        *            The cause of the insertion failure
+        */
+       public void imageInsertFailed(Image image, Throwable cause);
+
+}
diff --git a/src/main/java/net/pterodactylus/sone/core/ImageInserter.java b/src/main/java/net/pterodactylus/sone/core/ImageInserter.java
new file mode 100644 (file)
index 0000000..b4421ce
--- /dev/null
@@ -0,0 +1,104 @@
+/*
+ * Sone - ImageInserter.java - Copyright © 2011 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.core;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import net.pterodactylus.sone.core.FreenetInterface.InsertToken;
+import net.pterodactylus.sone.data.Image;
+import net.pterodactylus.sone.data.TemporaryImage;
+import net.pterodactylus.util.logging.Logging;
+import net.pterodactylus.util.validation.Validation;
+
+/**
+ * The image inserter is responsible for inserting images using
+ * {@link FreenetInterface#insertImage(TemporaryImage, Image, InsertToken)} and
+ * also tracks running inserts, giving the possibility to abort a running
+ * insert.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class ImageInserter {
+
+       /** The logger. */
+       private static final Logger logger = Logging.getLogger(ImageInserter.class);
+
+       /** The core. */
+       private final Core core;
+
+       /** The freenet interface. */
+       private final FreenetInterface freenetInterface;
+
+       /** The tokens of running inserts. */
+       private final Map<String, InsertToken> insertTokens = Collections.synchronizedMap(new HashMap<String, InsertToken>());
+
+       /**
+        * Creates a new image inserter.
+        *
+        * @param core
+        *            The Sone core
+        * @param freenetInterface
+        *            The freenet interface
+        */
+       public ImageInserter(Core core, FreenetInterface freenetInterface) {
+               this.core = core;
+               this.freenetInterface = freenetInterface;
+       }
+
+       /**
+        * Inserts the given image. The {@link #core} will automatically added as
+        * {@link ImageInsertListener} to the created {@link InsertToken}.
+        *
+        * @param temporaryImage
+        *            The temporary image data
+        * @param image
+        *            The image
+        */
+       public void insertImage(TemporaryImage temporaryImage, Image image) {
+               Validation.begin().isNotNull("Temporary Image", temporaryImage).isNotNull("Image", image).check().isEqual("Image IDs", image.getId(), temporaryImage.getId()).check();
+               try {
+                       InsertToken insertToken = freenetInterface.new InsertToken(image);
+                       insertTokens.put(image.getId(), insertToken);
+                       insertToken.addImageInsertListener(core);
+                       freenetInterface.insertImage(temporaryImage, image, insertToken);
+               } catch (SoneException se1) {
+                       logger.log(Level.WARNING, "Could not insert image!", se1);
+               }
+       }
+
+       /**
+        * Cancels a running image insert. If no insert is running for the given
+        * image, nothing happens.
+        *
+        * @param image
+        *            The image being inserted
+        */
+       public void cancelImageInsert(Image image) {
+               InsertToken insertToken = insertTokens.remove(image.getId());
+               if (insertToken == null) {
+                       return;
+               }
+               insertToken.cancel();
+               insertToken.removeImageInsertListener(core);
+       }
+
+}
index 0812f66..e8d452b 100644 (file)
 
 package net.pterodactylus.sone.core;
 
-import java.io.IOException;
 import java.io.InputStream;
 import java.net.MalformedURLException;
+import java.util.ArrayList;
 import java.util.HashSet;
+import java.util.List;
 import java.util.Set;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 
 import net.pterodactylus.sone.core.Core.SoneStatus;
+import net.pterodactylus.sone.data.Album;
 import net.pterodactylus.sone.data.Client;
+import net.pterodactylus.sone.data.Image;
 import net.pterodactylus.sone.data.Post;
 import net.pterodactylus.sone.data.Profile;
 import net.pterodactylus.sone.data.Reply;
@@ -198,8 +201,8 @@ public class SoneDownloader extends AbstractService {
                                }
                        }
                        return parsedSone;
-               } catch (IOException ioe1) {
-                       logger.log(Level.WARNING, "Could not parse Sone from " + requestUri + "!", ioe1);
+               } catch (Exception e1) {
+                       logger.log(Level.WARNING, "Could not parse Sone from " + requestUri + "!", e1);
                } finally {
                        Closer.close(soneInputStream);
                        soneBucket.free();
@@ -431,6 +434,64 @@ public class SoneDownloader extends AbstractService {
                        }
                }
 
+               /* parse albums. */
+               SimpleXML albumsXml = soneXml.getNode("albums");
+               List<Album> topLevelAlbums = new ArrayList<Album>();
+               if (albumsXml != null) {
+                       for (SimpleXML albumXml : albumsXml.getNodes("album")) {
+                               String id = albumXml.getValue("id", null);
+                               String parentId = albumXml.getValue("parent", null);
+                               String title = albumXml.getValue("title", null);
+                               String description = albumXml.getValue("description", null);
+                               String albumImageId = albumXml.getValue("album-image", null);
+                               if ((id == null) || (title == null) || (description == null)) {
+                                       logger.log(Level.WARNING, "Downloaded Sone %s contains invalid album!", new Object[] { sone });
+                                       return null;
+                               }
+                               Album parent = null;
+                               if (parentId != null) {
+                                       parent = core.getAlbum(parentId, false);
+                                       if (parent == null) {
+                                               logger.log(Level.WARNING, "Downloaded Sone %s has album with invalid parent!", new Object[] { sone });
+                                               return null;
+                                       }
+                               }
+                               Album album = core.getAlbum(id).setSone(sone).setTitle(title).setDescription(description).setAlbumImage(albumImageId);
+                               if (parent != null) {
+                                       parent.addAlbum(album);
+                               } else {
+                                       topLevelAlbums.add(album);
+                               }
+                               SimpleXML imagesXml = albumXml.getNode("images");
+                               if (imagesXml != null) {
+                                       for (SimpleXML imageXml : imagesXml.getNodes("image")) {
+                                               String imageId = imageXml.getValue("id", null);
+                                               String imageCreationTimeString = imageXml.getValue("creation-time", null);
+                                               String imageKey = imageXml.getValue("key", null);
+                                               String imageTitle = imageXml.getValue("title", null);
+                                               String imageDescription = imageXml.getValue("description", "");
+                                               String imageWidthString = imageXml.getValue("width", null);
+                                               String imageHeightString = imageXml.getValue("height", null);
+                                               if ((imageId == null) || (imageCreationTimeString == null) || (imageKey == null) || (imageTitle == null) || (imageWidthString == null) || (imageHeightString == null)) {
+                                                       logger.log(Level.WARNING, "Downloaded Sone %s contains invalid images!", new Object[] { sone });
+                                                       return null;
+                                               }
+                                               long creationTime = Numbers.safeParseLong(imageCreationTimeString, 0L);
+                                               int imageWidth = Numbers.safeParseInteger(imageWidthString, 0);
+                                               int imageHeight = Numbers.safeParseInteger(imageHeightString, 0);
+                                               if ((imageWidth < 1) || (imageHeight < 1)) {
+                                                       logger.log(Level.WARNING, "Downloaded Sone %s contains image %s with invalid dimensions (%s, %s)!", new Object[] { sone, imageId, imageWidthString, imageHeightString });
+                                                       return null;
+                                               }
+                                               Image image = core.getImage(imageId).setSone(sone).setKey(imageKey).setCreationTime(creationTime);
+                                               image.setTitle(imageTitle).setDescription(imageDescription);
+                                               image.setWidth(imageWidth).setHeight(imageHeight);
+                                               album.addImage(image);
+                                       }
+                               }
+                       }
+               }
+
                /* okay, apparently everything was parsed correctly. Now import. */
                /* atomic setter operation on the Sone. */
                synchronized (sone) {
@@ -439,6 +500,7 @@ public class SoneDownloader extends AbstractService {
                        sone.setReplies(replies);
                        sone.setLikePostIds(likedPostIds);
                        sone.setLikeReplyIds(likedReplyIds);
+                       sone.setAlbums(topLevelAlbums);
                }
 
                return sone;
index c786661..271627e 100644 (file)
@@ -37,6 +37,9 @@ public class SoneException extends Exception {
                /** An invalid URI was specified. */
                INVALID_URI,
 
+               /** An insert failed. */
+               INSERT_FAILED,
+
        }
 
        /** The type of the exception. */
index f654d09..44f8bbc 100644 (file)
@@ -302,6 +302,7 @@ public class SoneInserter extends AbstractService {
                        soneProperties.put("replies", new ListBuilder<Reply>(new ArrayList<Reply>(sone.getReplies())).sort(new ReverseComparator<Reply>(Reply.TIME_COMPARATOR)).get());
                        soneProperties.put("likedPostIds", new HashSet<String>(sone.getLikedPostIds()));
                        soneProperties.put("likedReplyIds", new HashSet<String>(sone.getLikedReplyIds()));
+                       soneProperties.put("albums", Sone.flattenAlbums(sone.getAlbums()));
                }
 
                //
diff --git a/src/main/java/net/pterodactylus/sone/data/Album.java b/src/main/java/net/pterodactylus/sone/data/Album.java
new file mode 100644 (file)
index 0000000..b15bc00
--- /dev/null
@@ -0,0 +1,464 @@
+/*
+ * Sone - Album.java - Copyright © 2011 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.data;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+import net.pterodactylus.util.collection.Mapper;
+import net.pterodactylus.util.collection.Mappers;
+import net.pterodactylus.util.object.Default;
+import net.pterodactylus.util.validation.Validation;
+
+/**
+ * Container for images that can also contain nested {@link Album}s.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class Album implements Fingerprintable {
+
+       /** The ID of this album. */
+       private final String id;
+
+       /** The Sone this album belongs to. */
+       private Sone sone;
+
+       /** Nested albums. */
+       private final List<Album> albums = new ArrayList<Album>();
+
+       /** The image IDs in order. */
+       private final List<String> imageIds = new ArrayList<String>();
+
+       /** The images in this album. */
+       private final Map<String, Image> images = new HashMap<String, Image>();
+
+       /** The parent album. */
+       private Album parent;
+
+       /** The title of this album. */
+       private String title;
+
+       /** The description of this album. */
+       private String description;
+
+       /** The ID of the album picture. */
+       private String albumImage;
+
+       /**
+        * Creates a new album with a random ID.
+        */
+       public Album() {
+               this(UUID.randomUUID().toString());
+       }
+
+       /**
+        * Creates a new album with the given ID.
+        *
+        * @param id
+        *            The ID of the album
+        */
+       public Album(String id) {
+               Validation.begin().isNotNull("Album ID", id).check();
+               this.id = id;
+       }
+
+       //
+       // ACCESSORS
+       //
+
+       /**
+        * Returns the ID of this album.
+        *
+        * @return The ID of this album
+        */
+       public String getId() {
+               return id;
+       }
+
+       /**
+        * Returns the Sone this album belongs to.
+        *
+        * @return The Sone this album belongs to
+        */
+       public Sone getSone() {
+               return sone;
+       }
+
+       /**
+        * Sets the owner of the album. The owner can only be set as long as the
+        * current owner is {@code null}.
+        *
+        * @param sone
+        *            The album owner
+        * @return This album
+        */
+       public Album setSone(Sone sone) {
+               Validation.begin().isNotNull("New Album Owner", sone).isEither("Old Album Owner", this.sone, null, sone).check();
+               this.sone = sone;
+               return this;
+       }
+
+       /**
+        * Returns the nested albums.
+        *
+        * @return The nested albums
+        */
+       public List<Album> getAlbums() {
+               return new ArrayList<Album>(albums);
+       }
+
+       /**
+        * Adds an album to this album.
+        *
+        * @param album
+        *            The album to add
+        */
+       public void addAlbum(Album album) {
+               Validation.begin().isNotNull("Album", album).check().isEqual("Album Owner", album.sone, sone).isEither("Old Album Parent", this.parent, null, album.parent).check();
+               album.setParent(this);
+               if (!albums.contains(album)) {
+                       albums.add(album);
+               }
+       }
+
+       /**
+        * Removes an album from this album.
+        *
+        * @param album
+        *            The album to remove
+        */
+       public void removeAlbum(Album album) {
+               Validation.begin().isNotNull("Album", album).check().isEqual("Album Owner", album.sone, sone).isEqual("Album Parent", album.parent, this).check();
+               albums.remove(album);
+               album.removeParent();
+       }
+
+       /**
+        * Moves the given album up in this album’s albums. If the album is already
+        * the first album, nothing happens.
+        *
+        * @param album
+        *            The album to move up
+        * @return The album that the given album swapped the place with, or
+        *         <code>null</code> if the album did not change its place
+        */
+       public Album moveAlbumUp(Album album) {
+               Validation.begin().isNotNull("Album", album).check().isEqual("Album Owner", album.sone, sone).isEqual("Album Parent", album.parent, this).check();
+               int oldIndex = albums.indexOf(album);
+               if (oldIndex <= 0) {
+                       return null;
+               }
+               albums.remove(oldIndex);
+               albums.add(oldIndex - 1, album);
+               return albums.get(oldIndex);
+       }
+
+       /**
+        * Moves the given album down in this album’s albums. If the album is
+        * already the last album, nothing happens.
+        *
+        * @param album
+        *            The album to move down
+        * @return The album that the given album swapped the place with, or
+        *         <code>null</code> if the album did not change its place
+        */
+       public Album moveAlbumDown(Album album) {
+               Validation.begin().isNotNull("Album", album).check().isEqual("Album Owner", album.sone, sone).isEqual("Album Parent", album.parent, this).check();
+               int oldIndex = albums.indexOf(album);
+               if ((oldIndex < 0) || (oldIndex >= (albums.size() - 1))) {
+                       return null;
+               }
+               albums.remove(oldIndex);
+               albums.add(oldIndex + 1, album);
+               return albums.get(oldIndex);
+       }
+
+       /**
+        * Returns the images in this album.
+        *
+        * @return The images in this album
+        */
+       public List<Image> getImages() {
+               return Mappers.mappedList(imageIds, new Mapper<String, Image>() {
+
+                       @Override
+                       @SuppressWarnings("synthetic-access")
+                       public Image map(String imageId) {
+                               return images.get(imageId);
+                       }
+
+               });
+       }
+
+       /**
+        * Adds the given image to this album.
+        *
+        * @param image
+        *            The image to add
+        */
+       public void addImage(Image image) {
+               Validation.begin().isNotNull("Image", image).check().isNotNull("Image Owner", image.getSone()).check().isEqual("Image Owner", image.getSone(), sone).check();
+               if (image.getAlbum() != null) {
+                       image.getAlbum().removeImage(image);
+               }
+               image.setAlbum(this);
+               if (imageIds.isEmpty() && (albumImage == null)) {
+                       albumImage = image.getId();
+               }
+               if (!imageIds.contains(image.getId())) {
+                       imageIds.add(image.getId());
+                       images.put(image.getId(), image);
+               }
+       }
+
+       /**
+        * Removes the given image from this album.
+        *
+        * @param image
+        *            The image to remove
+        */
+       public void removeImage(Image image) {
+               Validation.begin().isNotNull("Image", image).check().isEqual("Image Owner", image.getSone(), sone).check();
+               imageIds.remove(image.getId());
+               images.remove(image.getId());
+               if (image.getId().equals(albumImage)) {
+                       if (images.isEmpty()) {
+                               albumImage = null;
+                       } else {
+                               albumImage = images.values().iterator().next().getId();
+                       }
+               }
+       }
+
+       /**
+        * Moves the given image up in this album’s images. If the image is already
+        * the first image, nothing happens.
+        *
+        * @param image
+        *            The image to move up
+        * @return The image that the given image swapped the place with, or
+        *         <code>null</code> if the image did not change its place
+        */
+       public Image moveImageUp(Image image) {
+               Validation.begin().isNotNull("Image", image).check().isEqual("Image Album", image.getAlbum(), this).isEqual("Album Owner", image.getAlbum().getSone(), sone).check();
+               int oldIndex = imageIds.indexOf(image.getId());
+               if (oldIndex <= 0) {
+                       return null;
+               }
+               imageIds.remove(image.getId());
+               imageIds.add(oldIndex - 1, image.getId());
+               return images.get(imageIds.get(oldIndex));
+       }
+
+       /**
+        * Moves the given image down in this album’s images. If the image is
+        * already the last image, nothing happens.
+        *
+        * @param image
+        *            The image to move down
+        * @return The image that the given image swapped the place with, or
+        *         <code>null</code> if the image did not change its place
+        */
+       public Image moveImageDown(Image image) {
+               Validation.begin().isNotNull("Image", image).check().isEqual("Image Album", image.getAlbum(), this).isEqual("Album Owner", image.getAlbum().getSone(), sone).check();
+               int oldIndex = imageIds.indexOf(image.getId());
+               if ((oldIndex == -1) || (oldIndex >= (imageIds.size() - 1))) {
+                       return null;
+               }
+               imageIds.remove(image.getId());
+               imageIds.add(oldIndex + 1, image.getId());
+               return images.get(imageIds.get(oldIndex));
+       }
+
+       /**
+        * Returns the album image of this album, or {@code null} if no album image
+        * has been set.
+        *
+        * @return The image to show when this album is listed
+        */
+       public Image getAlbumImage() {
+               if (albumImage == null) {
+                       return null;
+               }
+               return Default.forNull(images.get(albumImage), images.values().iterator().next());
+       }
+
+       /**
+        * Sets the ID of the album image.
+        *
+        * @param id
+        *            The ID of the album image
+        * @return This album
+        */
+       public Album setAlbumImage(String id) {
+               this.albumImage = id;
+               return this;
+       }
+
+       /**
+        * Returns whether this album contains any other albums or images.
+        *
+        * @return {@code true} if this album is empty, {@code false} otherwise
+        */
+       public boolean isEmpty() {
+               return albums.isEmpty() && images.isEmpty();
+       }
+
+       /**
+        * Returns the parent album of this album.
+        *
+        * @return The parent album of this album, or {@code null} if this album
+        *         does not have a parent
+        */
+       public Album getParent() {
+               return parent;
+       }
+
+       /**
+        * Sets the parent album of this album.
+        *
+        * @param parent
+        *            The new parent album of this album
+        * @return This album
+        */
+       protected Album setParent(Album parent) {
+               Validation.begin().isNotNull("Album Parent", parent).check();
+               this.parent = parent;
+               return this;
+       }
+
+       /**
+        * Removes the parent album of this album.
+        *
+        * @return This album
+        */
+       protected Album removeParent() {
+               this.parent = null;
+               return this;
+       }
+
+       /**
+        * Returns the title of this album.
+        *
+        * @return The title of this album
+        */
+       public String getTitle() {
+               return title;
+       }
+
+       /**
+        * Sets the title of this album.
+        *
+        * @param title
+        *            The title of this album
+        * @return This album
+        */
+       public Album setTitle(String title) {
+               Validation.begin().isNotNull("Album Title", title).check();
+               this.title = title;
+               return this;
+       }
+
+       /**
+        * Returns the description of this album.
+        *
+        * @return The description of this album
+        */
+       public String getDescription() {
+               return description;
+       }
+
+       /**
+        * Sets the description of this album.
+        *
+        * @param description
+        *            The description of this album
+        * @return This album
+        */
+       public Album setDescription(String description) {
+               Validation.begin().isNotNull("Album Description", description).check();
+               this.description = description;
+               return this;
+       }
+
+       //
+       // FINGERPRINTABLE METHODS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public String getFingerprint() {
+               StringBuilder fingerprint = new StringBuilder();
+               fingerprint.append("Album(");
+               fingerprint.append("ID(").append(id).append(')');
+               fingerprint.append("Title(").append(title).append(')');
+               fingerprint.append("Description(").append(description).append(')');
+               if (albumImage != null) {
+                       fingerprint.append("AlbumImage(").append(albumImage).append(')');
+               }
+
+               /* add nested albums. */
+               fingerprint.append("Albums(");
+               for (Album album : albums) {
+                       fingerprint.append(album.getFingerprint());
+               }
+               fingerprint.append(')');
+
+               /* add images. */
+               fingerprint.append("Images(");
+               for (Image image : getImages()) {
+                       if (image.isInserted()) {
+                               fingerprint.append(image.getFingerprint());
+                       }
+               }
+               fingerprint.append(')');
+
+               fingerprint.append(')');
+               return fingerprint.toString();
+       }
+
+       //
+       // OBJECT METHODS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public int hashCode() {
+               return id.hashCode();
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public boolean equals(Object object) {
+               if (!(object instanceof Album)) {
+                       return false;
+               }
+               Album album = (Album) object;
+               return id.equals(album.id);
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/sone/data/Image.java b/src/main/java/net/pterodactylus/sone/data/Image.java
new file mode 100644 (file)
index 0000000..04eb349
--- /dev/null
@@ -0,0 +1,326 @@
+/*
+ * Sone - Image.java - Copyright © 2011 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.data;
+
+import java.util.UUID;
+
+import net.pterodactylus.util.validation.Validation;
+
+/**
+ * Container for image metadata.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class Image implements Fingerprintable {
+
+       /** The ID of the image. */
+       private final String id;
+
+       /** The Sone the image belongs to. */
+       private Sone sone;
+
+       /** The album this image belongs to. */
+       private Album album;
+
+       /** The request key of the image. */
+       private String key;
+
+       /** The creation time of the image. */
+       private long creationTime;
+
+       /** The width of the image. */
+       private int width;
+
+       /** The height of the image. */
+       private int height;
+
+       /** The title of the image. */
+       private String title;
+
+       /** The description of the image. */
+       private String description;
+
+       /**
+        * Creates a new image with a random ID.
+        */
+       public Image() {
+               this(UUID.randomUUID().toString());
+               setCreationTime(System.currentTimeMillis());
+       }
+
+       /**
+        * Creates a new image.
+        *
+        * @param id
+        *            The ID of the image
+        */
+       public Image(String id) {
+               Validation.begin().isNotNull("Image ID", id).check();
+               this.id = id;
+       }
+
+       //
+       // ACCESSORS
+       //
+
+       /**
+        * Returns the ID of this image.
+        *
+        * @return The ID of this image
+        */
+       public String getId() {
+               return id;
+       }
+
+       /**
+        * Returns the Sone this image belongs to.
+        *
+        * @return The Sone this image belongs to
+        */
+       public Sone getSone() {
+               return sone;
+       }
+
+       /**
+        * Sets the owner of this image. The owner can only be set if no owner has
+        * yet been set.
+        *
+        * @param sone
+        *            The new owner of this image
+        * @return This image
+        */
+       public Image setSone(Sone sone) {
+               Validation.begin().isNotNull("New Image Owner", sone).isEither("Old Image Owner", this.sone, null, sone).check();
+               this.sone = sone;
+               return this;
+       }
+
+       /**
+        * Returns the album this image belongs to.
+        *
+        * @return The album this image belongs to
+        */
+       public Album getAlbum() {
+               return album;
+       }
+
+       /**
+        * Sets the album this image belongs to. The album of an image can only be
+        * set once, and it is usually called by {@link Album#addImage(Image)}.
+        *
+        * @param album
+        *            The album this image belongs to
+        * @return This image
+        */
+       public Image setAlbum(Album album) {
+               Validation.begin().isNotNull("New Album", album).check().isEqual("Album Owner and Image Owner", album.getSone(), getSone()).check();
+               this.album = album;
+               return this;
+       }
+
+       /**
+        * Returns the request key of this image.
+        *
+        * @return The request key of this image
+        */
+       public String getKey() {
+               return key;
+       }
+
+       /**
+        * Sets the request key of this image. The request key can only be set as
+        * long as no request key has yet been set.
+        *
+        * @param key
+        *            The new request key of this image
+        * @return This image
+        */
+       public Image setKey(String key) {
+               Validation.begin().isNotNull("New Image Key", key).isEither("Old Image Key", this.key, null, key).check();
+               this.key = key;
+               return this;
+       }
+
+       /**
+        * Returns whether the image has already been inserted. An image is
+        * considered as having been inserted it its {@link #getKey() key} is not
+        * {@code null}.
+        *
+        * @return {@code true} if there is a key for this image, {@code false}
+        *         otherwise
+        */
+       public boolean isInserted() {
+               return key != null;
+       }
+
+       /**
+        * Returns the creation time of this image.
+        *
+        * @return The creation time of this image (in milliseconds since 1970, Jan
+        *         1, UTC)
+        */
+       public long getCreationTime() {
+               return creationTime;
+       }
+
+       /**
+        * Sets the new creation time of this image. The creation time can only be
+        * set as long as no creation time has been set yet.
+        *
+        * @param creationTime
+        *            The new creation time of this image
+        * @return This image
+        */
+       public Image setCreationTime(long creationTime) {
+               Validation.begin().isGreater("New Image Creation Time", creationTime, 0).isEither("Old Image Creation Time", this.creationTime, 0L, creationTime).check();
+               this.creationTime = creationTime;
+               return this;
+       }
+
+       /**
+        * Returns the width of this image.
+        *
+        * @return The width of this image (in pixels)
+        */
+       public int getWidth() {
+               return width;
+       }
+
+       /**
+        * Sets the width of this image. The width can only be set as long as no
+        * width has been set yet.
+        *
+        * @param width
+        *            The new width of this image
+        * @return This image
+        */
+       public Image setWidth(int width) {
+               Validation.begin().isGreater("New Image Width", width, 0).isEither("Old Image Width", this.width, 0, width).check();
+               this.width = width;
+               return this;
+       }
+
+       /**
+        * Returns the height of this image.
+        *
+        * @return The height of this image (in pixels)
+        */
+       public int getHeight() {
+               return height;
+       }
+
+       /**
+        * Sets the new height of this image. The height can only be set as long as
+        * no height has yet been set.
+        *
+        * @param height
+        *            The new height of this image
+        * @return This image
+        */
+       public Image setHeight(int height) {
+               Validation.begin().isGreater("New Image Height", height, 0).isEither("Old Image Height", this.height, 0, height).check();
+               this.height = height;
+               return this;
+       }
+
+       /**
+        * Returns the title of this image.
+        *
+        * @return The title of this image
+        */
+       public String getTitle() {
+               return title;
+       }
+
+       /**
+        * Sets the title of this image.
+        *
+        * @param title
+        *            The title of this image
+        * @return This image
+        */
+       public Image setTitle(String title) {
+               Validation.begin().isNotNull("Image Title", title).check();
+               this.title = title;
+               return this;
+       }
+
+       /**
+        * Returns the description of this image.
+        *
+        * @return The description of this image
+        */
+       public String getDescription() {
+               return description;
+       }
+
+       /**
+        * Sets the description of this image.
+        *
+        * @param description
+        *            The description of this image
+        * @return This image
+        */
+       public Image setDescription(String description) {
+               Validation.begin().isNotNull("Image Description", description).check();
+               this.description = description;
+               return this;
+       }
+
+       //
+       // FINGERPRINTABLE METHODS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public String getFingerprint() {
+               StringBuilder fingerprint = new StringBuilder();
+               fingerprint.append("Image(");
+               fingerprint.append("ID(").append(id).append(')');
+               fingerprint.append("Title(").append(title).append(')');
+               fingerprint.append("Description(").append(description).append(')');
+               fingerprint.append(')');
+               return fingerprint.toString();
+       }
+
+       //
+       // OBJECT METHODS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public int hashCode() {
+               return id.hashCode();
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public boolean equals(Object object) {
+               if (!(object instanceof Image)) {
+                       return false;
+               }
+               return ((Image) object).id.equals(id);
+       }
+
+}
index 3477346..e124387 100644 (file)
@@ -34,6 +34,7 @@ import net.pterodactylus.sone.freenet.wot.OwnIdentity;
 import net.pterodactylus.sone.template.SoneAccessor;
 import net.pterodactylus.util.filter.Filter;
 import net.pterodactylus.util.logging.Logging;
+import net.pterodactylus.util.validation.Validation;
 import freenet.keys.FreenetURI;
 
 /**
@@ -145,6 +146,9 @@ public class Sone implements Fingerprintable, Comparable<Sone> {
        /** The IDs of all liked replies. */
        private final Set<String> likedReplyIds = Collections.synchronizedSet(new HashSet<String>());
 
+       /** The albums of this Sone. */
+       private final List<Album> albums = Collections.synchronizedList(new ArrayList<Album>());
+
        /** Sone-specific options. */
        private final Options options = new Options();
 
@@ -282,7 +286,7 @@ public class Sone implements Fingerprintable, Comparable<Sone> {
         */
        public void setLatestEdition(long latestEdition) {
                if (!(latestEdition > this.latestEdition)) {
-                       logger.log(Level.INFO, "New latest edition %d is not greater than current latest edition %d!", new Object[] { latestEdition, this.latestEdition });
+                       logger.log(Level.FINE, "New latest edition %d is not greater than current latest edition %d!", new Object[] { latestEdition, this.latestEdition });
                        return;
                }
                this.latestEdition = latestEdition;
@@ -632,6 +636,91 @@ public class Sone implements Fingerprintable, Comparable<Sone> {
        }
 
        /**
+        * Returns the albums of this Sone.
+        *
+        * @return The albums of this Sone
+        */
+       public List<Album> getAlbums() {
+               return Collections.unmodifiableList(albums);
+       }
+
+       /**
+        * Adds an album to this Sone.
+        *
+        * @param album
+        *            The album to add
+        */
+       public synchronized void addAlbum(Album album) {
+               Validation.begin().isNotNull("Album", album).check().isEqual("Album Owner", album.getSone(), this).check();
+               albums.add(album);
+       }
+
+       /**
+        * Sets the albums of this Sone.
+        *
+        * @param albums
+        *            The albums of this Sone
+        */
+       public synchronized void setAlbums(Collection<? extends Album> albums) {
+               Validation.begin().isNotNull("Albums", albums).check();
+               this.albums.clear();
+               for (Album album : albums) {
+                       addAlbum(album);
+               }
+       }
+
+       /**
+        * Removes an album from this Sone.
+        *
+        * @param album
+        *            The album to remove
+        */
+       public synchronized void removeAlbum(Album album) {
+               Validation.begin().isNotNull("Album", album).check().isEqual("Album Owner", album.getSone(), this).check();
+               albums.remove(album);
+       }
+
+       /**
+        * Moves the given album up in this album’s albums. If the album is already
+        * the first album, nothing happens.
+        *
+        * @param album
+        *            The album to move up
+        * @return The album that the given album swapped the place with, or
+        *         <code>null</code> if the album did not change its place
+        */
+       public Album moveAlbumUp(Album album) {
+               Validation.begin().isNotNull("Album", album).check().isEqual("Album Owner", album.getSone(), this).isNull("Album Parent", album.getParent()).check();
+               int oldIndex = albums.indexOf(album);
+               if (oldIndex <= 0) {
+                       return null;
+               }
+               albums.remove(oldIndex);
+               albums.add(oldIndex - 1, album);
+               return albums.get(oldIndex);
+       }
+
+       /**
+        * Moves the given album down in this album’s albums. If the album is
+        * already the last album, nothing happens.
+        *
+        * @param album
+        *            The album to move down
+        * @return The album that the given album swapped the place with, or
+        *         <code>null</code> if the album did not change its place
+        */
+       public Album moveAlbumDown(Album album) {
+               Validation.begin().isNotNull("Album", album).check().isEqual("Album Owner", album.getSone(), this).isNull("Album Parent", album.getParent()).check();
+               int oldIndex = albums.indexOf(album);
+               if ((oldIndex < 0) || (oldIndex >= (albums.size() - 1))) {
+                       return null;
+               }
+               albums.remove(oldIndex);
+               albums.add(oldIndex + 1, album);
+               return albums.get(oldIndex);
+       }
+
+       /**
         * Returns Sone-specific options.
         *
         * @return The options of this Sone
@@ -682,10 +771,43 @@ public class Sone implements Fingerprintable, Comparable<Sone> {
                }
                fingerprint.append(')');
 
+               fingerprint.append("Albums(");
+               for (Album album : albums) {
+                       fingerprint.append(album.getFingerprint());
+               }
+               fingerprint.append(')');
+
                return fingerprint.toString();
        }
 
        //
+       // STATIC METHODS
+       //
+
+       /**
+        * Flattens the given top-level albums so that the resulting list contains
+        * parent albums before child albums and the resulting list can be parsed in
+        * a single pass.
+        *
+        * @param albums
+        *            The albums to flatten
+        * @return The flattened albums
+        */
+       public static List<Album> flattenAlbums(Collection<? extends Album> albums) {
+               List<Album> flatAlbums = new ArrayList<Album>();
+               flatAlbums.addAll(albums);
+               int lastAlbumIndex = 0;
+               while (lastAlbumIndex < flatAlbums.size()) {
+                       int previousAlbumCount = flatAlbums.size();
+                       for (Album album : new ArrayList<Album>(flatAlbums.subList(lastAlbumIndex, flatAlbums.size()))) {
+                               flatAlbums.addAll(album.getAlbums());
+                       }
+                       lastAlbumIndex = previousAlbumCount;
+               }
+               return flatAlbums;
+       }
+
+       //
        // INTERFACE Comparable<Sone>
        //
 
diff --git a/src/main/java/net/pterodactylus/sone/data/TemporaryImage.java b/src/main/java/net/pterodactylus/sone/data/TemporaryImage.java
new file mode 100644 (file)
index 0000000..ddac505
--- /dev/null
@@ -0,0 +1,113 @@
+/*
+ * Sone - TemporaryImage.java - Copyright © 2011 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.data;
+
+import java.util.UUID;
+
+import net.pterodactylus.util.validation.Validation;
+
+/**
+ * A temporary image stores an uploaded image in memory until it has been
+ * inserted into Freenet and is subsequently loaded from there.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class TemporaryImage {
+
+       /** The ID of the temporary image. */
+       private final String id;
+
+       /** The MIME type of the image. */
+       private String mimeType;
+
+       /** The encoded image data. */
+       private byte[] imageData;
+
+       /**
+        * Creates a new temporary image with a random ID.
+        */
+       public TemporaryImage() {
+               this(UUID.randomUUID().toString());
+       }
+
+       /**
+        * Creates a new temporary image.
+        *
+        * @param id
+        *            The ID of the temporary image
+        */
+       public TemporaryImage(String id) {
+               this.id = id;
+       }
+
+       /**
+        * Returns the ID of the temporary image.
+        *
+        * @return The ID of the temporary image
+        */
+       public String getId() {
+               return id;
+       }
+
+       /**
+        * Returns the MIME type of the image.
+        *
+        * @return The MIME type of the image
+        */
+       public String getMimeType() {
+               return mimeType;
+       }
+
+       /**
+        * Sets the MIME type of the image. The MIME type can only be set once and
+        * it must not be {@code null}.
+        *
+        * @param mimeType
+        *            The MIME type of the image
+        * @return This temporary image
+        */
+       public TemporaryImage setMimeType(String mimeType) {
+               Validation.begin().isNotNull("MIME Type", mimeType).isNull("Previous MIME Type", this.mimeType).check();
+               this.mimeType = mimeType;
+               return this;
+       }
+
+       /**
+        * Returns the encoded image data.
+        *
+        * @return The encoded image data
+        */
+       public byte[] getImageData() {
+               return imageData;
+       }
+
+       /**
+        * Sets the encoded image data. The encoded image data can only be set once
+        * and it must not be {@code null}.
+        *
+        * @param imageData
+        *            The encoded image data
+        * @return This temporary image
+        */
+       public TemporaryImage setImageData(byte[] imageData) {
+               Validation.begin().isNotNull("Image Data", imageData).isNull("Previous Image Data", this.imageData).check();
+               this.imageData = imageData;
+               return this;
+       }
+
+}
index 59da039..de30a5d 100644 (file)
@@ -70,6 +70,13 @@ public class WebOfTrustConnector implements ConnectorListener {
        //
 
        /**
+        * Stops the web of trust connector.
+        */
+       public void stop() {
+               pluginConnector.removeConnectorListener(WOT_PLUGIN_NAME, PLUGIN_CONNECTION_IDENTIFIER, this);
+       }
+
+       /**
         * Loads all own identities from the Web of Trust plugin.
         *
         * @return All own identity
index d642dd8..f21c1c2 100644 (file)
@@ -83,7 +83,7 @@ public class SonePlugin implements FredPlugin, FredPluginFCP, FredPluginL10n, Fr
        }
 
        /** The version. */
-       public static final Version VERSION = new Version(0, 6, 7);
+       public static final Version VERSION = new Version(0, 7);
 
        /** The logger. */
        private static final Logger logger = Logging.getLogger(SonePlugin.class);
@@ -103,6 +103,9 @@ public class SonePlugin implements FredPlugin, FredPluginFCP, FredPluginL10n, Fr
        /** The l10n helper. */
        private PluginL10n l10n;
 
+       /** The web of trust connector. */
+       private WebOfTrustConnector webOfTrustConnector;
+
        /** The identity manager. */
        private IdentityManager identityManager;
 
@@ -181,7 +184,7 @@ public class SonePlugin implements FredPlugin, FredPluginFCP, FredPluginL10n, Fr
 
                        /* create web of trust connector. */
                        PluginConnector pluginConnector = new PluginConnector(pluginRespirator);
-                       WebOfTrustConnector webOfTrustConnector = new WebOfTrustConnector(pluginConnector);
+                       webOfTrustConnector = new WebOfTrustConnector(pluginConnector);
                        identityManager = new IdentityManager(webOfTrustConnector);
                        identityManager.setContext("Sone");
 
@@ -236,6 +239,9 @@ public class SonePlugin implements FredPlugin, FredPluginFCP, FredPluginL10n, Fr
 
                        /* stop the identity manager. */
                        identityManager.stop();
+
+                       /* stop the web of trust connector. */
+                       webOfTrustConnector.stop();
                } catch (Throwable t1) {
                        logger.log(Level.SEVERE, "Error while shutting down!", t1);
                } finally {
diff --git a/src/main/java/net/pterodactylus/sone/template/AlbumAccessor.java b/src/main/java/net/pterodactylus/sone/template/AlbumAccessor.java
new file mode 100644 (file)
index 0000000..2c0a00e
--- /dev/null
@@ -0,0 +1,78 @@
+/*
+ * Sone - AlbumAccessor.java - Copyright © 2011 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.template;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import net.pterodactylus.sone.data.Album;
+import net.pterodactylus.util.template.Accessor;
+import net.pterodactylus.util.template.ReflectionAccessor;
+import net.pterodactylus.util.template.TemplateContext;
+
+/**
+ * {@link Accessor} implementation for {@link Album}s. A property named
+ * “backlinks” is added, it returns links to all parents and the owner Sone of
+ * an album.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class AlbumAccessor extends ReflectionAccessor {
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public Object get(TemplateContext templateContext, Object object, String member) {
+               Album album = (Album) object;
+               if ("backlinks".equals(member)) {
+                       List<Map<String, String>> backlinks = new ArrayList<Map<String, String>>();
+                       Album currentAlbum = album;
+                       while (currentAlbum != null) {
+                               backlinks.add(0, createLink("imageBrowser.html?album=" + currentAlbum.getId(), currentAlbum.getTitle()));
+                               currentAlbum = currentAlbum.getParent();
+                       }
+                       backlinks.add(0, createLink("imageBrowser.html?sone=" + album.getSone().getId(), SoneAccessor.getNiceName(album.getSone())));
+                       return backlinks;
+               }
+               return super.get(templateContext, object, member);
+       }
+
+       //
+       // PRIVATE METHODS
+       //
+
+       /**
+        * Creates a map containing mappings for “target” and “link.”
+        *
+        * @param target
+        *            The target to link to
+        * @param name
+        *            The name of the link
+        * @return The created map containing the mappings
+        */
+       private Map<String, String> createLink(String target, String name) {
+               Map<String, String> link = new HashMap<String, String>();
+               link.put("target", target);
+               link.put("name", name);
+               return link;
+       }
+
+}
index a25d619..b2a4962 100644 (file)
@@ -19,9 +19,10 @@ package net.pterodactylus.sone.template;
 
 import java.util.Map;
 
-import net.pterodactylus.sone.web.page.Page.Request;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.template.Plugin;
 import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.web.Request;
 
 /**
  * Extracts a page number from a {@link Request}’s parameters and stores it in
@@ -50,7 +51,7 @@ public class GetPagePlugin implements Plugin {
                        pageKey = "page";
                }
 
-               Request request = (Request) templateContext.get(requestKey);
+               FreenetRequest request = (FreenetRequest) templateContext.get(requestKey);
                String pageString = request.getHttpRequest().getParam(parameter);
                int page = 0;
                try {
diff --git a/src/main/java/net/pterodactylus/sone/template/ImageLinkFilter.java b/src/main/java/net/pterodactylus/sone/template/ImageLinkFilter.java
new file mode 100644 (file)
index 0000000..9e758cf
--- /dev/null
@@ -0,0 +1,108 @@
+/*
+ * Sone - ImageLinkFilter.java - Copyright © 2011 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.template;
+
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.util.Map;
+
+import net.pterodactylus.sone.data.Image;
+import net.pterodactylus.util.number.Numbers;
+import net.pterodactylus.util.object.Default;
+import net.pterodactylus.util.template.Filter;
+import net.pterodactylus.util.template.Template;
+import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.template.TemplateContextFactory;
+import net.pterodactylus.util.template.TemplateParser;
+
+/**
+ * Template filter that turns an {@link Image} into an HTML &lt;img&gt; tag,
+ * using some parameters to influence parameters of the image.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class ImageLinkFilter implements Filter {
+
+       /** The template to render for the &lt;img&gt; tag. */
+       private static final Template linkTemplate = TemplateParser.parse(new StringReader("<img<%ifnull !class> class=\"<%class|css>\"<%/if> src=\"<%src|html>\" alt=\"<%alt|html>\" title=\"<%title|html>\" width=\"<%width|html>\" height=\"<%height|html>\" style=\"position: relative;<%ifnull ! top>top: <% top|html>;<%/if><%ifnull ! left>left: <% left|html>;<%/if>\"/>"));
+
+       /** The template context factory. */
+       private final TemplateContextFactory templateContextFactory;
+
+       /**
+        * Creates a new image link filter.
+        *
+        * @param templateContextFactory
+        *            The template context factory
+        */
+       public ImageLinkFilter(TemplateContextFactory templateContextFactory) {
+               this.templateContextFactory = templateContextFactory;
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public Object format(TemplateContext templateContext, Object data, Map<String, String> parameters) {
+               Image image = (Image) data;
+               String imageClass = parameters.get("class");
+               int maxWidth = Numbers.safeParseInteger(parameters.get("max-width"), Integer.MAX_VALUE);
+               int maxHeight = Numbers.safeParseInteger(parameters.get("max-height"), Integer.MAX_VALUE);
+               String mode = String.valueOf(parameters.get("mode"));
+               String title = parameters.get("title");
+               if ((title != null) && title.startsWith("=")) {
+                       title = String.valueOf(templateContext.get(title.substring(1)));
+               }
+
+               TemplateContext linkTemplateContext = templateContextFactory.createTemplateContext();
+               linkTemplateContext.set("class", imageClass);
+               if (image.isInserted()) {
+                       linkTemplateContext.set("src", "/" + image.getKey());
+               } else {
+                       linkTemplateContext.set("src", "getImage.html?image=" + image.getId());
+               }
+               int imageWidth = image.getWidth();
+               int imageHeight = image.getHeight();
+               if ("enlarge".equals(mode)) {
+                       double scale = Math.max(maxWidth / (double) imageWidth, maxHeight / (double) imageHeight);
+                       linkTemplateContext.set("width", (int) (imageWidth * scale + 0.5));
+                       linkTemplateContext.set("height", (int) (imageHeight * scale + 0.5));
+                       if (scale >= 1) {
+                               linkTemplateContext.set("left", String.format("%dpx", (int) ((imageWidth * scale) - maxWidth) / 2));
+                               linkTemplateContext.set("top", String.format("%dpx", (int) ((imageHeight * scale) - maxHeight) / 2));
+                       } else {
+                               linkTemplateContext.set("left", String.format("%dpx", (int) (maxWidth - (imageWidth * scale)) / 2));
+                               linkTemplateContext.set("top", String.format("%dpx", (int) (maxHeight - (imageHeight * scale)) / 2));
+                       }
+               } else {
+                       double scale = 1;
+                       if ((imageWidth > maxWidth) || (imageHeight > maxHeight)) {
+                               scale = Math.min(maxWidth / (double) imageWidth, maxHeight / (double) imageHeight);
+                       }
+                       linkTemplateContext.set("width", (int) (imageWidth * scale + 0.5));
+                       linkTemplateContext.set("height", (int) (imageHeight * scale + 0.5));
+               }
+               linkTemplateContext.set("alt", Default.forNull(title, image.getDescription()));
+               linkTemplateContext.set("title", Default.forNull(title, image.getTitle()));
+
+               StringWriter stringWriter = new StringWriter();
+               linkTemplate.render(linkTemplateContext, stringWriter);
+               return stringWriter.toString();
+       }
+
+}
index 332f504..3494c21 100644 (file)
@@ -35,7 +35,7 @@ import net.pterodactylus.sone.text.PostPart;
 import net.pterodactylus.sone.text.SonePart;
 import net.pterodactylus.sone.text.SoneTextParser;
 import net.pterodactylus.sone.text.SoneTextParserContext;
-import net.pterodactylus.sone.web.page.Page.Request;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.template.Filter;
 import net.pterodactylus.util.template.Template;
 import net.pterodactylus.util.template.TemplateContext;
@@ -108,7 +108,7 @@ public class ParserFilter implements Filter {
                if (sone == null) {
                        sone = core.getSone(soneKey, false);
                }
-               Request request = (Request) templateContext.get("request");
+               FreenetRequest request = (FreenetRequest) templateContext.get("request");
                SoneTextParserContext context = new SoneTextParserContext(request, sone);
                StringWriter parsedTextWriter = new StringWriter();
                try {
index fb31719..a0d80af 100644 (file)
@@ -26,15 +26,15 @@ import java.util.HashMap;
 import java.util.Map;
 import java.util.Map.Entry;
 
-import net.pterodactylus.sone.web.page.Page.Request;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.template.Filter;
 import net.pterodactylus.util.template.TemplateContext;
 
 /**
- * This filter expects a {@link Request} as input and outputs a {@link URI} that
- * is modified by the parameters. The name of the parameter is handed in as
- * “name”, the value may either be stored in “value”, or in a template variable
- * whose key is stored in “key”.
+ * This filter expects a {@link FreenetRequest} as input and outputs a
+ * {@link URI} that is modified by the parameters. The name of the parameter is
+ * handed in as “name”, the value may either be stored in “value”, or in a
+ * template variable whose key is stored in “key”.
  *
  * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
  */
@@ -45,7 +45,7 @@ public class RequestChangeFilter implements Filter {
         */
        @Override
        public Object format(TemplateContext templateContext, Object data, Map<String, String> parameters) {
-               Request request = (Request) data;
+               FreenetRequest request = (FreenetRequest) data;
                String name = parameters.get("name");
                String nameKey = parameters.get("nameKey");
                if (nameKey != null) {
index 4195516..051c02a 100644 (file)
@@ -123,6 +123,11 @@ public class SoneTextParser implements Parser<SoneTextParserContext> {
                                continue;
                        }
                        emptyLines = 0;
+                       /*
+                        * lineComplete tracks whether the block you are parsing is the
+                        * first block of the line. this is important because sometimes you
+                        * have to add an additional line break.
+                        */
                        boolean lineComplete = true;
                        while (line.length() > 0) {
                                int nextKsk = line.indexOf("KSK@");
@@ -175,120 +180,111 @@ public class SoneTextParser implements Parser<SoneTextParserContext> {
                                        next = nextPost;
                                        linkType = LinkType.POST;
                                }
+
+                               /* cut off “freenet:” from before keys. */
+                               if (((linkType == LinkType.KSK) || (linkType == LinkType.CHK) || (linkType == LinkType.SSK) || (linkType == LinkType.USK)) && (next >= 8) && (line.substring(next - 8, next).equals("freenet:"))) {
+                                       next -= 8;
+                                       line = line.substring(0, next) + line.substring(next + 8);
+                               }
+
+                               /* if there is text before the next item, write it out. */
+                               if (lineComplete && !lastLineEmpty) {
+                                       parts.add(new PlainTextPart("\n"));
+                               }
+                               if (next > 0) {
+                                       parts.add(new PlainTextPart(line.substring(0, next)));
+                                       line = line.substring(next);
+                                       next = 0;
+                               }
+                               lineComplete = false;
+
                                if (linkType == LinkType.SONE) {
-                                       if (next > 0) {
-                                               parts.add(new PlainTextPart(line.substring(0, next)));
-                                       }
-                                       if (line.length() >= (next + 7 + 43)) {
-                                               String soneId = line.substring(next + 7, next + 50);
+                                       if (line.length() >= (7 + 43)) {
+                                               String soneId = line.substring(7, 50);
                                                Sone sone = soneProvider.getSone(soneId, false);
                                                if ((sone != null) && (sone.getName() != null)) {
                                                        parts.add(new SonePart(sone));
                                                } else {
-                                                       parts.add(new PlainTextPart(line.substring(next, next + 50)));
+                                                       parts.add(new PlainTextPart(line.substring(0, 50)));
                                                }
-                                               line = line.substring(next + 50);
+                                               line = line.substring(50);
                                        } else {
-                                               parts.add(new PlainTextPart(line.substring(next)));
+                                               parts.add(new PlainTextPart(line));
                                                line = "";
                                        }
                                        continue;
                                }
                                if (linkType == LinkType.POST) {
-                                       if (next > 0) {
-                                               parts.add(new PlainTextPart(line.substring(0, next)));
-                                       }
-                                       if (line.length() >= (next + 7 + 36)) {
-                                               String postId = line.substring(next + 7, next + 43);
+                                       if (line.length() >= (7 + 36)) {
+                                               String postId = line.substring(7, 43);
                                                Post post = postProvider.getPost(postId, false);
                                                if ((post != null) && (post.getSone() != null)) {
                                                        parts.add(new PostPart(post));
                                                } else {
-                                                       parts.add(new PlainTextPart(line.substring(next, next + 43)));
+                                                       parts.add(new PlainTextPart(line.substring(0, 43)));
                                                }
-                                               line = line.substring(next + 43);
+                                               line = line.substring(43);
                                        } else {
-                                               parts.add(new PlainTextPart(line.substring(next)));
+                                               parts.add(new PlainTextPart(line));
                                                line = "";
                                        }
                                        continue;
                                }
-                               if ((next >= 8) && (line.substring(next - 8, next).equals("freenet:"))) {
-                                       next -= 8;
-                                       line = line.substring(0, next) + line.substring(next + 8);
-                               }
                                Matcher matcher = whitespacePattern.matcher(line);
-                               int nextSpace = matcher.find(next) ? matcher.start() : line.length();
-                               if (nextSpace > (next + 4)) {
-                                       if (!lastLineEmpty && lineComplete) {
-                                               parts.add(new PlainTextPart("\n" + line.substring(0, next)));
-                                       } else {
-                                               if (next > 0) {
-                                                       parts.add(new PlainTextPart(line.substring(0, next)));
-                                               }
-                                       }
-                                       String link = line.substring(next, nextSpace);
-                                       String name = link;
-                                       logger.log(Level.FINER, "Found link: %s", link);
-                                       logger.log(Level.FINEST, "Next: %d, CHK: %d, SSK: %d, USK: %d", new Object[] { next, nextChk, nextSsk, nextUsk });
+                               int nextSpace = matcher.find(0) ? matcher.start() : line.length();
+                               String link = line.substring(0, nextSpace);
+                               String name = link;
+                               logger.log(Level.FINER, "Found link: %s", link);
+                               logger.log(Level.FINEST, "CHK: %d, SSK: %d, USK: %d", new Object[] { nextChk, nextSsk, nextUsk });
 
-                                       if ((linkType == LinkType.KSK) || (linkType == LinkType.CHK) || (linkType == LinkType.SSK) || (linkType == LinkType.USK)) {
-                                               FreenetURI uri;
-                                               if (name.indexOf('?') > -1) {
-                                                       name = name.substring(0, name.indexOf('?'));
-                                               }
-                                               if (name.endsWith("/")) {
-                                                       name = name.substring(0, name.length() - 1);
-                                               }
-                                               try {
-                                                       uri = new FreenetURI(name);
-                                                       name = uri.lastMetaString();
-                                                       if (name == null) {
-                                                               name = uri.getDocName();
-                                                       }
-                                                       if (name == null) {
-                                                               name = link.substring(0, Math.min(9, link.length()));
-                                                       }
-                                                       boolean fromPostingSone = ((linkType == LinkType.SSK) || (linkType == LinkType.USK)) && (context != null) && (context.getPostingSone() != null) && link.substring(4, Math.min(link.length(), 47)).equals(context.getPostingSone().getId());
-                                                       parts.add(new FreenetLinkPart(link, name, fromPostingSone));
-                                               } catch (MalformedURLException mue1) {
-                                                       /* not a valid link, insert as plain text. */
-                                                       parts.add(new PlainTextPart(link));
-                                               } catch (NullPointerException npe1) {
-                                                       /* FreenetURI sometimes throws these, too. */
-                                                       parts.add(new PlainTextPart(link));
-                                               } catch (ArrayIndexOutOfBoundsException aioobe1) {
-                                                       /* oh, and these, too. */
-                                                       parts.add(new PlainTextPart(link));
-                                               }
-                                       } else if ((linkType == LinkType.HTTP) || (linkType == LinkType.HTTPS)) {
-                                               name = link.substring(linkType == LinkType.HTTP ? 7 : 8);
-                                               int firstSlash = name.indexOf('/');
-                                               int lastSlash = name.lastIndexOf('/');
-                                               if ((lastSlash - firstSlash) > 3) {
-                                                       name = name.substring(0, firstSlash + 1) + "…" + name.substring(lastSlash);
-                                               }
-                                               if (name.endsWith("/")) {
-                                                       name = name.substring(0, name.length() - 1);
-                                               }
-                                               if (((name.indexOf('/') > -1) && (name.indexOf('.') < name.lastIndexOf('.', name.indexOf('/'))) || ((name.indexOf('/') == -1) && (name.indexOf('.') < name.lastIndexOf('.')))) && name.startsWith("www.")) {
-                                                       name = name.substring(4);
+                               if ((linkType == LinkType.KSK) || (linkType == LinkType.CHK) || (linkType == LinkType.SSK) || (linkType == LinkType.USK)) {
+                                       FreenetURI uri;
+                                       if (name.indexOf('?') > -1) {
+                                               name = name.substring(0, name.indexOf('?'));
+                                       }
+                                       if (name.endsWith("/")) {
+                                               name = name.substring(0, name.length() - 1);
+                                       }
+                                       try {
+                                               uri = new FreenetURI(name);
+                                               name = uri.lastMetaString();
+                                               if (name == null) {
+                                                       name = uri.getDocName();
                                                }
-                                               if (name.indexOf('?') > -1) {
-                                                       name = name.substring(0, name.indexOf('?'));
+                                               if (name == null) {
+                                                       name = link.substring(0, Math.min(9, link.length()));
                                                }
-                                               parts.add(new LinkPart(link, name));
+                                               boolean fromPostingSone = ((linkType == LinkType.SSK) || (linkType == LinkType.USK)) && (context != null) && (context.getPostingSone() != null) && link.substring(4, Math.min(link.length(), 47)).equals(context.getPostingSone().getId());
+                                               parts.add(new FreenetLinkPart(link, name, fromPostingSone));
+                                       } catch (MalformedURLException mue1) {
+                                               /* not a valid link, insert as plain text. */
+                                               parts.add(new PlainTextPart(link));
+                                       } catch (NullPointerException npe1) {
+                                               /* FreenetURI sometimes throws these, too. */
+                                               parts.add(new PlainTextPart(link));
+                                       } catch (ArrayIndexOutOfBoundsException aioobe1) {
+                                               /* oh, and these, too. */
+                                               parts.add(new PlainTextPart(link));
                                        }
-                                       line = line.substring(nextSpace);
-                               } else {
-                                       if (!lastLineEmpty && lineComplete) {
-                                               parts.add(new PlainTextPart("\n" + line.substring(0, next + 4)));
-                                       } else {
-                                               parts.add(new PlainTextPart(line.substring(0, next + 4)));
+                               } else if ((linkType == LinkType.HTTP) || (linkType == LinkType.HTTPS)) {
+                                       name = link.substring(linkType == LinkType.HTTP ? 7 : 8);
+                                       int firstSlash = name.indexOf('/');
+                                       int lastSlash = name.lastIndexOf('/');
+                                       if ((lastSlash - firstSlash) > 3) {
+                                               name = name.substring(0, firstSlash + 1) + "…" + name.substring(lastSlash);
+                                       }
+                                       if (name.endsWith("/")) {
+                                               name = name.substring(0, name.length() - 1);
                                        }
-                                       line = line.substring(next + 4);
+                                       if (((name.indexOf('/') > -1) && (name.indexOf('.') < name.lastIndexOf('.', name.indexOf('/'))) || ((name.indexOf('/') == -1) && (name.indexOf('.') < name.lastIndexOf('.')))) && name.startsWith("www.")) {
+                                               name = name.substring(4);
+                                       }
+                                       if (name.indexOf('?') > -1) {
+                                               name = name.substring(0, name.indexOf('?'));
+                                       }
+                                       parts.add(new LinkPart(link, name));
                                }
-                               lineComplete = false;
+                               line = line.substring(nextSpace);
                        }
                        lastLineEmpty = false;
                }
index 4a89016..35b190b 100644 (file)
@@ -18,7 +18,7 @@
 package net.pterodactylus.sone.text;
 
 import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.sone.web.page.Page.Request;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 
 /**
  * {@link ParserContext} implementation for the {@link SoneTextParser}. It
@@ -30,7 +30,7 @@ import net.pterodactylus.sone.web.page.Page.Request;
 public class SoneTextParserContext implements ParserContext {
 
        /** The request being processed. */
-       private final Request request;
+       private final FreenetRequest request;
 
        /** The posting Sone. */
        private final Sone postingSone;
@@ -43,7 +43,7 @@ public class SoneTextParserContext implements ParserContext {
         * @param postingSone
         *            The posting Sone
         */
-       public SoneTextParserContext(Request request, Sone postingSone) {
+       public SoneTextParserContext(FreenetRequest request, Sone postingSone) {
                this.request = request;
                this.postingSone = postingSone;
        }
@@ -53,7 +53,7 @@ public class SoneTextParserContext implements ParserContext {
         *
         * @return The request being processed
         */
-       public Request getRequest() {
+       public FreenetRequest getRequest() {
                return request;
        }
 
index 4c14457..a9698e4 100644 (file)
@@ -17,6 +17,7 @@
 
 package net.pterodactylus.sone.web;
 
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.template.Template;
 import net.pterodactylus.util.template.TemplateContext;
 import net.pterodactylus.util.version.Version;
@@ -54,7 +55,7 @@ public class AboutPage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+       protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
                super.processTemplate(request, templateContext);
                templateContext.set("version", version);
        }
index 12a0934..0ebf9d5 100644 (file)
 
 package net.pterodactylus.sone.web;
 
-import net.pterodactylus.sone.web.page.Page.Request.Method;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.template.Template;
 import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.web.Method;
 
 /**
  * Page that lets the user bookmark a post.
@@ -46,7 +47,7 @@ public class BookmarkPage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+       protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
                super.processTemplate(request, templateContext);
                if (request.getMethod() == Method.POST) {
                        String id = request.getHttpRequest().getPartAsStringFailsafe("post", 36);
index d6f63aa..ad1717b 100644 (file)
@@ -23,6 +23,7 @@ import java.util.List;
 import java.util.Set;
 
 import net.pterodactylus.sone.data.Post;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.collection.Pagination;
 import net.pterodactylus.util.filter.Filter;
 import net.pterodactylus.util.filter.Filters;
@@ -57,7 +58,7 @@ public class BookmarksPage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+       protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
                super.processTemplate(request, templateContext);
                Set<Post> allPosts = webInterface.getCore().getBookmarkedPosts();
                Set<Post> loadedPosts = Filters.filteredSet(allPosts, new Filter<Post>() {
diff --git a/src/main/java/net/pterodactylus/sone/web/CreateAlbumPage.java b/src/main/java/net/pterodactylus/sone/web/CreateAlbumPage.java
new file mode 100644 (file)
index 0000000..a695952
--- /dev/null
@@ -0,0 +1,73 @@
+/*
+ * Sone - CreateAlbumPage.java - Copyright © 2011 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 net.pterodactylus.sone.data.Album;
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.web.page.FreenetRequest;
+import net.pterodactylus.util.template.Template;
+import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.web.Method;
+
+/**
+ * Page that lets the user create a new album.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class CreateAlbumPage extends SoneTemplatePage {
+
+       /**
+        * Creates a new “create album” page.
+        *
+        * @param template
+        *            The template to render
+        * @param webInterface
+        *            The Sone web interface
+        */
+       public CreateAlbumPage(Template template, WebInterface webInterface) {
+               super("createAlbum.html", template, "Page.CreateAlbum.Title", webInterface, true);
+       }
+
+       //
+       // SONETEMPLATEPAGE METHODS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
+               super.processTemplate(request, templateContext);
+               if (request.getMethod() == Method.POST) {
+                       String name = request.getHttpRequest().getPartAsStringFailsafe("name", 64).trim();
+                       if (name.length() == 0) {
+                               templateContext.set("nameMissing", true);
+                               return;
+                       }
+                       String description = request.getHttpRequest().getPartAsStringFailsafe("description", 256).trim();
+                       Sone currentSone = getCurrentSone(request.getToadletContext());
+                       String parentId = request.getHttpRequest().getPartAsStringFailsafe("parent", 36);
+                       Album parent = webInterface.getCore().getAlbum(parentId, false);
+                       Album album = webInterface.getCore().createAlbum(currentSone, parent);
+                       album.setTitle(name).setDescription(description);
+                       webInterface.getCore().touchConfiguration();
+                       throw new RedirectException("imageBrowser.html?album=" + album.getId());
+               }
+       }
+
+}
index 147e4ae..22f6efa 100644 (file)
@@ -20,9 +20,10 @@ package net.pterodactylus.sone.web;
 import net.pterodactylus.sone.data.Post;
 import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.text.TextFilter;
-import net.pterodactylus.sone.web.page.Page.Request.Method;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.template.Template;
 import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.web.Method;
 
 /**
  * This page lets the user create a new {@link Post}.
@@ -51,7 +52,7 @@ public class CreatePostPage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+       protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
                super.processTemplate(request, templateContext);
                String returnPage = request.getHttpRequest().getPartAsStringFailsafe("returnPage", 256);
                if (request.getMethod() == Method.POST) {
index 2b12352..f28be35 100644 (file)
@@ -20,9 +20,10 @@ package net.pterodactylus.sone.web;
 import net.pterodactylus.sone.data.Post;
 import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.text.TextFilter;
-import net.pterodactylus.sone.web.page.Page.Request.Method;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.template.Template;
 import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.web.Method;
 
 /**
  * This page lets the user post a reply to a post.
@@ -51,7 +52,7 @@ public class CreateReplyPage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+       protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
                super.processTemplate(request, templateContext);
                String postId = request.getHttpRequest().getPartAsStringFailsafe("post", 36);
                String text = request.getHttpRequest().getPartAsStringFailsafe("text", 65536).trim();
index 3f940a3..9878358 100644 (file)
@@ -28,10 +28,11 @@ import java.util.logging.Logger;
 import net.pterodactylus.sone.core.Core;
 import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.freenet.wot.OwnIdentity;
-import net.pterodactylus.sone.web.page.Page.Request.Method;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.logging.Logging;
 import net.pterodactylus.util.template.Template;
 import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.web.Method;
 import freenet.clients.http.ToadletContext;
 
 /**
@@ -94,7 +95,7 @@ public class CreateSonePage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+       protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
                super.processTemplate(request, templateContext);
                List<OwnIdentity> ownIdentitiesWithoutSone = getOwnIdentitiesWithoutSone(webInterface.getCore());
                templateContext.set("identitiesWithoutSone", ownIdentitiesWithoutSone);
diff --git a/src/main/java/net/pterodactylus/sone/web/DeleteAlbumPage.java b/src/main/java/net/pterodactylus/sone/web/DeleteAlbumPage.java
new file mode 100644 (file)
index 0000000..d03e065
--- /dev/null
@@ -0,0 +1,78 @@
+/*
+ * Sone - DeleteAlbumPage.java - Copyright © 2011 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 net.pterodactylus.sone.data.Album;
+import net.pterodactylus.sone.web.page.FreenetRequest;
+import net.pterodactylus.util.template.Template;
+import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.web.Method;
+
+/**
+ * Page that lets the user delete an {@link Album}.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class DeleteAlbumPage extends SoneTemplatePage {
+
+       /**
+        * Creates a new “delete album” page.
+        *
+        * @param template
+        *            The template to render
+        * @param webInterface
+        *            The Sone web interface
+        */
+       public DeleteAlbumPage(Template template, WebInterface webInterface) {
+               super("deleteAlbum.html", template, "Page.DeleteAlbum.Title", webInterface, true);
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
+               super.processTemplate(request, templateContext);
+               if (request.getMethod() == Method.POST) {
+                       String albumId = request.getHttpRequest().getPartAsStringFailsafe("album", 36);
+                       Album album = webInterface.getCore().getAlbum(albumId, false);
+                       if (album == null) {
+                               throw new RedirectException("invalid.html");
+                       }
+                       if (!webInterface.getCore().isLocalSone(album.getSone())) {
+                               throw new RedirectException("noPermission.html");
+                       }
+                       if (request.getHttpRequest().isPartSet("abortDelete")) {
+                               throw new RedirectException("imageBrowser.html?album=" + album.getId());
+                       }
+                       Album parentAlbum = album.getParent();
+                       webInterface.getCore().deleteAlbum(album);
+                       if (parentAlbum == null) {
+                               throw new RedirectException("imageBrowser.html?sone=" + album.getSone().getId());
+                       }
+                       throw new RedirectException("imageBrowser.html?album=" + parentAlbum.getId());
+               }
+               String albumId = request.getHttpRequest().getParam("album");
+               Album album = webInterface.getCore().getAlbum(albumId, false);
+               if (album == null) {
+                       throw new RedirectException("invalid.html");
+               }
+               templateContext.set("album", album);
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/sone/web/DeleteImagePage.java b/src/main/java/net/pterodactylus/sone/web/DeleteImagePage.java
new file mode 100644 (file)
index 0000000..66098ff
--- /dev/null
@@ -0,0 +1,73 @@
+/*
+ * Sone - DeleteImagePage.java - Copyright © 2011 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 net.pterodactylus.sone.data.Image;
+import net.pterodactylus.sone.web.page.FreenetRequest;
+import net.pterodactylus.util.template.Template;
+import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.web.Method;
+
+/**
+ * Page that lets the user delete an {@link Image}.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class DeleteImagePage extends SoneTemplatePage {
+
+       /**
+        * Creates a new “delete image” page.
+        *
+        * @param template
+        *            The template to render
+        * @param webInterface
+        *            The Sone web interface
+        */
+       public DeleteImagePage(Template template, WebInterface webInterface) {
+               super("deleteImage.html", template, "Page.DeleteImage.Title", webInterface, true);
+       }
+
+       //
+       // SONETEMPLATEPAGE METHODS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
+               super.processTemplate(request, templateContext);
+               String imageId = (request.getMethod() == Method.POST) ? request.getHttpRequest().getPartAsStringFailsafe("image", 36) : request.getHttpRequest().getParam("image");
+               Image image = webInterface.getCore().getImage(imageId, false);
+               if (image == null) {
+                       throw new RedirectException("invalid.html");
+               }
+               if (!webInterface.getCore().isLocalSone(image.getSone())) {
+                       throw new RedirectException("noPermission.html");
+               }
+               if (request.getMethod() == Method.POST) {
+                       if (request.getHttpRequest().isPartSet("abortDelete")) {
+                               throw new RedirectException("imageBrowser.html?image=" + image.getId());
+                       }
+                       webInterface.getCore().deleteImage(image);
+                       throw new RedirectException("imageBrowser.html?album=" + image.getAlbum().getId());
+               }
+               templateContext.set("image", image);
+       }
+
+}
index 7a36d02..c8fd20f 100644 (file)
 package net.pterodactylus.sone.web;
 
 import net.pterodactylus.sone.data.Post;
-import net.pterodactylus.sone.web.page.Page.Request.Method;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.template.Template;
 import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.web.Method;
 
 /**
  * Lets the user delete a post they made.
@@ -49,7 +50,7 @@ public class DeletePostPage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+       protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
                super.processTemplate(request, templateContext);
                if (request.getMethod() == Method.GET) {
                        String postId = request.getHttpRequest().getParam("post");
index 82f2ace..030279b 100644 (file)
@@ -20,9 +20,10 @@ package net.pterodactylus.sone.web;
 import net.pterodactylus.sone.data.Profile;
 import net.pterodactylus.sone.data.Profile.Field;
 import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.sone.web.page.Page.Request.Method;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.template.Template;
 import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.web.Method;
 
 /**
  * Page that lets the user confirm the deletion of a profile field.
@@ -51,7 +52,7 @@ public class DeleteProfileFieldPage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+       protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
                super.processTemplate(request, templateContext);
                Sone currentSone = getCurrentSone(request.getToadletContext());
                Profile profile = currentSone.getProfile();
index 782589f..226e0d3 100644 (file)
 package net.pterodactylus.sone.web;
 
 import net.pterodactylus.sone.data.Reply;
-import net.pterodactylus.sone.web.page.Page.Request.Method;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.template.Template;
 import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.web.Method;
 
 /**
  * This page lets the user delete a reply.
@@ -49,7 +50,7 @@ public class DeleteReplyPage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+       protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
                super.processTemplate(request, templateContext);
                String replyId = request.getHttpRequest().getPartAsStringFailsafe("reply", 36);
                Reply reply = webInterface.getCore().getReply(replyId);
index 21979b8..bbf533f 100644 (file)
 package net.pterodactylus.sone.web;
 
 import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.sone.web.page.Page.Request.Method;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.template.Template;
 import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.web.Method;
 
 /**
  * Lets the user delete a Sone. Of course the Sone is not really deleted from
@@ -51,7 +52,7 @@ public class DeleteSonePage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+       protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
                super.processTemplate(request, templateContext);
                if (request.getMethod() == Method.POST) {
                        if (request.getHttpRequest().isPartSet("deleteSone")) {
index 15c5675..9d79195 100644 (file)
@@ -17,6 +17,7 @@
 
 package net.pterodactylus.sone.web;
 
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.notify.Notification;
 import net.pterodactylus.util.template.Template;
 import net.pterodactylus.util.template.TemplateContext;
@@ -48,7 +49,7 @@ public class DismissNotificationPage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+       protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
                super.processTemplate(request, templateContext);
                String notificationId = request.getHttpRequest().getPartAsStringFailsafe("notification", 36);
                Notification notification = webInterface.getNotifications().getNotification(notificationId);
index 37a792c..473bf1c 100644 (file)
@@ -19,9 +19,10 @@ package net.pterodactylus.sone.web;
 
 import net.pterodactylus.sone.core.Core;
 import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.sone.web.page.Page.Request.Method;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.template.Template;
 import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.web.Method;
 
 /**
  * Page that lets the user distrust another Sone. This will assign a
@@ -52,7 +53,7 @@ public class DistrustPage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+       protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
                super.processTemplate(request, templateContext);
                if (request.getMethod() == Method.POST) {
                        String returnPage = request.getHttpRequest().getPartAsStringFailsafe("returnPage", 256);
diff --git a/src/main/java/net/pterodactylus/sone/web/EditAlbumPage.java b/src/main/java/net/pterodactylus/sone/web/EditAlbumPage.java
new file mode 100644 (file)
index 0000000..fd4bf7c
--- /dev/null
@@ -0,0 +1,77 @@
+/*
+ * Sone - EditAlbumPage.java - Copyright © 2011 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 net.pterodactylus.sone.data.Album;
+import net.pterodactylus.sone.web.page.FreenetRequest;
+import net.pterodactylus.util.template.Template;
+import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.web.Method;
+
+/**
+ * Page that lets the user edit the name and description of an album.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class EditAlbumPage extends SoneTemplatePage {
+
+       /**
+        * Creates a new “edit album” page.
+        *
+        * @param template
+        *            The template to render
+        * @param webInterface
+        *            The Sone web interface
+        */
+       public EditAlbumPage(Template template, WebInterface webInterface) {
+               super("editAlbum.html", template, "Page.EditAlbum.Title", webInterface, true);
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
+               super.processTemplate(request, templateContext);
+               if (request.getMethod() == Method.POST) {
+                       String albumId = request.getHttpRequest().getPartAsStringFailsafe("album", 36);
+                       Album album = webInterface.getCore().getAlbum(albumId, false);
+                       if (album == null) {
+                               throw new RedirectException("invalid.html");
+                       }
+                       if (!webInterface.getCore().isLocalSone(album.getSone())) {
+                               throw new RedirectException("noPermission.html");
+                       }
+                       String albumImageId = request.getHttpRequest().getPartAsStringFailsafe("album-image", 36);
+                       if (webInterface.getCore().getImage(albumImageId, false) == null) {
+                               albumImageId = null;
+                       }
+                       album.setAlbumImage(albumImageId);
+                       String title = request.getHttpRequest().getPartAsStringFailsafe("title", 100).trim();
+                       if (title.length() == 0) {
+                               templateContext.set("titleMissing", true);
+                               return;
+                       }
+                       String description = request.getHttpRequest().getPartAsStringFailsafe("description", 1000).trim();
+                       album.setTitle(title).setDescription(description);
+                       webInterface.getCore().touchConfiguration();
+                       throw new RedirectException("imageBrowser.html?album=" + album.getId());
+               }
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/sone/web/EditImagePage.java b/src/main/java/net/pterodactylus/sone/web/EditImagePage.java
new file mode 100644 (file)
index 0000000..7e9eb7e
--- /dev/null
@@ -0,0 +1,83 @@
+/*
+ * FreenetSone - WebInterface.java - Copyright © 2010 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 net.pterodactylus.sone.data.Image;
+import net.pterodactylus.sone.web.page.FreenetRequest;
+import net.pterodactylus.util.template.Template;
+import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.web.Method;
+
+/**
+ * Page that lets the user edit title and description of an {@link Image}.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class EditImagePage extends SoneTemplatePage {
+
+       /**
+        * Creates a new “edit image” page.
+        *
+        * @param template
+        *            The template to render
+        * @param webInterface
+        *            The Sone web interface
+        */
+       public EditImagePage(Template template, WebInterface webInterface) {
+               super("editImage.html", template, "Page.EditImage.Title", webInterface, true);
+       }
+
+       //
+       // SONETEMPLATEPAGE METHODS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
+               super.processTemplate(request, templateContext);
+               if (request.getMethod() == Method.POST) {
+                       String imageId = request.getHttpRequest().getPartAsStringFailsafe("image", 36);
+                       String returnPage = request.getHttpRequest().getPartAsStringFailsafe("returnPage", 256);
+                       Image image = webInterface.getCore().getImage(imageId, false);
+                       if (image == null) {
+                               throw new RedirectException("invalid.html");
+                       }
+                       if (!webInterface.getCore().isLocalSone(image.getSone())) {
+                               throw new RedirectException("noPermission.html");
+                       }
+                       if ("true".equals(request.getHttpRequest().getPartAsStringFailsafe("moveLeft", 4))) {
+                               image.getAlbum().moveImageUp(image);
+                       } else  if ("true".equals(request.getHttpRequest().getPartAsStringFailsafe("moveRight", 4))) {
+                               image.getAlbum().moveImageDown(image);
+                       } else {
+                               String title = request.getHttpRequest().getPartAsStringFailsafe("title", 100).trim();
+                               String description = request.getHttpRequest().getPartAsStringFailsafe("description", 1024).trim();
+                               if (title.length() == 0) {
+                                       templateContext.set("titleMissing", true);
+                               }
+                               image.setTitle(title);
+                               image.setDescription(description);
+                       }
+                       webInterface.getCore().touchConfiguration();
+                       throw new RedirectException(returnPage);
+               }
+       }
+
+}
index 219bdc5..a06592a 100644 (file)
@@ -20,9 +20,10 @@ package net.pterodactylus.sone.web;
 import net.pterodactylus.sone.data.Profile;
 import net.pterodactylus.sone.data.Profile.Field;
 import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.sone.web.page.Page.Request.Method;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.template.Template;
 import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.web.Method;
 
 /**
  * Page that lets the user edit the name of a profile field.
@@ -51,7 +52,7 @@ public class EditProfileFieldPage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+       protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
                super.processTemplate(request, templateContext);
                Sone currentSone = getCurrentSone(request.getToadletContext());
                Profile profile = currentSone.getProfile();
index 43dd15b..59c7e3e 100644 (file)
@@ -22,10 +22,11 @@ import java.util.List;
 import net.pterodactylus.sone.data.Profile;
 import net.pterodactylus.sone.data.Profile.Field;
 import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.sone.web.page.Page.Request.Method;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.number.Numbers;
 import net.pterodactylus.util.template.Template;
 import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.web.Method;
 import freenet.clients.http.ToadletContext;
 
 /**
@@ -55,7 +56,7 @@ public class EditProfilePage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+       protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
                super.processTemplate(request, templateContext);
                ToadletContext toadletContenxt = request.getToadletContext();
                Sone currentSone = getCurrentSone(toadletContenxt);
@@ -153,7 +154,7 @@ public class EditProfilePage extends SoneTemplatePage {
         * @return The parsed ID, or {@code null} if there was no part matching the
         *         given string
         */
-       private String getFieldId(Request request, String partNameStart) {
+       private String getFieldId(FreenetRequest request, String partNameStart) {
                for (String partName : request.getHttpRequest().getParts()) {
                        if (partName.startsWith(partNameStart)) {
                                return partName.substring(partNameStart.length());
index 81225e9..143e0ba 100644 (file)
 package net.pterodactylus.sone.web;
 
 import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.sone.web.page.Page.Request.Method;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.template.Template;
 import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.web.Method;
 
 /**
  * This page lets the user follow another Sone.
@@ -47,7 +48,7 @@ public class FollowSonePage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+       protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
                super.processTemplate(request, templateContext);
                if (request.getMethod() == Method.POST) {
                        String returnPage = request.getHttpRequest().getPartAsStringFailsafe("returnPage", 256);
diff --git a/src/main/java/net/pterodactylus/sone/web/GetImagePage.java b/src/main/java/net/pterodactylus/sone/web/GetImagePage.java
new file mode 100644 (file)
index 0000000..29556c9
--- /dev/null
@@ -0,0 +1,77 @@
+/*
+ * Sone - GetImagePage.java - Copyright © 2011 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.IOException;
+
+import net.pterodactylus.sone.data.TemporaryImage;
+import net.pterodactylus.sone.web.page.FreenetRequest;
+import net.pterodactylus.util.web.Page;
+import net.pterodactylus.util.web.Response;
+
+/**
+ * Page that delivers a {@link TemporaryImage} to the browser.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class GetImagePage implements Page<FreenetRequest> {
+
+       /** The Sone web interface. */
+       private final WebInterface webInterface;
+
+       /**
+        * Creates a new “get image” page.
+        *
+        * @param webInterface
+        *            The Sone web interface
+        */
+       public GetImagePage(WebInterface webInterface) {
+               this.webInterface = webInterface;
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public String getPath() {
+               return "getImage.html";
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public boolean isPrefixPage() {
+               return false;
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public Response handleRequest(FreenetRequest request, Response response) throws IOException {
+               String imageId = request.getHttpRequest().getParam("image");
+               TemporaryImage temporaryImage = webInterface.getCore().getTemporaryImage(imageId);
+               if (temporaryImage == null) {
+                       return response.setStatusCode(404).setStatusText("Not found.").setContentType("text/html; charset=utf-8");
+               }
+               String contentType= temporaryImage.getMimeType();
+               return response.setStatusCode(200).setStatusText("OK").setContentType(contentType).addHeader("Content-Disposition", "attachment; filename=" + temporaryImage.getId() + "." + contentType.substring(contentType.lastIndexOf('/') + 1)).write(temporaryImage.getImageData());
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/sone/web/ImageBrowserPage.java b/src/main/java/net/pterodactylus/sone/web/ImageBrowserPage.java
new file mode 100644 (file)
index 0000000..ed31283
--- /dev/null
@@ -0,0 +1,79 @@
+/*
+ * Sone - ImageBrowserPage.java - Copyright © 2011 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 net.pterodactylus.sone.data.Album;
+import net.pterodactylus.sone.data.Image;
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.web.page.FreenetRequest;
+import net.pterodactylus.util.template.Template;
+import net.pterodactylus.util.template.TemplateContext;
+
+/**
+ * The image browser page is the entry page for the image management.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class ImageBrowserPage extends SoneTemplatePage {
+
+       /**
+        * Creates a new image browser page.
+        *
+        * @param template
+        *            The template to render
+        * @param webInterface
+        *            The Sone web interface
+        */
+       public ImageBrowserPage(Template template, WebInterface webInterface) {
+               super("imageBrowser.html", template, "Page.ImageBrowser.Title", webInterface, true);
+       }
+
+       //
+       // SONETEMPLATEPAGE METHODS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
+               super.processTemplate(request, templateContext);
+               String albumId = request.getHttpRequest().getParam("album", null);
+               if (albumId != null) {
+                       Album album = webInterface.getCore().getAlbum(albumId, false);
+                       templateContext.set("albumRequested", true);
+                       templateContext.set("album", album);
+                       return;
+               }
+               String imageId = request.getHttpRequest().getParam("image", null);
+               if (imageId != null) {
+                       Image image = webInterface.getCore().getImage(imageId, false);
+                       templateContext.set("imageRequested", true);
+                       templateContext.set("image", image);
+                       return;
+               }
+               Sone sone = getCurrentSone(request.getToadletContext(), false);
+               String soneId = request.getHttpRequest().getParam("sone", null);
+               if (soneId != null) {
+                       sone = webInterface.getCore().getSone(soneId, false);
+               }
+               templateContext.set("soneRequested", true);
+               templateContext.set("sone", sone);
+       }
+
+}
index 35b88a9..356b4ab 100644 (file)
@@ -24,6 +24,7 @@ import java.util.List;
 import net.pterodactylus.sone.data.Post;
 import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.notify.ListNotificationFilters;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.collection.Pagination;
 import net.pterodactylus.util.filter.Filter;
 import net.pterodactylus.util.filter.Filters;
@@ -57,7 +58,7 @@ public class IndexPage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+       protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
                super.processTemplate(request, templateContext);
                final Sone currentSone = getCurrentSone(request.getToadletContext());
                List<Post> allPosts = new ArrayList<Post>();
index ad63791..e5acad5 100644 (file)
@@ -22,6 +22,7 @@ import java.util.Collections;
 import java.util.List;
 
 import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.collection.Pagination;
 import net.pterodactylus.util.collection.ReverseComparator;
 import net.pterodactylus.util.filter.Filter;
@@ -57,7 +58,7 @@ public class KnownSonesPage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+       protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
                super.processTemplate(request, templateContext);
                String sortField = request.getHttpRequest().getParam("sort");
                String sortOrder = request.getHttpRequest().getParam("order");
@@ -107,4 +108,5 @@ public class KnownSonesPage extends SoneTemplatePage {
                templateContext.set("pagination", sonePagination);
                templateContext.set("knownSones", sonePagination.getItems());
        }
+
 }
index 56f55ef..c5174c6 100644 (file)
@@ -19,9 +19,10 @@ package net.pterodactylus.sone.web;
 
 import net.pterodactylus.sone.data.Post;
 import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.sone.web.page.Page.Request.Method;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.template.Template;
 import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.web.Method;
 
 /**
  * Page that lets the user like a {@link Post}.
@@ -50,7 +51,7 @@ public class LikePage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+       protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
                super.processTemplate(request, templateContext);
                if (request.getMethod() == Method.POST) {
                        String type = request.getHttpRequest().getPartAsStringFailsafe("type", 16);
index d09de66..a56d263 100644 (file)
@@ -18,6 +18,7 @@
 package net.pterodactylus.sone.web;
 
 import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.template.Template;
 import net.pterodactylus.util.template.TemplateContext;
 
@@ -49,7 +50,7 @@ public class LockSonePage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+       protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
                super.processTemplate(request, templateContext);
                String soneId = request.getHttpRequest().getPartAsStringFailsafe("sone", 44);
                Sone sone = webInterface.getCore().getLocalSone(soneId, false);
index 8e612ea..6f43b6f 100644 (file)
@@ -24,10 +24,11 @@ import java.util.logging.Logger;
 
 import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.freenet.wot.OwnIdentity;
-import net.pterodactylus.sone.web.page.Page.Request.Method;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.logging.Logging;
 import net.pterodactylus.util.template.Template;
 import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.web.Method;
 import freenet.clients.http.ToadletContext;
 
 /**
@@ -61,7 +62,7 @@ public class LoginPage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+       protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
                super.processTemplate(request, templateContext);
                /* get all own identities. */
                List<Sone> localSones = new ArrayList<Sone>(webInterface.getCore().getLocalSones());
@@ -87,7 +88,7 @@ public class LoginPage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected String getRedirectTarget(Request request) {
+       protected String getRedirectTarget(FreenetRequest request) {
                if (getCurrentSone(request.getToadletContext(), false) != null) {
                        return "index.html";
                }
index 7cd0587..a7f769f 100644 (file)
@@ -17,6 +17,7 @@
 
 package net.pterodactylus.sone.web;
 
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.template.Template;
 import net.pterodactylus.util.template.TemplateContext;
 import freenet.clients.http.ToadletContext;
@@ -46,7 +47,7 @@ public class LogoutPage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+       protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
                setCurrentSone(request.getToadletContext(), null);
                super.processTemplate(request, templateContext);
                throw new RedirectException("index.html");
index 9f91c84..987fa1f 100644 (file)
@@ -22,6 +22,7 @@ import java.util.StringTokenizer;
 import net.pterodactylus.sone.data.Post;
 import net.pterodactylus.sone.data.Reply;
 import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.template.Template;
 import net.pterodactylus.util.template.TemplateContext;
 
@@ -53,7 +54,7 @@ public class MarkAsKnownPage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+       protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
                super.processTemplate(request, templateContext);
                String type = request.getHttpRequest().getPartAsStringFailsafe("type", 5);
                if (!type.equals("sone") && !type.equals("post") && !type.equals("reply")) {
index fda6afd..75e73a0 100644 (file)
@@ -23,10 +23,11 @@ import java.util.List;
 import net.pterodactylus.sone.core.Core.Preferences;
 import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.fcp.FcpInterface.FullAccessRequired;
-import net.pterodactylus.sone.web.page.Page.Request.Method;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.number.Numbers;
 import net.pterodactylus.util.template.Template;
 import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.web.Method;
 
 /**
  * This page lets the user edit the options of the Sone plugin.
@@ -55,7 +56,7 @@ public class OptionsPage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+       protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
                super.processTemplate(request, templateContext);
                Preferences preferences = webInterface.getCore().getPreferences();
                Sone currentSone = webInterface.getCurrentSone(request.getToadletContext(), false);
index 40b03f1..f653615 100644 (file)
@@ -19,10 +19,11 @@ package net.pterodactylus.sone.web;
 
 import net.pterodactylus.sone.core.SoneRescuer;
 import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.sone.web.page.Page.Request.Method;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.number.Numbers;
 import net.pterodactylus.util.template.Template;
 import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.web.Method;
 
 /**
  * Page that lets the user control the rescue mode for a Sone.
@@ -52,7 +53,7 @@ public class RescuePage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+       protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
                super.processTemplate(request, templateContext);
                Sone currentSone = getCurrentSone(request.getToadletContext(), false);
                SoneRescuer soneRescuer = webInterface.getCore().getSoneRescuer(currentSone);
index fab7a57..91e2a08 100644 (file)
@@ -32,6 +32,7 @@ import net.pterodactylus.sone.data.Profile;
 import net.pterodactylus.sone.data.Profile.Field;
 import net.pterodactylus.sone.data.Reply;
 import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.cache.Cache;
 import net.pterodactylus.util.cache.CacheException;
 import net.pterodactylus.util.cache.CacheItem;
@@ -96,7 +97,7 @@ public class SearchPage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+       protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
                super.processTemplate(request, templateContext);
                String query = request.getHttpRequest().getParam("query").trim();
                if (query.length() == 0) {
index b7e36f4..095db9e 100644 (file)
@@ -27,8 +27,8 @@ import java.util.Map;
 import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.main.SonePlugin;
 import net.pterodactylus.sone.notify.ListNotificationFilters;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.sone.web.page.FreenetTemplatePage;
-import net.pterodactylus.sone.web.page.Page;
 import net.pterodactylus.util.collection.ListBuilder;
 import net.pterodactylus.util.collection.MapBuilder;
 import net.pterodactylus.util.template.Template;
@@ -202,7 +202,7 @@ public class SoneTemplatePage extends FreenetTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected String getPageTitle(Request request) {
+       protected String getPageTitle(FreenetRequest request) {
                if (pageTitleKey != null) {
                        return webInterface.getL10n().getString(pageTitleKey);
                }
@@ -213,7 +213,7 @@ public class SoneTemplatePage extends FreenetTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected List<Map<String, String>> getAdditionalLinkNodes(Request request) {
+       protected List<Map<String, String>> getAdditionalLinkNodes(FreenetRequest request) {
                return new ListBuilder<Map<String, String>>().add(new MapBuilder<String, String>().put("rel", "search").put("type", "application/opensearchdescription+xml").put("title", "Sone").put("href", "http://" + request.getHttpRequest().getHeader("host") + "/Sone/OpenSearch.xml").get()).get();
        }
 
@@ -247,7 +247,7 @@ public class SoneTemplatePage extends FreenetTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+       protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
                super.processTemplate(request, templateContext);
                Sone currentSone = getCurrentSone(request.getToadletContext(), false);
                templateContext.set("core", webInterface.getCore());
@@ -266,7 +266,7 @@ public class SoneTemplatePage extends FreenetTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected String getRedirectTarget(Page.Request request) {
+       protected String getRedirectTarget(FreenetRequest request) {
                if (requiresLogin() && (getCurrentSone(request.getToadletContext(), false) == null)) {
                        HTTPRequest httpRequest = request.getHttpRequest();
                        String originalUrl = httpRequest.getPath();
index b0dadef..f2cf92e 100644 (file)
@@ -19,9 +19,10 @@ package net.pterodactylus.sone.web;
 
 import net.pterodactylus.sone.core.Core;
 import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.sone.web.page.Page.Request.Method;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.template.Template;
 import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.web.Method;
 
 /**
  * Page that lets the user trust another Sone. This will assign a configurable
@@ -52,7 +53,7 @@ public class TrustPage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+       protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
                super.processTemplate(request, templateContext);
                if (request.getMethod() == Method.POST) {
                        String returnPage = request.getHttpRequest().getPartAsStringFailsafe("returnPage", 256);
index 60165b2..def3bf4 100644 (file)
@@ -20,9 +20,10 @@ package net.pterodactylus.sone.web;
 import java.util.Set;
 
 import net.pterodactylus.sone.data.Post;
-import net.pterodactylus.sone.web.page.Page.Request.Method;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.template.Template;
 import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.web.Method;
 
 /**
  * Page that lets the user unbookmark a post.
@@ -49,7 +50,7 @@ public class UnbookmarkPage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+       protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
                super.processTemplate(request, templateContext);
                if (request.getMethod() == Method.POST) {
                        String id = request.getHttpRequest().getPartAsStringFailsafe("post", 36);
index 33b88a1..d8e53ce 100644 (file)
 package net.pterodactylus.sone.web;
 
 import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.sone.web.page.Page.Request.Method;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.template.Template;
 import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.web.Method;
 
 /**
  * This page lets the user unfollow another Sone.
@@ -47,7 +48,7 @@ public class UnfollowSonePage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+       protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
                super.processTemplate(request, templateContext);
                if (request.getMethod() == Method.POST) {
                        String returnPage = request.getHttpRequest().getPartAsStringFailsafe("returnPage", 256);
index 24ff3ca..f47c4f2 100644 (file)
@@ -19,9 +19,10 @@ package net.pterodactylus.sone.web;
 
 import net.pterodactylus.sone.data.Post;
 import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.sone.web.page.Page.Request.Method;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.template.Template;
 import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.web.Method;
 
 /**
  * Page that lets the user unlike a {@link Post}.
@@ -50,7 +51,7 @@ public class UnlikePage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+       protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
                super.processTemplate(request, templateContext);
                if (request.getMethod() == Method.POST) {
                        String type = request.getHttpRequest().getPartAsStringFailsafe("type", 16);
index ccb5959..f527558 100644 (file)
@@ -18,6 +18,7 @@
 package net.pterodactylus.sone.web;
 
 import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.template.Template;
 import net.pterodactylus.util.template.TemplateContext;
 
@@ -48,7 +49,7 @@ public class UnlockSonePage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+       protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
                super.processTemplate(request, templateContext);
                String soneId = request.getHttpRequest().getPartAsStringFailsafe("sone", 44);
                Sone sone = webInterface.getCore().getLocalSone(soneId, false);
index c5e7dec..d711525 100644 (file)
@@ -19,9 +19,10 @@ package net.pterodactylus.sone.web;
 
 import net.pterodactylus.sone.core.Core;
 import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.sone.web.page.Page.Request.Method;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.template.Template;
 import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.web.Method;
 
 /**
  * Page that lets the user untrust another Sone. This will remove all trust
@@ -52,7 +53,7 @@ public class UntrustPage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+       protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
                super.processTemplate(request, templateContext);
                if (request.getMethod() == Method.POST) {
                        String returnPage = request.getHttpRequest().getPartAsStringFailsafe("returnPage", 256);
diff --git a/src/main/java/net/pterodactylus/sone/web/UploadImagePage.java b/src/main/java/net/pterodactylus/sone/web/UploadImagePage.java
new file mode 100644 (file)
index 0000000..e0aa1e1
--- /dev/null
@@ -0,0 +1,161 @@
+/*
+ * Sone - UploadImagePage.java - Copyright © 2011 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.awt.Image;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Iterator;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import javax.imageio.ImageIO;
+import javax.imageio.ImageReader;
+import javax.imageio.stream.ImageInputStream;
+
+import net.pterodactylus.sone.data.Album;
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.data.TemporaryImage;
+import net.pterodactylus.sone.web.page.FreenetRequest;
+import net.pterodactylus.util.io.Closer;
+import net.pterodactylus.util.io.StreamCopier;
+import net.pterodactylus.util.logging.Logging;
+import net.pterodactylus.util.template.Template;
+import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.web.Method;
+import freenet.support.api.Bucket;
+import freenet.support.api.HTTPUploadedFile;
+
+/**
+ * Page implementation that lets the user upload an image.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class UploadImagePage extends SoneTemplatePage {
+
+       /** The logger. */
+       private static final Logger logger = Logging.getLogger(UploadImagePage.class);
+
+       /**
+        * Creates a new “upload image” page.
+        *
+        * @param template
+        *            The template to render
+        * @param webInterface
+        *            The Sone web interface
+        */
+       public UploadImagePage(Template template, WebInterface webInterface) {
+               super("uploadImage.html", template, "Page.UploadImage.Title", webInterface, true);
+       }
+
+       //
+       // SONETEMPLATEPAGE METHODS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
+               super.processTemplate(request, templateContext);
+               if (request.getMethod() == Method.POST) {
+                       Sone currentSone = getCurrentSone(request.getToadletContext());
+                       String parentId = request.getHttpRequest().getPartAsStringFailsafe("parent", 36);
+                       Album parent = webInterface.getCore().getAlbum(parentId, false);
+                       if (parent == null) {
+                               /* TODO - signal error */
+                               return;
+                       }
+                       if (!currentSone.equals(parent.getSone())) {
+                               /* TODO - signal error. */
+                               return;
+                       }
+                       String name = request.getHttpRequest().getPartAsStringFailsafe("title", 200);
+                       String description = request.getHttpRequest().getPartAsStringFailsafe("description", 4000);
+                       HTTPUploadedFile uploadedFile = request.getHttpRequest().getUploadedFile("image");
+                       Bucket fileBucket = uploadedFile.getData();
+                       InputStream imageInputStream = null;
+                       ByteArrayOutputStream imageDataOutputStream = null;
+                       net.pterodactylus.sone.data.Image image = null;
+                       try {
+                               imageInputStream = fileBucket.getInputStream();
+                               /* TODO - check length */
+                               imageDataOutputStream = new ByteArrayOutputStream((int) fileBucket.size());
+                               StreamCopier.copy(imageInputStream, imageDataOutputStream);
+                       } catch (IOException ioe1) {
+                               logger.log(Level.WARNING, "Could not read uploaded image!", ioe1);
+                               return;
+                       } finally {
+                               fileBucket.free();
+                               Closer.close(imageInputStream);
+                               Closer.close(imageDataOutputStream);
+                       }
+                       byte[] imageData = imageDataOutputStream.toByteArray();
+                       ByteArrayInputStream imageDataInputStream = null;
+                       Image uploadedImage = null;
+                       try {
+                               imageDataInputStream = new ByteArrayInputStream(imageData);
+                               uploadedImage = ImageIO.read(imageDataInputStream);
+                               if (uploadedImage == null) {
+                                       templateContext.set("messages", webInterface.getL10n().getString("Page.UploadImage.Error.InvalidImage"));
+                                       return;
+                               }
+                               String mimeType = getMimeType(imageData);
+                               TemporaryImage temporaryImage = webInterface.getCore().createTemporaryImage(mimeType, imageData);
+                               image = webInterface.getCore().createImage(currentSone, parent, temporaryImage);
+                               image.setTitle(name).setDescription(description).setWidth(uploadedImage.getWidth(null)).setHeight(uploadedImage.getHeight(null));
+                       } catch (IOException ioe1) {
+                               logger.log(Level.WARNING, "Could not read uploaded image!", ioe1);
+                               return;
+                       } finally {
+                               Closer.close(imageDataInputStream);
+                               Closer.flush(uploadedImage);
+                       }
+                       throw new RedirectException("imageBrowser.html?album=" + parent.getId());
+               }
+       }
+
+       //
+       // PRIVATE METHODS
+       //
+
+       /**
+        * Tries to detect the MIME type of the encoded image.
+        *
+        * @param imageData
+        *            The encoded image
+        * @return The MIME type of the image, or “application/octet-stream” if the
+        *         image type could not be detected
+        */
+       private String getMimeType(byte[] imageData) {
+               ByteArrayInputStream imageDataInputStream = new ByteArrayInputStream(imageData);
+               try {
+                       ImageInputStream imageInputStream = ImageIO.createImageInputStream(imageDataInputStream);
+                       Iterator<ImageReader> imageReaders = ImageIO.getImageReaders(imageInputStream);
+                       if (imageReaders.hasNext()) {
+                               return imageReaders.next().getOriginatingProvider().getMIMETypes()[0];
+                       }
+               } catch (IOException ioe1) {
+                       logger.log(Level.FINE, "Could not detect MIME type for image.", ioe1);
+               }
+               return "application/octet-stream";
+       }
+
+}
index b62e19c..4cc9864 100644 (file)
@@ -19,6 +19,7 @@ package net.pterodactylus.sone.web;
 
 import net.pterodactylus.sone.data.Post;
 import net.pterodactylus.sone.template.SoneAccessor;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.template.Template;
 import net.pterodactylus.util.template.TemplateContext;
 
@@ -49,7 +50,7 @@ public class ViewPostPage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected String getPageTitle(Request request) {
+       protected String getPageTitle(FreenetRequest request) {
                String postId = request.getHttpRequest().getParam("post");
                Post post = webInterface.getCore().getPost(postId, false);
                String title = "";
@@ -65,7 +66,7 @@ public class ViewPostPage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+       protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
                super.processTemplate(request, templateContext);
                String postId = request.getHttpRequest().getParam("post");
                boolean raw = request.getHttpRequest().getParam("raw").equals("true");
index 889c74c..5d463c0 100644 (file)
@@ -29,6 +29,7 @@ import net.pterodactylus.sone.data.Post;
 import net.pterodactylus.sone.data.Reply;
 import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.template.SoneAccessor;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.collection.Pagination;
 import net.pterodactylus.util.number.Numbers;
 import net.pterodactylus.util.template.Template;
@@ -61,7 +62,7 @@ public class ViewSonePage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected String getPageTitle(Request request) {
+       protected String getPageTitle(FreenetRequest request) {
                String soneId = request.getHttpRequest().getParam("sone");
                Sone sone = webInterface.getCore().getSone(soneId, false);
                if ((sone != null) && (sone.getTime() > 0)) {
@@ -75,7 +76,7 @@ public class ViewSonePage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+       protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
                super.processTemplate(request, templateContext);
                String soneId = request.getHttpRequest().getParam("sone");
                Sone sone = webInterface.getCore().getSone(soneId, false);
index 08202d4..283e156 100644 (file)
@@ -37,6 +37,8 @@ import java.util.logging.Logger;
 
 import net.pterodactylus.sone.core.Core;
 import net.pterodactylus.sone.core.CoreListener;
+import net.pterodactylus.sone.data.Album;
+import net.pterodactylus.sone.data.Image;
 import net.pterodactylus.sone.data.Post;
 import net.pterodactylus.sone.data.Reply;
 import net.pterodactylus.sone.data.Sone;
@@ -45,10 +47,12 @@ import net.pterodactylus.sone.freenet.wot.Identity;
 import net.pterodactylus.sone.freenet.wot.Trust;
 import net.pterodactylus.sone.main.SonePlugin;
 import net.pterodactylus.sone.notify.ListNotification;
+import net.pterodactylus.sone.template.AlbumAccessor;
 import net.pterodactylus.sone.template.CollectionAccessor;
 import net.pterodactylus.sone.template.CssClassNameFilter;
 import net.pterodactylus.sone.template.HttpRequestAccessor;
 import net.pterodactylus.sone.template.IdentityAccessor;
+import net.pterodactylus.sone.template.ImageLinkFilter;
 import net.pterodactylus.sone.template.JavascriptFilter;
 import net.pterodactylus.sone.template.ParserFilter;
 import net.pterodactylus.sone.template.PostAccessor;
@@ -71,6 +75,8 @@ import net.pterodactylus.sone.web.ajax.DeleteProfileFieldAjaxPage;
 import net.pterodactylus.sone.web.ajax.DeleteReplyAjaxPage;
 import net.pterodactylus.sone.web.ajax.DismissNotificationAjaxPage;
 import net.pterodactylus.sone.web.ajax.DistrustAjaxPage;
+import net.pterodactylus.sone.web.ajax.EditAlbumAjaxPage;
+import net.pterodactylus.sone.web.ajax.EditImageAjaxPage;
 import net.pterodactylus.sone.web.ajax.EditProfileFieldAjaxPage;
 import net.pterodactylus.sone.web.ajax.FollowSoneAjaxPage;
 import net.pterodactylus.sone.web.ajax.GetLikesAjaxPage;
@@ -90,11 +96,9 @@ import net.pterodactylus.sone.web.ajax.UnfollowSoneAjaxPage;
 import net.pterodactylus.sone.web.ajax.UnlikeAjaxPage;
 import net.pterodactylus.sone.web.ajax.UnlockSoneAjaxPage;
 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.sone.web.page.RedirectPage;
-import net.pterodactylus.sone.web.page.StaticPage;
-import net.pterodactylus.sone.web.page.TemplatePage;
 import net.pterodactylus.util.cache.Cache;
 import net.pterodactylus.util.cache.CacheException;
 import net.pterodactylus.util.cache.CacheItem;
@@ -113,6 +117,7 @@ import net.pterodactylus.util.template.DateFilter;
 import net.pterodactylus.util.template.FormatFilter;
 import net.pterodactylus.util.template.HtmlFilter;
 import net.pterodactylus.util.template.MatchFilter;
+import net.pterodactylus.util.template.ModFilter;
 import net.pterodactylus.util.template.Provider;
 import net.pterodactylus.util.template.ReflectionAccessor;
 import net.pterodactylus.util.template.ReplaceFilter;
@@ -125,10 +130,13 @@ import net.pterodactylus.util.template.TemplateParser;
 import net.pterodactylus.util.template.XmlFilter;
 import net.pterodactylus.util.thread.Ticker;
 import net.pterodactylus.util.version.Version;
+import net.pterodactylus.util.web.RedirectPage;
+import net.pterodactylus.util.web.StaticPage;
+import net.pterodactylus.util.web.TemplatePage;
 import freenet.clients.http.SessionManager;
-import freenet.clients.http.SessionManager.Session;
 import freenet.clients.http.ToadletContainer;
 import freenet.clients.http.ToadletContext;
+import freenet.clients.http.SessionManager.Session;
 import freenet.l10n.BaseL10n;
 import freenet.support.api.HTTPRequest;
 
@@ -191,6 +199,15 @@ public class WebInterface implements CoreListener {
        /** The “new version” notification. */
        private final TemplateNotification newVersionNotification;
 
+       /** The “inserting images” notification. */
+       private final ListNotification<Image> insertingImagesNotification;
+
+       /** The “inserted images” notification. */
+       private final ListNotification<Image> insertedImagesNotification;
+
+       /** The “image insert failed” notification. */
+       private final ListNotification<Image> imageInsertFailedNotification;
+
        /**
         * Creates a new web interface.
         *
@@ -209,6 +226,7 @@ public class WebInterface implements CoreListener {
                templateContextFactory.addAccessor(Sone.class, new SoneAccessor(getCore()));
                templateContextFactory.addAccessor(Post.class, new PostAccessor(getCore()));
                templateContextFactory.addAccessor(Reply.class, new ReplyAccessor(getCore()));
+               templateContextFactory.addAccessor(Album.class, new AlbumAccessor());
                templateContextFactory.addAccessor(Identity.class, new IdentityAccessor(getCore()));
                templateContextFactory.addAccessor(Trust.class, new TrustAccessor());
                templateContextFactory.addAccessor(HTTPRequest.class, new HttpRequestAccessor());
@@ -227,9 +245,11 @@ public class WebInterface implements CoreListener {
                templateContextFactory.addFilter("unknown", new UnknownDateFilter(getL10n(), "View.Sone.Text.UnknownDate"));
                templateContextFactory.addFilter("format", new FormatFilter());
                templateContextFactory.addFilter("sort", new CollectionSortFilter());
+               templateContextFactory.addFilter("image-link", new ImageLinkFilter(templateContextFactory));
                templateContextFactory.addFilter("replyGroup", new ReplyGroupFilter());
                templateContextFactory.addFilter("in", new ContainsFilter());
                templateContextFactory.addFilter("unique", new UniqueElementFilter());
+               templateContextFactory.addFilter("mod", new ModFilter());
                templateContextFactory.addProvider(Provider.TEMPLATE_CONTEXT_PROVIDER);
                templateContextFactory.addProvider(new ClassPathTemplateProvider());
                templateContextFactory.addTemplateObject("webInterface", this);
@@ -259,6 +279,15 @@ public class WebInterface implements CoreListener {
 
                Template newVersionTemplate = TemplateParser.parse(createReader("/templates/notify/newVersionNotification.html"));
                newVersionNotification = new TemplateNotification("new-version-notification", newVersionTemplate);
+
+               Template insertingImagesTemplate = TemplateParser.parse(createReader("/templates/notify/inserting-images-notification.html"));
+               insertingImagesNotification = new ListNotification<Image>("inserting-images-notification", "images", insertingImagesTemplate);
+
+               Template insertedImagesTemplate = TemplateParser.parse(createReader("/templates/notify/inserted-images-notification.html"));
+               insertedImagesNotification = new ListNotification<Image>("inserted-images-notification", "images", insertedImagesTemplate);
+
+               Template imageInsertFailedTemplate = TemplateParser.parse(createReader("/templates/notify/image-insert-failed-notification.html"));
+               imageInsertFailedNotification = new ListNotification<Image>("image-insert-failed-notification", "images", imageInsertFailedTemplate);
        }
 
        //
@@ -559,6 +588,10 @@ public class WebInterface implements CoreListener {
                Template deletePostTemplate = TemplateParser.parse(createReader("/templates/deletePost.html"));
                Template deleteReplyTemplate = TemplateParser.parse(createReader("/templates/deleteReply.html"));
                Template deleteSoneTemplate = TemplateParser.parse(createReader("/templates/deleteSone.html"));
+               Template imageBrowserTemplate = TemplateParser.parse(createReader("/templates/imageBrowser.html"));
+               Template createAlbumTemplate = TemplateParser.parse(createReader("/templates/createAlbum.html"));
+               Template deleteAlbumTemplate = TemplateParser.parse(createReader("/templates/deleteAlbum.html"));
+               Template deleteImageTemplate = TemplateParser.parse(createReader("/templates/deleteImage.html"));
                Template noPermissionTemplate = TemplateParser.parse(createReader("/templates/noPermission.html"));
                Template optionsTemplate = TemplateParser.parse(createReader("/templates/options.html"));
                Template rescueTemplate = TemplateParser.parse(createReader("/templates/rescue.html"));
@@ -569,7 +602,7 @@ public class WebInterface implements CoreListener {
                Template openSearchTemplate = TemplateParser.parse(createReader("/templates/xml/OpenSearch.xml"));
 
                PageToadletFactory pageToadletFactory = new PageToadletFactory(sonePlugin.pluginRespirator().getHLSimpleClient(), "/Sone/");
-               pageToadlets.add(pageToadletFactory.createPageToadlet(new RedirectPage("", "index.html")));
+               pageToadlets.add(pageToadletFactory.createPageToadlet(new RedirectPage<FreenetRequest>("", "index.html")));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new IndexPage(indexTemplate, this), "Index"));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new CreateSonePage(createSoneTemplate, this), "CreateSone"));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new KnownSonesPage(knownSonesTemplate, this), "KnownSones"));
@@ -588,6 +621,13 @@ public class WebInterface implements CoreListener {
                pageToadlets.add(pageToadletFactory.createPageToadlet(new UnlockSonePage(emptyTemplate, this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new FollowSonePage(emptyTemplate, this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new UnfollowSonePage(emptyTemplate, this)));
+               pageToadlets.add(pageToadletFactory.createPageToadlet(new ImageBrowserPage(imageBrowserTemplate, this), "ImageBrowser"));
+               pageToadlets.add(pageToadletFactory.createPageToadlet(new CreateAlbumPage(createAlbumTemplate, this)));
+               pageToadlets.add(pageToadletFactory.createPageToadlet(new EditAlbumPage(emptyTemplate, this)));
+               pageToadlets.add(pageToadletFactory.createPageToadlet(new DeleteAlbumPage(deleteAlbumTemplate, this)));
+               pageToadlets.add(pageToadletFactory.createPageToadlet(new UploadImagePage(invalidTemplate, this)));
+               pageToadlets.add(pageToadletFactory.createPageToadlet(new EditImagePage(emptyTemplate, this)));
+               pageToadlets.add(pageToadletFactory.createPageToadlet(new DeleteImagePage(deleteImageTemplate, this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new TrustPage(emptyTemplate, this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new DistrustPage(emptyTemplate, this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new UntrustPage(emptyTemplate, this)));
@@ -605,10 +645,11 @@ public class WebInterface implements CoreListener {
                pageToadlets.add(pageToadletFactory.createPageToadlet(new SoneTemplatePage("noPermission.html", noPermissionTemplate, "Page.NoPermission.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("css/", "/static/css/", "text/css")));
-               pageToadlets.add(pageToadletFactory.createPageToadlet(new StaticPage("javascript/", "/static/javascript/", "text/javascript")));
-               pageToadlets.add(pageToadletFactory.createPageToadlet(new StaticPage("images/", "/static/images/", "image/png")));
-               pageToadlets.add(pageToadletFactory.createPageToadlet(new TemplatePage("OpenSearch.xml", "application/opensearchdescription+xml", templateContextFactory, openSearchTemplate)));
+               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(new TemplatePage<FreenetRequest>("OpenSearch.xml", "application/opensearchdescription+xml", templateContextFactory, openSearchTemplate)));
+               pageToadlets.add(pageToadletFactory.createPageToadlet(new GetImagePage(this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new GetTranslationPage(this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new GetStatusAjaxPage(this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new GetNotificationAjaxPage(this)));
@@ -625,6 +666,8 @@ public class WebInterface implements CoreListener {
                pageToadlets.add(pageToadletFactory.createPageToadlet(new UnlockSoneAjaxPage(this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new FollowSoneAjaxPage(this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new UnfollowSoneAjaxPage(this)));
+               pageToadlets.add(pageToadletFactory.createPageToadlet(new EditAlbumAjaxPage(this)));
+               pageToadlets.add(pageToadletFactory.createPageToadlet(new EditImageAjaxPage(this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new TrustAjaxPage(this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new DistrustAjaxPage(this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new UntrustAjaxPage(this)));
@@ -771,7 +814,7 @@ public class WebInterface implements CoreListener {
                }
                if (!hasFirstStartNotification()) {
                        notificationManager.addNotification(isLocal ? localReplyNotification : newReplyNotification);
-                       if (!getMentionedSones(reply.getText()).isEmpty() && !isLocal) {
+                       if (!getMentionedSones(reply.getText()).isEmpty() && !isLocal && (reply.getPost().getSone() != null) && (reply.getTime() <= System.currentTimeMillis())) {
                                mentionNotification.add(reply.getPost());
                                notificationManager.addNotification(mentionNotification);
                        }
@@ -911,6 +954,43 @@ public class WebInterface implements CoreListener {
        }
 
        /**
+        * {@inheritDoc}
+        */
+       @Override
+       public void imageInsertStarted(Image image) {
+               insertingImagesNotification.add(image);
+               notificationManager.addNotification(insertingImagesNotification);
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public void imageInsertAborted(Image image) {
+               insertingImagesNotification.remove(image);
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public void imageInsertFinished(Image image) {
+               insertingImagesNotification.remove(image);
+               insertedImagesNotification.add(image);
+               notificationManager.addNotification(insertedImagesNotification);
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public void imageInsertFailed(Image image, Throwable cause) {
+               insertingImagesNotification.remove(image);
+               imageInsertFailedNotification.add(image);
+               notificationManager.addNotification(imageInsertFailedNotification);
+       }
+
+       /**
         * Template provider implementation that uses
         * {@link WebInterface#createReader(String)} to load templates for
         * inclusion.
index 760bd34..0cf7b01 100644 (file)
@@ -18,6 +18,7 @@
 package net.pterodactylus.sone.web.ajax;
 
 import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.json.JsonObject;
 
 /**
@@ -45,7 +46,7 @@ public class BookmarkAjaxPage extends JsonPage {
         * {@inheritDoc}
         */
        @Override
-       protected JsonObject createJsonObject(Request request) {
+       protected JsonObject createJsonObject(FreenetRequest request) {
                String id = request.getHttpRequest().getParam("post", null);
                if ((id == null) || (id.length() == 0)) {
                        return createErrorJsonObject("invalid-post-id");
index 0fc1236..c6f4455 100644 (file)
@@ -21,6 +21,7 @@ import net.pterodactylus.sone.data.Post;
 import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.text.TextFilter;
 import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.json.JsonObject;
 
 /**
@@ -44,7 +45,7 @@ public class CreatePostAjaxPage extends JsonPage {
         * {@inheritDoc}
         */
        @Override
-       protected JsonObject createJsonObject(Request request) {
+       protected JsonObject createJsonObject(FreenetRequest request) {
                Sone sone = getCurrentSone(request.getToadletContext());
                if (sone == null) {
                        return createErrorJsonObject("auth-required");
index ce78f58..9f5c882 100644 (file)
@@ -22,6 +22,7 @@ import net.pterodactylus.sone.data.Reply;
 import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.text.TextFilter;
 import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.json.JsonObject;
 
 /**
@@ -49,7 +50,7 @@ public class CreateReplyAjaxPage extends JsonPage {
         * {@inheritDoc}
         */
        @Override
-       protected JsonObject createJsonObject(Request request) {
+       protected JsonObject createJsonObject(FreenetRequest request) {
                String postId = request.getHttpRequest().getParam("post");
                String text = request.getHttpRequest().getParam("text").trim();
                String senderId = request.getHttpRequest().getParam("sender");
index 8d3b414..37ab040 100644 (file)
@@ -19,6 +19,7 @@ package net.pterodactylus.sone.web.ajax;
 
 import net.pterodactylus.sone.data.Post;
 import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.json.JsonObject;
 
 /**
@@ -46,7 +47,7 @@ public class DeletePostAjaxPage extends JsonPage {
         * {@inheritDoc}
         */
        @Override
-       protected JsonObject createJsonObject(Request request) {
+       protected JsonObject createJsonObject(FreenetRequest request) {
                String postId = request.getHttpRequest().getParam("post");
                Post post = webInterface.getCore().getPost(postId, false);
                if ((post == null) || (post.getSone() == null)) {
index 4625c13..205fb85 100644 (file)
@@ -21,6 +21,7 @@ import net.pterodactylus.sone.data.Profile;
 import net.pterodactylus.sone.data.Profile.Field;
 import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.json.JsonObject;
 
 /**
@@ -44,7 +45,7 @@ public class DeleteProfileFieldAjaxPage extends JsonPage {
         * {@inheritDoc}
         */
        @Override
-       protected JsonObject createJsonObject(Request request) {
+       protected JsonObject createJsonObject(FreenetRequest request) {
                String fieldId = request.getHttpRequest().getParam("field");
                Sone currentSone = getCurrentSone(request.getToadletContext());
                Profile profile = currentSone.getProfile();
index e12b2cf..7bea4cf 100644 (file)
@@ -19,6 +19,7 @@ package net.pterodactylus.sone.web.ajax;
 
 import net.pterodactylus.sone.data.Reply;
 import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.json.JsonObject;
 
 /**
@@ -46,7 +47,7 @@ public class DeleteReplyAjaxPage extends JsonPage {
         * {@inheritDoc}
         */
        @Override
-       protected JsonObject createJsonObject(Request request) {
+       protected JsonObject createJsonObject(FreenetRequest request) {
                String replyId = request.getHttpRequest().getParam("reply");
                Reply reply = webInterface.getCore().getReply(replyId);
                if (reply == null) {
index 08e3ee5..232cab6 100644 (file)
@@ -18,6 +18,7 @@
 package net.pterodactylus.sone.web.ajax;
 
 import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.json.JsonObject;
 import net.pterodactylus.util.notify.Notification;
 
@@ -42,7 +43,7 @@ public class DismissNotificationAjaxPage extends JsonPage {
         * {@inheritDoc}
         */
        @Override
-       protected JsonObject createJsonObject(Request request) {
+       protected JsonObject createJsonObject(FreenetRequest request) {
                String notificationId = request.getHttpRequest().getParam("notification");
                Notification notification = webInterface.getNotifications().getNotification(notificationId);
                if (notification == null) {
index ca770e4..4ba3dbb 100644 (file)
@@ -21,6 +21,7 @@ import net.pterodactylus.sone.core.Core;
 import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.freenet.wot.Trust;
 import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.json.JsonObject;
 
 /**
@@ -45,7 +46,7 @@ public class DistrustAjaxPage extends JsonPage {
         * {@inheritDoc}
         */
        @Override
-       protected JsonObject createJsonObject(Request request) {
+       protected JsonObject createJsonObject(FreenetRequest request) {
                Sone currentSone = getCurrentSone(request.getToadletContext(), false);
                if (currentSone == null) {
                        return createErrorJsonObject("auth-required");
diff --git a/src/main/java/net/pterodactylus/sone/web/ajax/EditAlbumAjaxPage.java b/src/main/java/net/pterodactylus/sone/web/ajax/EditAlbumAjaxPage.java
new file mode 100644 (file)
index 0000000..53f0466
--- /dev/null
@@ -0,0 +1,76 @@
+/*
+ * Sone - EditAlbumAjaxPage.java - Copyright © 2011 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.ajax;
+
+import net.pterodactylus.sone.data.Album;
+import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
+import net.pterodactylus.util.json.JsonObject;
+
+/**
+ * Page that stores a user’s album modifications.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class EditAlbumAjaxPage extends JsonPage {
+
+       /**
+        * Creates a new edit album AJAX page.
+        *
+        * @param webInterface
+        *            The Sone web interface
+        */
+       public EditAlbumAjaxPage(WebInterface webInterface) {
+               super("editAlbum.ajax", webInterface);
+       }
+
+       //
+       // JSONPAGE METHODS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected JsonObject createJsonObject(FreenetRequest request) {
+               String albumId = request.getHttpRequest().getParam("album");
+               Album album = webInterface.getCore().getAlbum(albumId, false);
+               if (album == null) {
+                       return createErrorJsonObject("invalid-album-id");
+               }
+               if (!webInterface.getCore().isLocalSone(album.getSone())) {
+                       return createErrorJsonObject("not-authorized");
+               }
+               if ("true".equals(request.getHttpRequest().getParam("moveLeft"))) {
+                       Album swappedAlbum = (album.getParent() != null) ? album.getParent().moveAlbumUp(album) : album.getSone().moveAlbumUp(album);
+                       webInterface.getCore().touchConfiguration();
+                       return createSuccessJsonObject().put("sourceAlbumId", album.getId()).put("destinationAlbumId", swappedAlbum.getId());
+               }
+               if ("true".equals(request.getHttpRequest().getParam("moveRight"))) {
+                       Album swappedAlbum = (album.getParent() != null) ? album.getParent().moveAlbumDown(album) : album.getSone().moveAlbumDown(album);
+                       webInterface.getCore().touchConfiguration();
+                       return createSuccessJsonObject().put("sourceAlbumId", album.getId()).put("destinationAlbumId", swappedAlbum.getId());
+               }
+               String title = request.getHttpRequest().getParam("title").trim();
+               String description = request.getHttpRequest().getParam("description").trim();
+               album.setTitle(title).setDescription(description);
+               webInterface.getCore().touchConfiguration();
+               return createSuccessJsonObject().put("albumId", album.getId()).put("title", album.getTitle()).put("description", album.getDescription());
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/sone/web/ajax/EditImageAjaxPage.java b/src/main/java/net/pterodactylus/sone/web/ajax/EditImageAjaxPage.java
new file mode 100644 (file)
index 0000000..17d171b
--- /dev/null
@@ -0,0 +1,76 @@
+/*
+ * Sone - EditImageAjaxPage.java - Copyright © 2011 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.ajax;
+
+import net.pterodactylus.sone.data.Image;
+import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
+import net.pterodactylus.util.json.JsonObject;
+
+/**
+ * Page that stores a user’s image modifications.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class EditImageAjaxPage extends JsonPage {
+
+       /**
+        * Creates a new edit image AJAX page.
+        *
+        * @param webInterface
+        *            The Sone web interface
+        */
+       public EditImageAjaxPage(WebInterface webInterface) {
+               super("editImage.ajax", webInterface);
+       }
+
+       //
+       // JSONPAGE METHODS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected JsonObject createJsonObject(FreenetRequest request) {
+               String imageId = request.getHttpRequest().getParam("image");
+               Image image = webInterface.getCore().getImage(imageId, false);
+               if (image == null) {
+                       return createErrorJsonObject("invalid-image-id");
+               }
+               if (!webInterface.getCore().isLocalSone(image.getSone())) {
+                       return createErrorJsonObject("not-authorized");
+               }
+               if ("true".equals(request.getHttpRequest().getParam("moveLeft"))) {
+                       Image swappedImage = image.getAlbum().moveImageUp(image);
+                       webInterface.getCore().touchConfiguration();
+                       return createSuccessJsonObject().put("sourceImageId", image.getId()).put("destinationImageId", swappedImage.getId());
+               }
+               if ("true".equals(request.getHttpRequest().getParam("moveRight"))) {
+                       Image swappedImage = image.getAlbum().moveImageDown(image);
+                       webInterface.getCore().touchConfiguration();
+                       return createSuccessJsonObject().put("sourceImageId", image.getId()).put("destinationImageId", swappedImage.getId());
+               }
+               String title = request.getHttpRequest().getParam("title").trim();
+               String description = request.getHttpRequest().getParam("description").trim();
+               image.setTitle(title).setDescription(description);
+               webInterface.getCore().touchConfiguration();
+               return createSuccessJsonObject().put("imageId", image.getId()).put("title", image.getTitle()).put("description", image.getDescription());
+       }
+
+}
index 0036545..5351281 100644 (file)
@@ -21,6 +21,7 @@ import net.pterodactylus.sone.data.Profile;
 import net.pterodactylus.sone.data.Profile.Field;
 import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.json.JsonObject;
 
 /**
@@ -48,7 +49,7 @@ public class EditProfileFieldAjaxPage extends JsonPage {
         * {@inheritDoc}
         */
        @Override
-       protected JsonObject createJsonObject(Request request) {
+       protected JsonObject createJsonObject(FreenetRequest request) {
                String fieldId = request.getHttpRequest().getParam("field");
                Sone currentSone = getCurrentSone(request.getToadletContext());
                Profile profile = currentSone.getProfile();
index d82ab1d..5c3e5f4 100644 (file)
@@ -19,6 +19,7 @@ package net.pterodactylus.sone.web.ajax;
 
 import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.json.JsonObject;
 
 /**
@@ -42,7 +43,7 @@ public class FollowSoneAjaxPage extends JsonPage {
         * {@inheritDoc}
         */
        @Override
-       protected JsonObject createJsonObject(Request request) {
+       protected JsonObject createJsonObject(FreenetRequest request) {
                String soneId = request.getHttpRequest().getParam("sone");
                if (!webInterface.getCore().hasSone(soneId)) {
                        return createErrorJsonObject("invalid-sone-id");
index a8b991d..539be3d 100644 (file)
@@ -27,6 +27,7 @@ import net.pterodactylus.sone.data.Reply;
 import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.template.SoneAccessor;
 import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.json.JsonArray;
 import net.pterodactylus.util.json.JsonObject;
 
@@ -55,7 +56,7 @@ public class GetLikesAjaxPage extends JsonPage {
         * {@inheritDoc}
         */
        @Override
-       protected JsonObject createJsonObject(Request request) {
+       protected JsonObject createJsonObject(FreenetRequest request) {
                String type = request.getHttpRequest().getParam("type", null);
                String id = request.getHttpRequest().getParam(type, null);
                if ((id == null) || (id.length() == 0)) {
index 8bfe472..dfac8e4 100644 (file)
@@ -27,6 +27,7 @@ import net.pterodactylus.sone.main.SonePlugin;
 import net.pterodactylus.sone.notify.ListNotification;
 import net.pterodactylus.sone.notify.ListNotificationFilters;
 import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.json.JsonObject;
 import net.pterodactylus.util.notify.Notification;
 import net.pterodactylus.util.notify.TemplateNotification;
@@ -75,12 +76,16 @@ public class GetNotificationAjaxPage extends JsonPage {
         */
        @Override
        @SuppressWarnings("unchecked")
-       protected JsonObject createJsonObject(Request request) {
+       protected JsonObject createJsonObject(FreenetRequest request) {
                String[] notificationIds = request.getHttpRequest().getParam("notifications").split(",");
                JsonObject jsonNotifications = new JsonObject();
                Sone currentSone = getCurrentSone(request.getToadletContext(), false);
                for (String notificationId : notificationIds) {
                        Notification notification = webInterface.getNotifications().getNotification(notificationId);
+                       if (notification == null) {
+                               // TODO - show error
+                               continue;
+                       }
                        if ("new-post-notification".equals(notificationId)) {
                                notification = ListNotificationFilters.filterNewPostNotification((ListNotification<Post>) notification, currentSone, false);
                        } else if ("new-reply-notification".equals(notificationId)) {
@@ -110,7 +115,7 @@ public class GetNotificationAjaxPage extends JsonPage {
         *            The notification to create a JSON object
         * @return The JSON object
         */
-       private JsonObject createJsonNotification(Request request, Notification notification) {
+       private JsonObject createJsonNotification(FreenetRequest request, Notification notification) {
                JsonObject jsonNotification = new JsonObject();
                jsonNotification.put("id", notification.getId());
                StringWriter notificationWriter = new StringWriter();
index f56c5b7..8be816d 100644 (file)
@@ -22,6 +22,7 @@ import java.io.StringWriter;
 import net.pterodactylus.sone.data.Post;
 import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.io.Closer;
 import net.pterodactylus.util.json.JsonObject;
 import net.pterodactylus.util.template.Template;
@@ -56,7 +57,7 @@ public class GetPostAjaxPage extends JsonPage {
         * {@inheritDoc}
         */
        @Override
-       protected JsonObject createJsonObject(Request request) {
+       protected JsonObject createJsonObject(FreenetRequest request) {
                String postId = request.getHttpRequest().getParam("post");
                Post post = webInterface.getCore().getPost(postId, false);
                if (post == null) {
@@ -89,7 +90,7 @@ public class GetPostAjaxPage extends JsonPage {
         *            The currently logged in Sone (to store in the template)
         * @return The JSON representation of the post
         */
-       private JsonObject createJsonPost(Request request, Post post, Sone currentSone) {
+       private JsonObject createJsonPost(FreenetRequest request, Post post, Sone currentSone) {
                JsonObject jsonPost = new JsonObject();
                jsonPost.put("id", post.getId());
                jsonPost.put("sone", post.getSone().getId());
index 6cc7d47..2eef58a 100644 (file)
@@ -22,6 +22,7 @@ import java.io.StringWriter;
 import net.pterodactylus.sone.data.Reply;
 import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.io.Closer;
 import net.pterodactylus.util.json.JsonObject;
 import net.pterodactylus.util.template.Template;
@@ -59,7 +60,7 @@ public class GetReplyAjaxPage extends JsonPage {
         * {@inheritDoc}
         */
        @Override
-       protected JsonObject createJsonObject(Request request) {
+       protected JsonObject createJsonObject(FreenetRequest request) {
                String replyId = request.getHttpRequest().getParam("reply");
                Reply reply = webInterface.getCore().getReply(replyId);
                if ((reply == null) || (reply.getSone() == null)) {
@@ -91,7 +92,7 @@ public class GetReplyAjaxPage extends JsonPage {
         *            The currently logged in Sone (to store in the template)
         * @return The JSON representation of the reply
         */
-       private JsonObject createJsonReply(Request request, Reply reply, Sone currentSone) {
+       private JsonObject createJsonReply(FreenetRequest request, Reply reply, Sone currentSone) {
                JsonObject jsonReply = new JsonObject();
                jsonReply.put("id", reply.getId());
                jsonReply.put("postId", reply.getPost().getId());
index f704905..eaa6506 100644 (file)
@@ -31,6 +31,7 @@ import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.notify.ListNotificationFilters;
 import net.pterodactylus.sone.template.SoneAccessor;
 import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.filter.Filter;
 import net.pterodactylus.util.filter.Filters;
 import net.pterodactylus.util.json.JsonArray;
@@ -62,7 +63,7 @@ public class GetStatusAjaxPage extends JsonPage {
         * {@inheritDoc}
         */
        @Override
-       protected JsonObject createJsonObject(Request request) {
+       protected JsonObject createJsonObject(FreenetRequest request) {
                final Sone currentSone = getCurrentSone(request.getToadletContext(), false);
                /* load Sones. */
                boolean loadAllSones = Boolean.parseBoolean(request.getHttpRequest().getParam("loadAllSones", "false"));
index f03e6e4..1711a5b 100644 (file)
@@ -24,6 +24,7 @@ import java.util.Date;
 import net.pterodactylus.sone.data.Post;
 import net.pterodactylus.sone.data.Reply;
 import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.json.JsonObject;
 import net.pterodactylus.util.number.Digits;
 
@@ -51,7 +52,7 @@ public class GetTimesAjaxPage extends JsonPage {
         * {@inheritDoc}
         */
        @Override
-       protected JsonObject createJsonObject(Request request) {
+       protected JsonObject createJsonObject(FreenetRequest request) {
                String allIds = request.getHttpRequest().getParam("posts");
                JsonObject postTimes = new JsonObject();
                if (allIds.length() > 0) {
index 725b13a..e0909ca 100644 (file)
@@ -18,6 +18,7 @@
 package net.pterodactylus.sone.web.ajax;
 
 import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.json.JsonObject;
 
 /**
@@ -45,7 +46,7 @@ public class GetTranslationPage extends JsonPage {
         * {@inheritDoc}
         */
        @Override
-       protected JsonObject createJsonObject(Request request) {
+       protected JsonObject createJsonObject(FreenetRequest request) {
                String key = request.getHttpRequest().getParam("key");
                String translation = webInterface.getL10n().getString(key);
                return createSuccessJsonObject().put("value", translation);
index b027ab8..1e5e8ed 100644 (file)
 
 package net.pterodactylus.sone.web.ajax;
 
+import java.io.IOException;
+
 import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.web.WebInterface;
-import net.pterodactylus.sone.web.page.Page;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.json.JsonObject;
 import net.pterodactylus.util.json.JsonUtils;
+import net.pterodactylus.util.web.Page;
+import net.pterodactylus.util.web.Response;
 import freenet.clients.http.SessionManager.Session;
 import freenet.clients.http.ToadletContext;
 
@@ -31,7 +35,7 @@ import freenet.clients.http.ToadletContext;
  *
  * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
  */
-public abstract class JsonPage implements Page {
+public abstract class JsonPage implements Page<FreenetRequest> {
 
        /** The path of the page. */
        private final String path;
@@ -124,7 +128,7 @@ public abstract class JsonPage implements Page {
         *            The request to handle
         * @return The created JSON object
         */
-       protected abstract JsonObject createJsonObject(Request request);
+       protected abstract JsonObject createJsonObject(FreenetRequest request);
 
        /**
         * Returns whether this command needs the form password for authentication
@@ -187,23 +191,31 @@ public abstract class JsonPage implements Page {
         * {@inheritDoc}
         */
        @Override
-       public Response handleRequest(Request request) {
+       public boolean isPrefixPage() {
+               return false;
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public Response handleRequest(FreenetRequest request, Response response) throws IOException {
                if (webInterface.getCore().getPreferences().isRequireFullAccess() && !request.getToadletContext().isAllowedFullAccess()) {
-                       return new Response(403, "Forbidden", "application/json", JsonUtils.format(new JsonObject().put("success", false).put("error", "auth-required")));
+                       return response.setStatusCode(403).setStatusText("Forbidden").setContentType("application/json").write(JsonUtils.format(new JsonObject().put("success", false).put("error", "auth-required")));
                }
                if (needsFormPassword()) {
                        String formPassword = request.getHttpRequest().getParam("formPassword");
                        if (!webInterface.getFormPassword().equals(formPassword)) {
-                               return new Response(403, "Forbidden", "application/json", JsonUtils.format(new JsonObject().put("success", false).put("error", "auth-required")));
+                               return response.setStatusCode(403).setStatusText("Forbidden").setContentType("application/json").write(JsonUtils.format(new JsonObject().put("success", false).put("error", "auth-required")));
                        }
                }
                if (requiresLogin()) {
                        if (getCurrentSone(request.getToadletContext(), false) == null) {
-                               return new Response(403, "Forbidden", "application/json", JsonUtils.format(createErrorJsonObject("auth-required")));
+                               return response.setStatusCode(403).setStatusText("Forbidden").setContentType("application/json").write(JsonUtils.format(new JsonObject().put("success", false).put("error", "auth-required")));
                        }
                }
                JsonObject jsonObject = createJsonObject(request);
-               return new Response(200, "OK", "application/json", JsonUtils.format(jsonObject));
+               return response.setStatusCode(200).setStatusText("OK").setContentType("application/json").write(JsonUtils.format(jsonObject));
        }
 
 }
index bf87ead..e4d168f 100644 (file)
@@ -20,6 +20,7 @@ package net.pterodactylus.sone.web.ajax;
 import net.pterodactylus.sone.data.Post;
 import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.json.JsonObject;
 
 /**
@@ -43,7 +44,7 @@ public class LikeAjaxPage extends JsonPage {
         * {@inheritDoc}
         */
        @Override
-       protected JsonObject createJsonObject(Request request) {
+       protected JsonObject createJsonObject(FreenetRequest request) {
                String type = request.getHttpRequest().getParam("type", null);
                String id = request.getHttpRequest().getParam(type, null);
                if ((id == null) || (id.length() == 0)) {
index bca9a35..1d80905 100644 (file)
@@ -20,6 +20,7 @@ package net.pterodactylus.sone.web.ajax;
 import net.pterodactylus.sone.core.Core;
 import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.json.JsonObject;
 
 /**
@@ -43,7 +44,7 @@ public class LockSoneAjaxPage extends JsonPage {
         * {@inheritDoc}
         */
        @Override
-       protected JsonObject createJsonObject(Request request) {
+       protected JsonObject createJsonObject(FreenetRequest request) {
                String soneId = request.getHttpRequest().getParam("sone");
                Sone sone = webInterface.getCore().getLocalSone(soneId, false);
                if (sone == null) {
index b641ea0..42ee8b2 100644 (file)
@@ -22,6 +22,7 @@ import net.pterodactylus.sone.data.Post;
 import net.pterodactylus.sone.data.Reply;
 import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.json.JsonObject;
 
 /**
@@ -46,7 +47,7 @@ public class MarkAsKnownAjaxPage extends JsonPage {
         * {@inheritDoc}
         */
        @Override
-       protected JsonObject createJsonObject(Request request) {
+       protected JsonObject createJsonObject(FreenetRequest request) {
                String type = request.getHttpRequest().getParam("type");
                if (!type.equals("sone") && !type.equals("post") && !type.equals("reply")) {
                        return createErrorJsonObject("invalid-type");
index 092c1f7..b932a82 100644 (file)
@@ -21,6 +21,7 @@ import net.pterodactylus.sone.data.Profile;
 import net.pterodactylus.sone.data.Profile.Field;
 import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.json.JsonObject;
 
 /**
@@ -50,7 +51,7 @@ public class MoveProfileFieldAjaxPage extends JsonPage {
         * {@inheritDoc}
         */
        @Override
-       protected JsonObject createJsonObject(Request request) {
+       protected JsonObject createJsonObject(FreenetRequest request) {
                Sone currentSone = getCurrentSone(request.getToadletContext());
                Profile profile = currentSone.getProfile();
                String fieldId = request.getHttpRequest().getParam("field");
index d6e2750..b27bedf 100644 (file)
@@ -21,6 +21,7 @@ import net.pterodactylus.sone.core.Core;
 import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.freenet.wot.Trust;
 import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.json.JsonObject;
 
 /**
@@ -45,7 +46,7 @@ public class TrustAjaxPage extends JsonPage {
         * {@inheritDoc}
         */
        @Override
-       protected JsonObject createJsonObject(Request request) {
+       protected JsonObject createJsonObject(FreenetRequest request) {
                Sone currentSone = getCurrentSone(request.getToadletContext(), false);
                if (currentSone == null) {
                        return createErrorJsonObject("auth-required");
index f07dcb0..6a9f8fe 100644 (file)
@@ -18,6 +18,7 @@
 package net.pterodactylus.sone.web.ajax;
 
 import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.json.JsonObject;
 
 /**
@@ -45,7 +46,7 @@ public class UnbookmarkAjaxPage extends JsonPage {
         * {@inheritDoc}
         */
        @Override
-       protected JsonObject createJsonObject(Request request) {
+       protected JsonObject createJsonObject(FreenetRequest request) {
                String id = request.getHttpRequest().getParam("post", null);
                if ((id == null) || (id.length() == 0)) {
                        return createErrorJsonObject("invalid-post-id");
index 7f719b7..26f1a11 100644 (file)
@@ -19,6 +19,7 @@ package net.pterodactylus.sone.web.ajax;
 
 import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.json.JsonObject;
 
 /**
@@ -42,7 +43,7 @@ public class UnfollowSoneAjaxPage extends JsonPage {
         * {@inheritDoc}
         */
        @Override
-       protected JsonObject createJsonObject(Request request) {
+       protected JsonObject createJsonObject(FreenetRequest request) {
                String soneId = request.getHttpRequest().getParam("sone");
                if (!webInterface.getCore().hasSone(soneId)) {
                        return createErrorJsonObject("invalid-sone-id");
index 84b0593..1841806 100644 (file)
@@ -20,6 +20,7 @@ package net.pterodactylus.sone.web.ajax;
 import net.pterodactylus.sone.data.Post;
 import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.json.JsonObject;
 
 /**
@@ -43,7 +44,7 @@ public class UnlikeAjaxPage extends JsonPage {
         * {@inheritDoc}
         */
        @Override
-       protected JsonObject createJsonObject(Request request) {
+       protected JsonObject createJsonObject(FreenetRequest request) {
                String type = request.getHttpRequest().getParam("type", null);
                String id = request.getHttpRequest().getParam(type, null);
                if ((id == null) || (id.length() == 0)) {
index 3160db0..d7430c1 100644 (file)
@@ -20,6 +20,7 @@ package net.pterodactylus.sone.web.ajax;
 import net.pterodactylus.sone.core.Core;
 import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.json.JsonObject;
 
 /**
@@ -43,7 +44,7 @@ public class UnlockSoneAjaxPage extends JsonPage {
         * {@inheritDoc}
         */
        @Override
-       protected JsonObject createJsonObject(Request request) {
+       protected JsonObject createJsonObject(FreenetRequest request) {
                String soneId = request.getHttpRequest().getParam("sone");
                Sone sone = webInterface.getCore().getLocalSone(soneId, false);
                if (sone == null) {
index ad613ff..86222e0 100644 (file)
@@ -21,6 +21,7 @@ import net.pterodactylus.sone.core.Core;
 import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.freenet.wot.Trust;
 import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.json.JsonObject;
 
 /**
@@ -45,7 +46,7 @@ public class UntrustAjaxPage extends JsonPage {
         * {@inheritDoc}
         */
        @Override
-       protected JsonObject createJsonObject(Request request) {
+       protected JsonObject createJsonObject(FreenetRequest request) {
                Sone currentSone = getCurrentSone(request.getToadletContext(), false);
                if (currentSone == null) {
                        return createErrorJsonObject("auth-required");
diff --git a/src/main/java/net/pterodactylus/sone/web/page/FreenetRequest.java b/src/main/java/net/pterodactylus/sone/web/page/FreenetRequest.java
new file mode 100644 (file)
index 0000000..526f3ed
--- /dev/null
@@ -0,0 +1,80 @@
+/*
+ * Sone - FreenetRequest.java - Copyright © 2011 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.page;
+
+import java.net.URI;
+
+import net.pterodactylus.util.web.Method;
+import net.pterodactylus.util.web.Request;
+import freenet.clients.http.ToadletContext;
+import freenet.support.api.HTTPRequest;
+
+/**
+ * Encapsulates all Freenet-specific properties of a request.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class FreenetRequest extends Request {
+
+       /** The underlying HTTP request from Freenet. */
+       private final HTTPRequest httpRequest;
+
+       /** The toadlet context. */
+       private final ToadletContext toadletContext;
+
+       /**
+        * Creates a new freenet request.
+        *
+        * @param uri
+        *            The URI that is being accessed
+        * @param method
+        *            The method used to access this page
+        * @param httpRequest
+        *            The underlying HTTP request from Freenet
+        * @param toadletContext
+        *            The toadlet context
+        */
+       public FreenetRequest(URI uri, Method method, HTTPRequest httpRequest, ToadletContext toadletContext) {
+               super(uri, method);
+               this.httpRequest = httpRequest;
+               this.toadletContext = toadletContext;
+       }
+
+       //
+       // ACCESSORS
+       //
+
+       /**
+        * Returns the underlying HTTP request from Freenet.
+        *
+        * @return The underlying HTTP request from Freenet
+        */
+       public HTTPRequest getHttpRequest() {
+               return httpRequest;
+       }
+
+       /**
+        * Returns the toadlet context.
+        *
+        * @return The toadlet context
+        */
+       public ToadletContext getToadletContext() {
+               return toadletContext;
+       }
+
+}
index 5831a1b..5c027e5 100644 (file)
@@ -17,6 +17,7 @@
 
 package net.pterodactylus.sone.web.page;
 
+import java.io.IOException;
 import java.io.StringWriter;
 import java.util.Collection;
 import java.util.Collections;
@@ -26,11 +27,14 @@ import java.util.Map.Entry;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 
-import net.pterodactylus.sone.web.page.Page.Request.Method;
 import net.pterodactylus.util.logging.Logging;
 import net.pterodactylus.util.template.Template;
 import net.pterodactylus.util.template.TemplateContext;
 import net.pterodactylus.util.template.TemplateContextFactory;
+import net.pterodactylus.util.web.Method;
+import net.pterodactylus.util.web.Page;
+import net.pterodactylus.util.web.RedirectResponse;
+import net.pterodactylus.util.web.Response;
 import freenet.clients.http.LinkEnabledCallback;
 import freenet.clients.http.PageMaker;
 import freenet.clients.http.PageNode;
@@ -43,7 +47,7 @@ import freenet.support.HTMLNode;
  *
  * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
  */
-public class FreenetTemplatePage implements Page, LinkEnabledCallback {
+public class FreenetTemplatePage implements Page<FreenetRequest>, LinkEnabledCallback {
 
        /** The logger. */
        private static final Logger logger = Logging.getLogger(FreenetTemplatePage.class);
@@ -95,7 +99,7 @@ public class FreenetTemplatePage implements Page, LinkEnabledCallback {
         *            The request to serve
         * @return The title of the page
         */
-       protected String getPageTitle(Request request) {
+       protected String getPageTitle(FreenetRequest request) {
                return null;
        }
 
@@ -103,14 +107,22 @@ public class FreenetTemplatePage implements Page, LinkEnabledCallback {
         * {@inheritDoc}
         */
        @Override
-       public Response handleRequest(Request request) {
+       public boolean isPrefixPage() {
+               return false;
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public Response handleRequest(FreenetRequest request, Response response) throws IOException {
                String redirectTarget = getRedirectTarget(request);
                if (redirectTarget != null) {
                        return new RedirectResponse(redirectTarget);
                }
 
                if (isFullAccessOnly() && !request.getToadletContext().isAllowedFullAccess()) {
-                       return new Response(401, "Not authorized", "text/html", "Not authorized");
+                       return response.setStatusCode(401).setStatusText("Not authorized").setContentType("text/html");
                }
                ToadletContext toadletContext = request.getToadletContext();
                if (request.getMethod() == Method.POST) {
@@ -153,7 +165,7 @@ public class FreenetTemplatePage implements Page, LinkEnabledCallback {
 
                postProcess(request, templateContext);
 
-               return new Response(200, "OK", "text/html", pageNode.outer.generate());
+               return response.setStatusCode(200).setStatusText("OK").setContentType("text/html").write(pageNode.outer.generate());
        }
 
        /**
@@ -186,16 +198,15 @@ public class FreenetTemplatePage implements Page, LinkEnabledCallback {
         * @throws RedirectException
         *             if the processing page wants to redirect after processing
         */
-       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+       protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
                /* do nothing. */
        }
 
        /**
         * This method will be called after
-        * {@link #processTemplate(net.pterodactylus.sone.web.page.Page.Request, TemplateContext)}
-        * has processed the template and the template was rendered. This method
-        * will not be called if
-        * {@link #processTemplate(net.pterodactylus.sone.web.page.Page.Request, TemplateContext)}
+        * {@link #processTemplate(FreenetRequest, TemplateContext)} has processed
+        * the template and the template was rendered. This method will not be
+        * called if {@link #processTemplate(FreenetRequest, TemplateContext)}
         * throws a {@link RedirectException}!
         *
         * @param request
@@ -203,7 +214,7 @@ public class FreenetTemplatePage implements Page, LinkEnabledCallback {
         * @param templateContext
         *            The template context that supplied the rendered data
         */
-       protected void postProcess(Request request, TemplateContext templateContext) {
+       protected void postProcess(FreenetRequest request, TemplateContext templateContext) {
                /* do nothing. */
        }
 
@@ -215,7 +226,7 @@ public class FreenetTemplatePage implements Page, LinkEnabledCallback {
         *            The request that is processed
         * @return The URL to redirect to, or {@code null} to not redirect
         */
-       protected String getRedirectTarget(Page.Request request) {
+       protected String getRedirectTarget(FreenetRequest request) {
                return null;
        }
 
@@ -226,7 +237,7 @@ public class FreenetTemplatePage implements Page, LinkEnabledCallback {
         *            The request for which to return the link nodes
         * @return All link nodes that should be added to the HTML head
         */
-       protected List<Map<String, String>> getAdditionalLinkNodes(Request request) {
+       protected List<Map<String, String>> getAdditionalLinkNodes(FreenetRequest request) {
                return Collections.emptyList();
        }
 
@@ -256,7 +267,7 @@ public class FreenetTemplatePage implements Page, LinkEnabledCallback {
        /**
         * Exception that can be thrown to signal that a subclassed {@link Page}
         * wants to redirect the user during the
-        * {@link FreenetTemplatePage#processTemplate(net.pterodactylus.sone.web.page.Page.Request, TemplateContext)}
+        * {@link FreenetTemplatePage#processTemplate(FreenetRequest, TemplateContext)}
         * method call.
         *
         * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
diff --git a/src/main/java/net/pterodactylus/sone/web/page/Page.java b/src/main/java/net/pterodactylus/sone/web/page/Page.java
deleted file mode 100644 (file)
index 297081e..0000000
+++ /dev/null
@@ -1,402 +0,0 @@
-/*
- * Sone - Page.java - Copyright © 2010 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.page;
-
-import java.io.ByteArrayInputStream;
-import java.io.InputStream;
-import java.io.UnsupportedEncodingException;
-import java.net.URI;
-import java.util.HashMap;
-import java.util.Map;
-
-import freenet.clients.http.ToadletContext;
-import freenet.support.api.HTTPRequest;
-
-/**
- * A page is responsible for handling HTTP requests and creating appropriate
- * responses.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public interface Page {
-
-       /**
-        * Returns the path of this toadlet.
-        *
-        * @return The path of this toadlet
-        */
-       public String getPath();
-
-       /**
-        * Handles a request.
-        *
-        * @param request
-        *            The request to handle
-        * @return The response
-        */
-       public Response handleRequest(Request request);
-
-       /**
-        * Container for request data.
-        *
-        * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
-        */
-       public class Request {
-
-               /**
-                * Enumeration for all possible HTTP request methods.
-                *
-                * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’
-                *         Roden</a>
-                */
-               public enum Method {
-
-                       /** GET. */
-                       GET,
-
-                       /** POST. */
-                       POST,
-
-                       /** PUT. */
-                       PUT,
-
-                       /** DELETE. */
-                       DELETE,
-
-                       /** HEAD. */
-                       HEAD,
-
-                       /** OPTIONS. */
-                       OPTIONS,
-
-                       /** TRACE. */
-                       TRACE,
-
-               }
-
-               /** The URI that was accessed. */
-               private final URI uri;
-
-               /** The HTTP method that was used. */
-               private final Method method;
-
-               /** The HTTP request. */
-               private final HTTPRequest httpRequest;
-
-               /** The toadlet context. */
-               private final ToadletContext toadletContext;
-
-               /**
-                * Creates a new request that holds the given data.
-                *
-                * @param uri
-                *            The URI of the request
-                * @param method
-                *            The HTTP method of the request
-                * @param httpRequest
-                *            The HTTP request
-                * @param toadletContext
-                *            The toadlet context of the request
-                */
-               public Request(URI uri, Method method, HTTPRequest httpRequest, ToadletContext toadletContext) {
-                       this.uri = uri;
-                       this.method = method;
-                       this.httpRequest = httpRequest;
-                       this.toadletContext = toadletContext;
-               }
-
-               /**
-                * Returns the URI that was accessed.
-                *
-                * @return The accessed URI
-                */
-               public URI getUri() {
-                       return uri;
-               }
-
-               /**
-                * Returns the HTTP method that was used to access the page.
-                *
-                * @return The HTTP method
-                */
-               public Method getMethod() {
-                       return method;
-               }
-
-               /**
-                * Returns the HTTP request.
-                *
-                * @return The HTTP request
-                */
-               public HTTPRequest getHttpRequest() {
-                       return httpRequest;
-               }
-
-               /**
-                * Returns the toadlet context.
-                *
-                * @return The toadlet context
-                */
-               public ToadletContext getToadletContext() {
-                       return toadletContext;
-               }
-
-       }
-
-       /**
-        * Container for the HTTP response of a {@link Page}.
-        *
-        * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
-        */
-       public class Response {
-
-               /** The HTTP status code of the response. */
-               private final int statusCode;
-
-               /** The HTTP status text of the response. */
-               private final String statusText;
-
-               /** The content type of the response. */
-               private final String contentType;
-
-               /** The headers of the response. */
-               private final Map<String, String> headers;
-
-               /** The content of the response body. */
-               private final InputStream content;
-
-               /**
-                * Creates a new response.
-                *
-                * @param statusCode
-                *            The HTTP status code of the response
-                * @param statusText
-                *            The HTTP status text of the response
-                * @param contentType
-                *            The content type of the response
-                * @param text
-                *            The text in the response body
-                */
-               public Response(int statusCode, String statusText, String contentType, String text) {
-                       this(statusCode, statusText, contentType, getBytes(text));
-               }
-
-               /**
-                * Creates a new response.
-                *
-                * @param statusCode
-                *            The HTTP status code of the response
-                * @param statusText
-                *            The HTTP status text of the response
-                * @param contentType
-                *            The content type of the response
-                * @param content
-                *            The content of the reponse body
-                */
-               public Response(int statusCode, String statusText, String contentType, byte[] content) {
-                       this(statusCode, statusText, contentType, new HashMap<String, String>(), content);
-               }
-
-               /**
-                * Creates a new response.
-                *
-                * @param statusCode
-                *            The HTTP status code of the response
-                * @param statusText
-                *            The HTTP status text of the response
-                * @param contentType
-                *            The content type of the response
-                * @param headers
-                *            The headers of the response
-                */
-               public Response(int statusCode, String statusText, String contentType, Map<String, String> headers) {
-                       this(statusCode, statusText, contentType, headers, (InputStream) null);
-               }
-
-               /**
-                * Creates a new response.
-                *
-                * @param statusCode
-                *            The HTTP status code of the response
-                * @param statusText
-                *            The HTTP status text of the response
-                * @param contentType
-                *            The content type of the response
-                * @param headers
-                *            The headers of the response
-                * @param content
-                *            The content of the reponse body
-                */
-               public Response(int statusCode, String statusText, String contentType, Map<String, String> headers, byte[] content) {
-                       this(statusCode, statusText, contentType, headers, new ByteArrayInputStream(content));
-               }
-
-               /**
-                * Creates a new response.
-                *
-                * @param statusCode
-                *            The HTTP status code of the response
-                * @param statusText
-                *            The HTTP status text of the response
-                * @param contentType
-                *            The content type of the response
-                * @param headers
-                *            The headers of the response
-                * @param content
-                *            The content of the reponse body
-                */
-               public Response(int statusCode, String statusText, String contentType, Map<String, String> headers, InputStream content) {
-                       this.statusCode = statusCode;
-                       this.statusText = statusText;
-                       this.contentType = contentType;
-                       this.headers = headers;
-                       this.content = content;
-               }
-
-               /**
-                * Returns the HTTP status code of the response.
-                *
-                * @return The HTTP status code
-                */
-               public int getStatusCode() {
-                       return statusCode;
-               }
-
-               /**
-                * Returns the HTTP status text.
-                *
-                * @return The HTTP status text
-                */
-               public String getStatusText() {
-                       return statusText;
-               }
-
-               /**
-                * Returns the content type of the response.
-                *
-                * @return The content type of the reponse
-                */
-               public String getContentType() {
-                       return contentType;
-               }
-
-               /**
-                * Returns HTTP headers of the response. May be {@code null} if no
-                * headers are returned.
-                *
-                * @return The response headers, or {@code null} if there are no
-                *         response headers
-                */
-               public Map<String, String> getHeaders() {
-                       return headers;
-               }
-
-               /**
-                * Sets the HTTP header with the given name to the given value. Multiple
-                * headers with the same name are not implemented so that latest call to
-                * {@link #setHeader(String, String)} determines what is sent to the
-                * browser.
-                *
-                * @param name
-                *            The name of the header
-                * @param value
-                *            The value of the header
-                */
-               public void setHeader(String name, String value) {
-                       headers.put(name, value);
-               }
-
-               /**
-                * Returns the content of the response body. May be {@code null} if the
-                * response does not have a body.
-                *
-                * @return The content of the response body
-                */
-               public InputStream getContent() {
-                       return content;
-               }
-
-               //
-               // PRIVATE METHODS
-               //
-
-               /**
-                * Returns the UTF-8 representation of the given text.
-                *
-                * @param text
-                *            The text to encode
-                * @return The encoded text
-                */
-               private static byte[] getBytes(String text) {
-                       try {
-                               return text.getBytes("UTF-8");
-                       } catch (UnsupportedEncodingException uee1) {
-                               /* every JVM needs to support UTF-8. */
-                       }
-                       return null;
-               }
-
-               /**
-                * Creates a header map containing a single header.
-                *
-                * @param name
-                *            The name of the header
-                * @param value
-                *            The value of the header
-                * @return The map containing the single header
-                */
-               protected static Map<String, String> createHeader(String name, String value) {
-                       Map<String, String> headers = new HashMap<String, String>();
-                       headers.put(name, value);
-                       return headers;
-               }
-
-       }
-
-       /**
-        * {@link Response} implementation that performs an HTTP redirect.
-        *
-        * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
-        */
-       public class RedirectResponse extends Response {
-
-               /**
-                * Creates a new redirect response to the new location.
-                *
-                * @param newLocation
-                *            The new location
-                */
-               public RedirectResponse(String newLocation) {
-                       this(newLocation, true);
-               }
-
-               /**
-                * Creates a new redirect response to the new location.
-                *
-                * @param newLocation
-                *            The new location
-                * @param permanent
-                *            Whether the redirect should be marked as permanent
-                */
-               public RedirectResponse(String newLocation, boolean permanent) {
-                       super(permanent ? 302 : 307, "Redirected", null, createHeader("Location", newLocation));
-               }
-
-       }
-
-}
index f594e58..2d1f35a 100644 (file)
@@ -19,9 +19,11 @@ package net.pterodactylus.sone.web.page;
 
 import java.io.IOException;
 import java.net.URI;
-import java.util.Map.Entry;
 
-import net.pterodactylus.sone.web.page.Page.Request.Method;
+import net.pterodactylus.util.web.Header;
+import net.pterodactylus.util.web.Method;
+import net.pterodactylus.util.web.Page;
+import net.pterodactylus.util.web.Response;
 import freenet.client.HighLevelSimpleClient;
 import freenet.clients.http.LinkEnabledCallback;
 import freenet.clients.http.Toadlet;
@@ -30,7 +32,6 @@ import freenet.clients.http.ToadletContextClosedException;
 import freenet.support.MultiValueTable;
 import freenet.support.api.Bucket;
 import freenet.support.api.HTTPRequest;
-import freenet.support.io.BucketTools;
 import freenet.support.io.Closer;
 
 /**
@@ -44,7 +45,7 @@ public class PageToadlet extends Toadlet implements LinkEnabledCallback {
        private final String menuName;
 
        /** The page that handles processing. */
-       private final Page page;
+       private final Page<FreenetRequest> page;
 
        /** The path prefix for the page. */
        private final String pathPrefix;
@@ -62,7 +63,7 @@ public class PageToadlet extends Toadlet implements LinkEnabledCallback {
         *            Prefix that is prepended to all {@link Page#getPath()} return
         *            values
         */
-       protected PageToadlet(HighLevelSimpleClient highLevelSimpleClient, String menuName, Page page, String pathPrefix) {
+       protected PageToadlet(HighLevelSimpleClient highLevelSimpleClient, String menuName, Page<FreenetRequest> page, String pathPrefix) {
                super(highLevelSimpleClient);
                this.menuName = menuName;
                this.page = page;
@@ -101,7 +102,7 @@ public class PageToadlet extends Toadlet implements LinkEnabledCallback {
         *             if the toadlet context is closed
         */
        public void handleMethodGET(URI uri, HTTPRequest httpRequest, ToadletContext toadletContext) throws IOException, ToadletContextClosedException {
-               handleRequest(new Page.Request(uri, Method.GET, httpRequest, toadletContext));
+               handleRequest(new FreenetRequest(uri, Method.GET, httpRequest, toadletContext));
        }
 
        /**
@@ -119,7 +120,7 @@ public class PageToadlet extends Toadlet implements LinkEnabledCallback {
         *             if the toadlet context is closed
         */
        public void handleMethodPOST(URI uri, HTTPRequest httpRequest, ToadletContext toadletContext) throws IOException, ToadletContextClosedException {
-               handleRequest(new Page.Request(uri, Method.POST, httpRequest, toadletContext));
+               handleRequest(new FreenetRequest(uri, Method.POST, httpRequest, toadletContext));
        }
 
        /**
@@ -140,32 +141,25 @@ public class PageToadlet extends Toadlet implements LinkEnabledCallback {
         * @throws ToadletContextClosedException
         *             if the toadlet context is closed
         */
-       private void handleRequest(Page.Request pageRequest) throws IOException, ToadletContextClosedException {
-               Bucket data = null;
+       private void handleRequest(FreenetRequest pageRequest) throws IOException, ToadletContextClosedException {
+               Bucket pageBucket = null;
                try {
-                       Page.Response pageResponse = page.handleRequest(pageRequest);
+                       pageBucket = pageRequest.getToadletContext().getBucketFactory().makeBucket(-1);
+                       Response pageResponse = new Response(pageBucket.getOutputStream());
+                       pageResponse = page.handleRequest(pageRequest, pageResponse);
                        MultiValueTable<String, String> headers = new MultiValueTable<String, String>();
                        if (pageResponse.getHeaders() != null) {
-                               for (Entry<String, String> headerEntry : pageResponse.getHeaders().entrySet()) {
-                                       headers.put(headerEntry.getKey(), headerEntry.getValue());
+                               for (Header header : pageResponse.getHeaders()) {
+                                       for (String value : header) {
+                                               headers.put(header.getName(), value);
+                                       }
                                }
                        }
-                       data = pageRequest.getToadletContext().getBucketFactory().makeBucket(-1);
-                       if (pageResponse.getContent() != null) {
-                               try {
-                                       BucketTools.copyFrom(data, pageResponse.getContent(), -1);
-                               } finally {
-                                       Closer.close(pageResponse.getContent());
-                               }
-                       } else {
-                               /* get an OutputStream and close it immediately. */
-                               Closer.close(data.getOutputStream());
-                       }
-                       writeReply(pageRequest.getToadletContext(), pageResponse.getStatusCode(), pageResponse.getContentType(), pageResponse.getStatusText(), headers, data);
+                       writeReply(pageRequest.getToadletContext(), pageResponse.getStatusCode(), pageResponse.getContentType(), pageResponse.getStatusText(), headers, pageBucket);
                } catch (Throwable t1) {
                        writeInternalError(t1, pageRequest.getToadletContext());
                } finally {
-                       Closer.close(data);
+                       Closer.close(pageBucket);
                }
        }
 
index bd0adb9..0abf3a5 100644 (file)
 
 package net.pterodactylus.sone.web.page;
 
+import net.pterodactylus.util.web.Page;
 import freenet.client.HighLevelSimpleClient;
 
 /**
  * Factory that creates {@link PageToadlet}s using a given
  * {@link HighLevelSimpleClient}.
- * 
+ *
  * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
  */
 public class PageToadletFactory {
@@ -35,7 +36,7 @@ public class PageToadletFactory {
 
        /**
         * Creates a new {@link PageToadlet} factory.
-        * 
+        *
         * @param highLevelSimpleClient
         *            The client to use when creating the toadlets
         * @param pathPrefix
@@ -49,26 +50,26 @@ public class PageToadletFactory {
        /**
         * Creates a {@link PageToadlet} that wraps the given page and does not
         * appear in the node’s menu.
-        * 
+        *
         * @param page
         *            The page to wrap
         * @return The toadlet wrapped around the page
         */
-       public PageToadlet createPageToadlet(Page page) {
+       public PageToadlet createPageToadlet(Page<FreenetRequest> page) {
                return createPageToadlet(page, null);
        }
 
        /**
         * Creates a {@link PageToadlet} that wraps the given page and appears in
         * the node’s menu under the given name.
-        * 
+        *
         * @param page
         *            The page to wrap
         * @param menuName
         *            The name of the menu item
         * @return The toadlet wrapped around the page
         */
-       public PageToadlet createPageToadlet(Page page, String menuName) {
+       public PageToadlet createPageToadlet(Page<FreenetRequest> page, String menuName) {
                return new PageToadlet(highLevelSimpleClient, menuName, page, pathPrefix);
        }
 
diff --git a/src/main/java/net/pterodactylus/sone/web/page/RedirectPage.java b/src/main/java/net/pterodactylus/sone/web/page/RedirectPage.java
deleted file mode 100644 (file)
index 2ce34f9..0000000
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- * Sone - RedirectPage.java - Copyright © 2011 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.page;
-
-/**
- * Page implementation that redirects the user to another URL.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class RedirectPage implements Page {
-
-       /** The original path. */
-       private String originalPath;
-
-       /** The path to redirect the browser to. */
-       private String newPath;
-
-       /**
-        * Creates a new redirect page.
-        *
-        * @param originalPath
-        *            The original path
-        * @param newPath
-        *            The path to redirect the browser to
-        */
-       public RedirectPage(String originalPath, String newPath) {
-               this.originalPath = originalPath;
-               this.newPath = newPath;
-       }
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       public String getPath() {
-               return originalPath;
-       }
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       public Response handleRequest(Request request) {
-               return new RedirectResponse(newPath);
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/web/page/StaticPage.java b/src/main/java/net/pterodactylus/sone/web/page/StaticPage.java
deleted file mode 100644 (file)
index 3d24fdd..0000000
+++ /dev/null
@@ -1,77 +0,0 @@
-/*
- * Sone - StaticPage.java - Copyright © 2010 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.page;
-
-import java.io.InputStream;
-
-/**
- * {@link Page} implementation that delivers static files from the class path.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class StaticPage implements Page {
-
-       /** The prefix for {@link #getPath()}. */
-       private final String pathPrefix;
-
-       /** The path used as prefix when loading resources. */
-       private final String resourcePathPrefix;
-
-       /** The MIME type for the files this path contains. */
-       private final String mimeType;
-
-       /**
-        * Creates a new CSS page.
-        *
-        * @param pathPrefix
-        *            The prefix for {@link #getPath()}
-        * @param resourcePathPrefix
-        *            The path prefix when loading resources
-        * @param mimeType
-        *            The MIME type of the files this path contains
-        */
-       public StaticPage(String pathPrefix, String resourcePathPrefix, String mimeType) {
-               this.pathPrefix = pathPrefix;
-               this.resourcePathPrefix = resourcePathPrefix;
-               this.mimeType = mimeType;
-       }
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       public String getPath() {
-               return pathPrefix;
-       }
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       public Response handleRequest(Request request) {
-               String path = request.getUri().getPath();
-               int lastSlash = path.lastIndexOf('/');
-               String filename = path.substring(lastSlash + 1);
-               InputStream fileInputStream = getClass().getResourceAsStream(resourcePathPrefix + filename);
-               if (fileInputStream == null) {
-                       return new Response(404, "Not found.", null, "");
-               }
-               return new Response(200, "OK", mimeType, null, fileInputStream);
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/web/page/TemplatePage.java b/src/main/java/net/pterodactylus/sone/web/page/TemplatePage.java
deleted file mode 100644 (file)
index 06fc9fc..0000000
+++ /dev/null
@@ -1,106 +0,0 @@
-/*
- * Sone - StaticTemplatePage.java - Copyright © 2011 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.page;
-
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.io.OutputStreamWriter;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-
-import net.pterodactylus.util.io.Closer;
-import net.pterodactylus.util.logging.Logging;
-import net.pterodactylus.util.template.Template;
-import net.pterodactylus.util.template.TemplateContext;
-import net.pterodactylus.util.template.TemplateContextFactory;
-
-/**
- * A template page is a single page that is created from a {@link Template} but
- * does not necessarily return HTML.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class TemplatePage implements Page {
-
-       /** The logger. */
-       private static final Logger logger = Logging.getLogger(TemplatePage.class);
-
-       /** The path of this page. */
-       private final String path;
-
-       /** The content type of this page. */
-       private final String contentType;
-
-       /** The template context factory. */
-       private final TemplateContextFactory templateContextFactory;
-
-       /** The template to render. */
-       private final Template template;
-
-       /**
-        * Creates a new template page.
-        *
-        * @param path
-        *            The path of the page
-        * @param contentType
-        *            The content type of the page
-        * @param templateContextFactory
-        *            The template context factory
-        * @param template
-        *            The template to render
-        */
-       public TemplatePage(String path, String contentType, TemplateContextFactory templateContextFactory, Template template) {
-               this.path = path;
-               this.contentType = contentType;
-               this.templateContextFactory = templateContextFactory;
-               this.template = template;
-       }
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       public String getPath() {
-               return path;
-       }
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       public Response handleRequest(Request request) {
-               ByteArrayOutputStream responseOutputStream = new ByteArrayOutputStream();
-               OutputStreamWriter responseWriter = null;
-               try {
-                       responseWriter = new OutputStreamWriter(responseOutputStream, "UTF-8");
-                       TemplateContext templateContext = templateContextFactory.createTemplateContext();
-                       templateContext.set("request", request);
-                       template.render(templateContext, responseWriter);
-               } catch (IOException ioe1) {
-                       logger.log(Level.WARNING, "Could not render template for path “" + path + "”!", ioe1);
-               } finally {
-                       Closer.close(responseWriter);
-                       Closer.close(responseOutputStream);
-               }
-               ByteArrayInputStream responseInputStream = new ByteArrayInputStream(responseOutputStream.toByteArray());
-               /* no need to close a ByteArrayInputStream. */
-               return new Response(200, "OK", contentType, null, responseInputStream);
-       }
-
-}
index 739c615..75f9d41 100644 (file)
@@ -12,6 +12,8 @@ Navigation.Menu.Item.Bookmarks.Name=Bookmarks
 Navigation.Menu.Item.Bookmarks.Tooltip=Show bookmarked posts
 Navigation.Menu.Item.EditProfile.Name=Edit Profile
 Navigation.Menu.Item.EditProfile.Tooltip=Edit the Profile of your Sone
+Navigation.Menu.Item.ImageBrowser.Name=Images
+Navigation.Menu.Item.ImageBrowser.Tooltip=Manages your Images
 Navigation.Menu.Item.DeleteSone.Name=Delete Sone
 Navigation.Menu.Item.DeleteSone.Tooltip=Deletes the current Sone
 Navigation.Menu.Item.Logout.Name=Logout
@@ -147,6 +149,8 @@ Page.ViewSone.PostList.Title=Posts by {sone}
 Page.ViewSone.PostList.Text.NoPostYet=This Sone has not yet posted anything.
 Page.ViewSone.Profile.Title=Profile
 Page.ViewSone.Profile.Label.Name=Name
+Page.ViewSone.Profile.Label.Albums=Albums
+Page.ViewSone.Profile.Albums.Text.All=All albums
 Page.ViewSone.Profile.Name.WoTLink=web of trust profile
 Page.ViewSone.Replies.Title=Posts {sone} has replied to
 
@@ -178,6 +182,54 @@ Page.FollowSone.Title=Follow Sone - Sone
 
 Page.UnfollowSone.Title=Unfollow Sone - Sone
 
+Page.ImageBrowser.Title=Image Browser - Sone
+Page.ImageBrowser.Album.Title=Album “{album}”
+Page.ImageBrowser.Album.Error.NotFound.Text=The requested album could not be found. It is possible that it has not yet been downloaded, or that it has been deleted.
+Page.ImageBrowser.Sone.Title=Albums of {sone}
+Page.ImageBrowser.Sone.Error.NotFound.Text=The requested Sone could not be found. It is possible that it has not yet been downloaded.
+Page.ImageBrowser.Header.Albums=Albums
+Page.ImageBrowser.Header.Images=Images
+Page.ImageBrowser.CreateAlbum.Button.CreateAlbum=Create Album
+Page.ImageBrowser.Album.Edit.Title=Edit Album
+Page.ImageBrowser.Album.Delete.Title=Delete Album
+Page.ImageBrowser.Album.Label.AlbumImage=Album Image:
+Page.ImageBrowser.Album.Label.Title=Title:
+Page.ImageBrowser.Album.Label.Description=Description:
+Page.ImageBrowser.Album.AlbumImage.Choose=Choose Album Image…
+Page.ImageBrowser.Album.Button.Save=Save Album
+Page.ImageBrowser.Album.Button.Delete=Delete Album
+Page.ImageBrowser.Image.Edit.Title=Edit Image
+Page.ImageBrowser.Image.Title.Label=Title:
+Page.ImageBrowser.Image.Description.Label=Description:
+Page.ImageBrowser.Image.Button.MoveLeft=◀
+Page.ImageBrowser.Image.Button.Save=Save Image
+Page.ImageBrowser.Image.Button.MoveRight=►
+Page.ImageBrowser.Image.Delete.Title=Delete Image
+Page.ImageBrowser.Image.Button.Delete=Delete Image
+
+Page.CreateAlbum.Title=Create Album - Sone
+Page.CreateAlbum.Page.Title=Create Album
+Page.CreateAlbum.Error.NameMissing=You seem to have forgotten to enter a name for your new album.
+
+Page.UploadImage.Title=Upload Image - Sone
+Page.UploadImage.Error.InvalidImage=The image you were trying to upload could not be recognized. Please upload only JPEG (*.jpg or *.jpeg), or PNG (*.png) images.
+
+Page.EditImage.Title=Edit Image
+
+Page.DeleteImage.Title=Delete Image
+Page.DeleteImage.Page.Title=Delete Image
+Page.DeleteImage.Text.ImageWillBeGone=This will remove the image “{image}” from your album “{album}”. If it has already been inserted into Freenet it can not be removed from there forcefully. Do you want to do delete the image?
+Page.DeleteImage.Button.Yes=Yes, delete image.
+Page.DeleteImage.Button.No=No, don’t delete image.
+
+Page.EditAlbum.Title=Edit Album
+
+Page.DeleteAlbum.Title=Delete Album
+Page.DeleteAlbum.Page.Title=Delete Album
+Page.DeleteAlbum.Text.AlbumWillBeGone=This will remove your album “{title}”. Do you really want to do that?
+Page.DeleteAlbum.Button.Yes=Yes, delete album.
+Page.DeleteAlbum.Button.No=No, don’t delete album.
+
 Page.Trust.Title=Trust Sone - Sone
 
 Page.Distrust.Title=Distrust Sone - Sone
@@ -250,6 +302,8 @@ View.Sone.Status.Idle=This Sone is idle, i.e. not being inserted or downloaded.
 View.Sone.Status.Downloading=This Sone is currently being downloaded.
 View.Sone.Status.Inserting=This Sone is currently being inserted.
 
+View.SoneMenu.Link.AllAlbums=all albums
+
 View.Post.UnknownAuthor=(unknown)
 View.Post.WebOfTrustLink=web of trust profile
 View.Post.Permalink=link post
@@ -272,6 +326,15 @@ View.Trust.Tooltip.Trust=Trust this person
 View.Trust.Tooltip.Distrust=Assign negative trust to this person
 View.Trust.Tooltip.Untrust=Remove your trust assignment for this person
 
+View.CreateAlbum.Title=Create Album
+View.CreateAlbum.Label.Name=Name:
+View.CreateAlbum.Label.Description=Description:
+
+View.UploadImage.Title=Upload Image
+View.UploadImage.Label.Title=Title:
+View.UploadImage.Label.Description=Description:
+View.UploadImage.Button.UploadImage=Upload Image
+
 View.Time.InTheFuture=in the future
 View.Time.AFewSecondsAgo=a few seconds ago
 View.Time.HalfAMinuteAgo=about half a minute ago
@@ -300,12 +363,20 @@ WebInterface.DefaultText.BirthMonth=Month
 WebInterface.DefaultText.BirthYear=Year
 WebInterface.DefaultText.FieldName=Field name
 WebInterface.DefaultText.Option.InsertionDelay=Time to wait after a Sone is modified before insert (in seconds)
+WebInterface.DefaultText.Search=What are you looking for?
+WebInterface.DefaultText.CreateAlbum.Name=Album title
+WebInterface.DefaultText.CreateAlbum.Description=Album description
+WebInterface.DefaultText.EditAlbum.Title=Album title
+WebInterface.DefaultText.EditAlbum.Description=Album description
+WebInterface.DefaultText.UploadImage.Title=Image title
+WebInterface.DefaultText.UploadImage.Description=Image description
+WebInterface.DefaultText.EditImage.Title=Image title
+WebInterface.DefaultText.EditImage.Description=Image description
 WebInterface.DefaultText.Option.PostsPerPage=Number of posts to show on a page
 WebInterface.DefaultText.Option.CharactersPerPost=Number of characters per post after which to cut the post off
 WebInterface.DefaultText.Option.PositiveTrust=The positive trust to assign
 WebInterface.DefaultText.Option.NegativeTrust=The negative trust to assign
 WebInterface.DefaultText.Option.TrustComment=The comment to set in the web of trust
-WebInterface.DefaultText.Search=What are you looking for?
 WebInterface.Confirmation.DeletePostButton=Yes, delete!
 WebInterface.Confirmation.DeleteReplyButton=Yes, delete!
 WebInterface.SelectBox.Choose=Choose…
@@ -332,6 +403,9 @@ Notification.SoneRescued.Text=The following Sones have been rescued:
 Notification.SoneRescued.Text.RememberToUnlock=Please remember to control the posts and replies you have given and don’t forget to unlock your Sones!
 Notification.LockedSones.Text=The following Sones have been locked for more than 5 minutes. Please check if you really want to keep these Sones locked:
 Notification.NewVersion.Text=Version {version} of the Sone plugin was found. Download it from USK@nwa8lHa271k2QvJ8aa0Ov7IHAV-DFOCFgmDt3X6BpCI,DuQSUZiI~agF8c-6tjsFFGuZ8eICrzWCILB60nT8KKo,AQACAAE/sone/{edition}​!
+Notification.InsertingImages.Text=The following images are being inserted:
+Notification.InsertedImages.Text=The following images have been inserted:
+Notification.ImageInsertFailed.Text=The following images could not be inserted:
 Notification.Mention.ShortText=You have been mentioned.
 Notification.Mention.Text=You have been mentioned in the following posts:
 Notification.SoneInsert.Duration={0,number} {0,choice,0#seconds|1#second|1<seconds}
index a5328a0..6ad458e 100644 (file)
@@ -51,6 +51,10 @@ textarea {
        display: none;
 }
 
+#sone .toggle-link {
+       margin-top: 1em;
+}
+
 #sone #formPassword {
        display: none;
 }
@@ -97,6 +101,14 @@ textarea {
        border: none;
 }
 
+#sone .small-link {
+       font-size: 85%;
+}
+
+#sone .parsed {
+       white-space: pre-wrap;
+}
+
 #sone #main.offline {
        opacity: 0.5;
 }
@@ -628,6 +640,66 @@ textarea {
        position: relative;
 }
 
+#sone .backlinks {
+       font-size: 80%;
+       margin-bottom: 1em;
+}
+
+#sone .backlinks .backlink {
+       display: inline;
+}
+
+#sone .album {
+}
+
+#sone .image-row, #sone .album-row {
+       display: table-row;
+}
+
+#sone .image-container, #sone .album-container {
+       width: 250px;
+       height: 250px;
+       overflow: hidden;
+       padding: -1px;
+       border: solid 1px #000;
+}
+
+#sone .image, #sone .album {
+       display: table-cell;
+       vertical-align: top;
+       text-align: center;
+       padding: 0.5ex;
+}
+
+#sone .single-image img {
+       border: solid 1px #000;
+       background-color: #fff;
+}
+
+#sone .image .edit-image input, #sone .album .edit-album input {
+       width: 95%;
+}
+
+#sone .image .edit-image textarea, #sone .album .edit-album textarea {
+       width: 95%;
+}
+
+#sone .image .image-title, #sone .album .album-title {
+       font-weight: bold;
+}
+
+#sone .image .image-description, #sone .album .album-description {
+       text-align: left;
+       width: 195px;
+       word-wrap: break-word;
+       max-height: 5em;
+       overflow: auto;
+}
+
+#sone .backlinks .separator {
+       display: inline;
+}
+
 #sone #search {
        text-align: right;
 }
@@ -702,6 +774,10 @@ textarea {
        clear: both;
 }
 
+#sone h1.backlink {
+       margin-bottom: 0px;
+}
+
 #sone h2 {
        font-family: inherit;
        font-size: 150%;
@@ -714,7 +790,7 @@ textarea {
        font-weight: bold;
 }
 
-#sone input.default {
+#sone input.default, #sone textarea.default {
        color: #888;
 }
 
diff --git a/src/main/resources/static/images/unknown-image-0.png b/src/main/resources/static/images/unknown-image-0.png
new file mode 100644 (file)
index 0000000..50ba705
Binary files /dev/null and b/src/main/resources/static/images/unknown-image-0.png differ
index ca80b48..bfb925b 100644 (file)
@@ -291,7 +291,7 @@ function getSoneElement(element) {
  * @return The Sone ID
  */
 function getMenuSone(element) {
-       return $(element).closest(".sone-menu").find(".sone-id").text();
+       return $(element).closest(".sone-menu").find(".sone-menu-id").text();
 }
 
 /**
@@ -858,14 +858,23 @@ function ajaxifyPost(postElement) {
 
        /* show Sone menu when hovering over the avatar. */
        $(postElement).find(".post-avatar").mouseover(function() {
-               $(".sone-menu:visible").fadeOut();
-               $(".sone-post-menu", postElement).mouseleave(function() {
-                       $(this).fadeOut();
-               }).fadeIn();
-               return false;
+               if (typeof currentSoneMenuTimeoutHandler != undefined) {
+                       clearTimeout(currentSoneMenuTimeoutHandler);
+               }
+               currentSoneMenuId = getPostId(this);
+               currentSoneMenuTimeoutHandler = setTimeout(function() {
+                       $(".sone-menu:visible").fadeOut();
+                       $(".sone-post-menu", postElement).mouseleave(function() {
+                               $(this).fadeOut();
+                       }).fadeIn();
+               }, 1000);
+       }).mouseleave(function() {
+               if (currentSoneMenuId = getPostId(this)) {
+                       clearTimeout(currentSoneMenuTimeoutHandler);
+               }
        });
        (function(postElement) {
-               var soneId = $(".sone-id", postElement).text();
+               var soneId = $(".sone-menu-id", postElement).text();
                $(".sone-post-menu .follow", postElement).click(function() {
                        var followElement = this;
                        ajaxGet("followSone.ajax", { "sone": soneId, "formPassword": getFormPassword() }, function() {
@@ -988,14 +997,23 @@ function ajaxifyReply(replyElement) {
 
        /* show Sone menu when hovering over the avatar. */
        $(replyElement).find(".reply-avatar").mouseover(function() {
-               $(".sone-menu:visible").fadeOut();
-               $(".sone-reply-menu", replyElement).mouseleave(function() {
-                       $(this).fadeOut();
-               }).fadeIn();
-               return false;
+               if (typeof currentSoneMenuTimeoutHandler != undefined) {
+                       clearTimeout(currentSoneMenuTimeoutHandler);
+               }
+               currentSoneMenuId = getPostId(this) + "-" + getReplyId(this);
+               currentSoneMenuTimeoutHandler = setTimeout(function() {
+                       $(".sone-menu:visible").fadeOut();
+                       $(".sone-reply-menu", replyElement).mouseleave(function() {
+                               $(this).fadeOut();
+                       }).fadeIn();
+               }, 1000);
+       }).mouseleave(function() {
+               if (currentSoneMenuId = getPostId(this) + "-" + getReplyId(this)) {
+                       clearTimeout(currentSoneMenuTimeoutHandler);
+               }
        });
        (function(replyElement) {
-               var soneId = $(".sone-id", replyElement).text();
+               var soneId = $(".sone-menu-id", replyElement).text();
                $(".sone-menu .follow", replyElement).click(function() {
                        var followElement = this;
                        ajaxGet("followSone.ajax", { "sone": soneId, "formPassword": getFormPassword() }, function() {
@@ -1828,6 +1846,12 @@ var online = true;
 var initiallyLoggedIn = $("#sone #loggedIn").text() == "true";
 var notLoggedIn = !initiallyLoggedIn;
 
+/** ID of the next-to-show Sone context menu. */
+var currentSoneMenuId;
+
+/** Timeout handler for the next-to-show Sone context menu. */
+var currentSoneMenuTimeoutHandler;
+
 $(document).ready(function() {
 
        /* this initializes the status update input field. */
diff --git a/src/main/resources/templates/createAlbum.html b/src/main/resources/templates/createAlbum.html
new file mode 100644 (file)
index 0000000..7df8103
--- /dev/null
@@ -0,0 +1,11 @@
+<%include include/head.html>
+
+       <h1><%= Page.CreateAlbum.Page.Title|l10n|html></h1>
+
+       <%if nameMissing>
+               <p><%= Page.CreateAlbum.Error.NameMissing|l10n|html></p>
+       <%/if>
+
+       <%include include/createAlbum.html>
+
+<%include include/tail.html>
diff --git a/src/main/resources/templates/deleteAlbum.html b/src/main/resources/templates/deleteAlbum.html
new file mode 100644 (file)
index 0000000..cb27c19
--- /dev/null
@@ -0,0 +1,14 @@
+<%include include/head.html>
+
+       <h1><%= Page.DeleteAlbum.Page.Title|l10n|html></h1>
+
+       <p><%= Page.DeleteAlbum.Text.AlbumWillBeGone|l10n|replace needle="{title}" replacementKey=album.title|html></p>
+
+       <form method="post">
+               <input type="hidden" name="formPassword" value="<% formPassword|html>" />
+               <input type="hidden" name="album" value="<%album.id|html>" />
+               <button type="submit" name="confirmDelete" value="1"><%= Page.DeleteAlbum.Button.Yes|l10n|html></button>
+               <button type="submit" name="abortDelete" value="1"><%= Page.DeleteAlbum.Button.No|l10n|html></button>
+       </form>
+
+<%include include/tail.html>
diff --git a/src/main/resources/templates/deleteImage.html b/src/main/resources/templates/deleteImage.html
new file mode 100644 (file)
index 0000000..68b403e
--- /dev/null
@@ -0,0 +1,14 @@
+<%include include/head.html>
+
+       <h1><%= Page.DeleteImage.Page.Title|l10n|html></h1>
+
+       <p><%= Page.DeleteImage.Text.ImageWillBeGone|l10n|replace needle="{image}" replacementKey=image.title|replace needle="{album}" replacementKey=image.album.title|html></p>
+
+       <form method="post">
+               <input type="hidden" name="formPassword" value="<% formPassword|html>" />
+               <input type="hidden" name="image" value="<%image.id|html>" />
+               <button type="submit" name="confirmDelete" value="1"><%= Page.DeleteImage.Button.Yes|l10n|html></button>
+               <button type="submit" name="abortDelete" value="1"><%= Page.DeleteImage.Button.No|l10n|html></button>
+       </form>
+
+<%include include/tail.html>
diff --git a/src/main/resources/templates/imageBrowser.html b/src/main/resources/templates/imageBrowser.html
new file mode 100644 (file)
index 0000000..d97fc25
--- /dev/null
@@ -0,0 +1,570 @@
+<%include include/head.html>
+
+       <div class="page-id hidden">image-browser</div>
+
+       <script language="javascript">
+
+               /* hide all those forms. */
+               function hideAndShowBlock(blockElement, clickToShowElement, clickToHideElement) {
+                       $(blockElement).hide();
+                       $(clickToShowElement).removeClass("hidden");
+                       $(clickToShowElement).click(function() {
+                               $(blockElement).slideDown();
+                               $(clickToShowElement).addClass("hidden");
+                               $(clickToHideElement).removeClass("hidden");
+                       });
+                       $(clickToHideElement).click(function() {
+                               $(blockElement).slideUp();
+                               $(clickToHideElement).addClass("hidden");
+                               $(clickToShowElement).removeClass("hidden");
+                       });
+               }
+
+               /* ID of the image currently being edited. */
+               var editingImageId = null;
+
+               /**
+                * Shows the form for editing an image.
+                *
+                * @param imageId The ID of the image to edit.
+                */
+               function editImage(imageId) {
+                       if (editingImageId != imageId) {
+                               cancelImageEditing();
+                       } else {
+                               return;
+                       }
+                       editingImageId = imageId;
+                       $(".show-data", getImage(imageId)).hide();
+                       $(".edit-data", getImage(imageId)).show();
+                       $(document).bind("click.sone", function(event) {
+                               if ($(event.target).closest("#image-" + imageId).size() == 0) {
+                                       cancelImageEditing();
+                               }
+                       });
+               }
+
+               /**
+                * Cancels all image editing.
+                */
+               function cancelImageEditing() {
+                       $(".image .show-data").show();
+                       $(".image .edit-data").hide();
+                       $("form.edit-image").each(function() {
+                               this.reset();
+                       });
+                       $(document).unbind("click.sone");
+                       editingImageId = null;
+               }
+
+               /**
+                * Returns the image element with the given ID.
+                *
+                * @param imageId The ID of the image
+                * @return The image element
+                */
+               function getImage(imageId) {
+                       return $("#sone .image .image-id:contains('" + imageId + "')").closest(".image");
+               }
+
+               /**
+                * Swaps two images.
+                *
+                * @param sourceId The ID of the source image
+                * @param destinationId The ID of the destionation image
+                */
+               function swapImage(sourceId, destinationId) {
+                       sourceElement = getImage(sourceId);
+                       destinationElement = getImage(destinationId);
+                       sourceParent = sourceElement.closest(".image-row");
+                       sourcePrevSibling = sourceElement.prev();
+                       sourceElement.detach();
+                       destinationElement.before(sourceElement);
+                       if (sourcePrevSibling.get(0) != destinationElement.get(0)) {
+                               destinationElement.detach();
+                               (sourcePrevSibling.size() > 0) ? sourcePrevSibling.after(destinationElement) : sourceParent.prepend(destinationElement);
+                       }
+                       if ($("button[name='moveLeft']", sourceElement).hasClass("hidden") != $("button[name='moveLeft']", destinationElement).hasClass("hidden")) {
+                               $("button[name='moveLeft']", sourceElement).toggleClass("hidden");
+                               $("button[name='moveLeft']", destinationElement).toggleClass("hidden");
+                       }
+                       if ($("button[name='moveRight']", sourceElement).hasClass("hidden") != $("button[name='moveRight']", destinationElement).hasClass("hidden")) {
+                               $("button[name='moveRight']", sourceElement).toggleClass("hidden");
+                               $("button[name='moveRight']", destinationElement).toggleClass("hidden");
+                       }
+               }
+
+               /**
+                * Prepare all images for inline editing.
+                */
+               function prepareImages() {
+                       $(".image").each(function() {
+                               imageId = $(this).closest(".image").find(".image-id").text();
+                               (function(element, imageId) {
+                                       $(".show-data", element).click(function() {
+                                               editImage(imageId);
+                                       });
+                                       $("button[name='moveLeft'], button[name='moveRight']", element).click(function() {
+                                               ajaxGet("editImage.ajax", { "formPassword": getFormPassword(), "image": imageId, "moveLeft": this.name == "moveLeft", "moveRight": this.name == "moveRight" }, function(data) {
+                                                       if (data && data.success) {
+                                                               swapImage(data.sourceImageId, data.destinationImageId);
+                                                       }
+                                               });
+                                               return false;
+                                       });
+                                       $("button[name='submit']", element).click(function() {
+                                               title = $(":input[name='title']:enabled", this.form).val();
+                                               description = $(":input[name='description']:enabled", this.form).val();
+                                               ajaxGet("editImage.ajax", { "formPassword": getFormPassword(), "image": imageId, "title": title, "description": description }, function(data) {
+                                                       if (data && data.success) {
+                                                               getImage(data.imageId).find(".image-title").text(data.title);
+                                                               getImage(data.imageId).find(".image-description").text(data.description);
+                                                               getImage(data.imageId).find(":input[name='title']").attr("defaultValue", title);
+                                                               getImage(data.imageId).find(":input[name='description']").attr("defaultValue", description);
+                                                               cancelImageEditing();
+                                                       }
+                                               });
+                                               return false;
+                                       });
+                               })(this, imageId);
+                       });
+               }
+
+               /* ID of the album currently being edited. */
+               var editingAlbumId = null;
+
+               /**
+                * Shows the form for editing an album.
+                *
+                * @param albumId The ID of the album to edit.
+                */
+               function editAlbum(albumId) {
+                       if (editingAlbumId != albumId) {
+                               if (editingAlbumId != null) {
+                                       cancelAlbumEditing();
+                               }
+                       } else {
+                               console.log("already editing " + albumId);
+                               return;
+                       }
+                       editingAlbumId = albumId;
+                       $(".show-data", getAlbum(albumId)).hide();
+                       $(".edit-data", getAlbum(albumId)).show();
+                       console.log(getAlbum(albumId));
+                       $(document).bind("click.sone", function(event) {
+                               if ($(event.target).closest("#album-" + albumId).size() == 0) {
+                                       cancelAlbumEditing();
+                               }
+                       });
+               }
+
+               /**
+                * Cancels all album editing.
+                */
+               function cancelAlbumEditing() {
+                       console.log("cancel-album-edit");
+                       $(".album .show-data").show();
+                       $(".album .edit-data").hide();
+                       $("form.edit-album").each(function() {
+                               this.reset();
+                       });
+                       $(document).unbind("click.sone");
+                       editingAlbumId = null;
+               }
+
+               /**
+                * Returns the album element with the given ID.
+                *
+                * @param albumId The ID of the album
+                * @return The album element
+                */
+               function getAlbum(albumId) {
+                       return $("#sone .album .album-id:contains('" + albumId + "')").closest(".album");
+               }
+
+               /**
+                * Swaps two albums.
+                *
+                * @param sourceId The ID of the source album
+                * @param destinationId The ID of the destionation album
+                */
+               function swapAlbum(sourceId, destinationId) {
+                       sourceElement = getAlbum(sourceId);
+                       destinationElement = getAlbum(destinationId);
+                       sourceParent = sourceElement.closest(".album-row");
+                       sourcePrevSibling = sourceElement.prev();
+                       sourceElement.detach();
+                       destinationElement.before(sourceElement);
+                       if (sourcePrevSibling.get(0) != destinationElement.get(0)) {
+                               destinationElement.detach();
+                               (sourcePrevSibling.size() > 0) ? sourcePrevSibling.after(destinationElement) : sourceParent.prepend(destinationElement);
+                       }
+                       if ($("button[name='moveLeft']", sourceElement).hasClass("hidden") != $("button[name='moveLeft']", destinationElement).hasClass("hidden")) {
+                               $("button[name='moveLeft']", sourceElement).toggleClass("hidden");
+                               $("button[name='moveLeft']", destinationElement).toggleClass("hidden");
+                       }
+                       if ($("button[name='moveRight']", sourceElement).hasClass("hidden") != $("button[name='moveRight']", destinationElement).hasClass("hidden")) {
+                               $("button[name='moveRight']", sourceElement).toggleClass("hidden");
+                               $("button[name='moveRight']", destinationElement).toggleClass("hidden");
+                       }
+               }
+
+               /**
+                * Prepare all albums for inline editing.
+                */
+               function prepareAlbums() {
+                       $(".album").each(function() {
+                               albumId = $(this).closest(".album").find(".album-id").text();
+                               (function(element, albumId) {
+                                       $(".show-data", element).click(function() {
+                                               console.log("show-data");
+                                               editAlbum(albumId);
+                                       });
+                                       $("button[name='moveLeft'], button[name='moveRight']", element).click(function() {
+                                               ajaxGet("editAlbum.ajax", { "formPassword": getFormPassword(), "album": albumId, "moveLeft": this.name == "moveLeft", "moveRight": this.name == "moveRight" }, function(data) {
+                                                       if (data && data.success) {
+                                                               swapAlbum(data.sourceAlbumId, data.destinationAlbumId);
+                                                       }
+                                               });
+                                               return false;
+                                       });
+                                       $("button[name='submit']", element).click(function() {
+                                               title = $(":input[name='title']:enabled", this.form).val();
+                                               description = $(":input[name='description']:enabled", this.form).val();
+                                               ajaxGet("editAlbum.ajax", { "formPassword": getFormPassword(), "album": albumId, "title": title, "description": description }, function(data) {
+                                                       if (data && data.success) {
+                                                               getAlbum(data.albumId).find(".album-title").text(data.title);
+                                                               getAlbum(data.albumId).find(".album-description").text(data.description);
+                                                               getAlbum(data.albumId).find(":input[name='title']").attr("defaultValue", title);
+                                                               getAlbum(data.albumId).find(":input[name='description']").attr("defaultValue", description);
+                                                               cancelAlbumEditing();
+                                                       }
+                                               });
+                                               return false;
+                                       });
+                               })(this, albumId);
+                       });
+               }
+
+       </script>
+
+       <%if albumRequested>
+
+               <%ifnull album>
+
+                       <p><%= Page.ImageBrowser.Album.Error.NotFound.Text|l10n|html></p>
+
+               <%elseifnull album.title>
+
+                       <p><%= Page.ImageBrowser.Album.Error.NotFound.Text|l10n|html></p>
+
+               <%else>
+
+                       <%if album.sone.local>
+                               <script language="javascript">
+
+                                       $(function() {
+                                               getTranslation("WebInterface.DefaultText.UploadImage.Title", function(text) {
+                                                       $("#upload-image :input[name='title']").each(function() {
+                                                               registerInputTextareaSwap(this, text, "title", false, true);
+                                                       });
+                                               });
+                                               getTranslation("WebInterface.DefaultText.UploadImage.Description", function(text) {
+                                                       $("#upload-image :input[name='description']").each(function() {
+                                                               registerInputTextareaSwap(this, text, "description", true, false);
+                                                       });
+                                               });
+                                               $("#upload-image label").hide();
+                                               getTranslation("WebInterface.DefaultText.CreateAlbum.Name", function(text) {
+                                                       $("#create-album input[name='name']").each(function() {
+                                                               registerInputTextareaSwap(this, text, "name", false, true);
+                                                       });
+                                               });
+                                               getTranslation("WebInterface.DefaultText.CreateAlbum.Description", function(text) {
+                                                       $("#create-album input[name='description']").each(function() {
+                                                               registerInputTextareaSwap(this, text, "description", true, true);
+                                                       });
+                                               });
+                                               $("#create-album label").hide();
+                                               getTranslation("WebInterface.DefaultText.EditAlbum.Title", function(text) {
+                                                       $("#edit-album input[name='title']").each(function() {
+                                                               registerInputTextareaSwap(this, text, "title", false, true);
+                                                       });
+                                               });
+                                               getTranslation("WebInterface.DefaultText.EditAlbum.Description", function(text) {
+                                                       $("#edit-album :input[name='description']").each(function() {
+                                                               registerInputTextareaSwap(this, text, "description", true, false);
+                                                       });
+                                               });
+                                               $("#edit-album label").hide();
+
+                                               hideAndShowBlock("div.edit-album", ".show-edit-album", ".hide-edit-album");
+                                               hideAndShowBlock("div.create-album", ".show-create-album", ".hide-create-album");
+                                               hideAndShowBlock("div.upload-image", ".show-upload-image", ".hide-upload-image");
+                                               hideAndShowBlock("div.delete-album", ".show-delete-album", ".hide-delete-album");
+
+                                               prepareAlbums();
+                                               prepareImages();
+                                       });
+                               </script>
+                       <%/if>
+
+                       <h1 class="backlink"><%= Page.ImageBrowser.Album.Title|l10n|replace needle='{album}' replacementKey=album.title|html></h1>
+
+                       <div class="backlinks">
+                               <%foreach album.backlinks backlink backlinks>
+                                       <div class="backlink">
+                                               <a href="<% backlink.target|html>"><% backlink.name|html></a>
+                                       </div>
+                                       <%if ! backlinks.last>
+                                               <div class="separator">&gt;</div>
+                                       <%/if>
+                               <%/foreach>
+                       </div>
+
+                       <p id="description"><% album.description|html></p>
+
+                       <%if album.sone.local>
+                               <div class="show-edit-album hidden toggle-link"><a class="small-link">» <%= Page.ImageBrowser.Album.Edit.Title|l10n|html></a></div>
+                               <div class="hide-edit-album hidden toggle-link"><a class="small-link">« <%= Page.ImageBrowser.Album.Edit.Title|l10n|html></a></div>
+                               <div class="edit-album">
+                                       <h2><%= Page.ImageBrowser.Album.Edit.Title|l10n|html></h2>
+
+                                       <form id="edit-album" action="editAlbum.html" method="post">
+                                               <input type="hidden" name="formPassword" value="<%formPassword|html>" />
+                                               <input type="hidden" name="album" value="<%album.id|html>" />
+
+                                               <%if ! album.images.empty>
+                                                       <div>
+                                                               <label for="album-image"><%= Page.ImageBrowser.Album.Label.AlbumImage|l10n|html></label>
+                                                               <select name="album-image">
+                                                                       <option disabled="disabled"><%= Page.ImageBrowser.Album.AlbumImage.Choose|l10n|html></option>
+                                                                       <%foreach album.images image>
+                                                                               <option value="<% image.id|html>"<%if album.albumImage.id|match key=image.id> selected="selected"<%/if>><% image.title|html></option>
+                                                                       <%/foreach>
+                                                               </select>
+                                                       </div>
+                                               <%/if>
+                                               <div>
+                                                       <label for="title"><%= Page.ImageBrowser.Album.Label.Title|l10n|html></label>
+                                                       <input type="text" name="title" value="<%album.title|html>" />
+                                               </div>
+                                               <div>
+                                                       <label for="description"><%= Page.ImageBrowser.Album.Label.Description|l10n|html></label>
+                                                       <textarea name="description"><%album.description|html></textarea>
+                                               </div>
+                                               <button type="submit"><%= Page.ImageBrowser.Album.Button.Save|l10n|html></button>
+                                       </form>
+                               </div>
+                       <%/if>
+
+                       <%include include/browseAlbums.html albums=album.albums>
+
+                       <%if album.sone.local>
+                               <div class="show-create-album hidden toggle-link"><a class="small-link">» <%= View.CreateAlbum.Title|l10n|html></a></div>
+                               <div class="hide-create-album hidden toggle-link"><a class="small-link">« <%= View.CreateAlbum.Title|l10n|html></a></div>
+                               <div class="create-album">
+                                       <%include include/createAlbum.html>
+                               </div>
+                       <%/if>
+
+                       <%foreach album.images image>
+                               <%first><h2><%= Page.ImageBrowser.Header.Images|l10n|html></h2><%/first>
+                               <%if loop.count|mod divisor=3><div class="image-row"><%/if>
+                               <div id="image-<% image.id|html>" class="image">
+                                       <div class="image-id hidden"><% image.id|html></div>
+                                       <div class="image-container">
+                                               <a href="imageBrowser.html?image=<%image.id|html>"><% image|image-link max-width=250 max-height=250 mode=enlarge title==image.title></a>
+                                       </div>
+                                       <div class="show-data">
+                                               <div class="image-title"><% image.title|html></div>
+                                               <div class="image-description"><% image.description|html></div>
+                                       </div>
+                                       <%if album.sone.local>
+                                               <form class="edit-image" action="editImage.html" method="post">
+                                                       <input type="hidden" name="formPassword" value="<%formPassword|html>" />
+                                                       <input type="hidden" name="returnPage" value="<%request.uri|html>" />
+                                                       <input type="hidden" name="image" value="<%image.id|html>" />
+
+                                                       <div class="edit-data hidden">
+                                                               <div>
+                                                                       <input type="text" name="title" value="<%image.title|html>" />
+                                                               </div>
+                                                               <div>
+                                                                       <textarea name="description"><%image.description|html></textarea>
+                                                               </div>
+                                                               <div>
+                                                                       <button <%first>class="hidden" <%/first>type="submit" name="moveLeft" value="true"><%= Page.ImageBrowser.Image.Button.MoveLeft|l10n|html></button>
+                                                                       <button type="submit" name="submit"><%= Page.ImageBrowser.Image.Button.Save|l10n|html></button>
+                                                                       <button <%last>class="hidden" <%/last>type="submit" name="moveRight" value="true"><%= Page.ImageBrowser.Image.Button.MoveRight|l10n|html></button>
+                                                               </div>
+                                                       </div>
+                                               </form>
+                                       <%/if>
+                               </div>
+                               <%= false|store key=endRow>
+                               <%if loop.count|mod divisor=3 offset=1><%= true|store key=endRow><%/if>
+                               <%last><%= true|store key=endRow><%/last>
+                               <%if endRow></div><%/if>
+                       <%/foreach>
+
+                       <%if album.sone.local>
+                               <div class="show-upload-image hidden toggle-link"><a class="small-link">» <%= View.UploadImage.Title|l10n|html></a></div>
+                               <div class="hide-upload-image hidden toggle-link"><a class="small-link">« <%= View.UploadImage.Title|l10n|html></a></div>
+                               <div class="upload-image">
+                                       <%include include/uploadImage.html>
+                               </div>
+
+                               <%if album.empty>
+                                       <div class="show-delete-album hidden toggle-link"><a class="small-link">» <%= Page.ImageBrowser.Album.Delete.Title|l10n|html></a></div>
+                                       <div class="hide-delete-album hidden toggle-link"><a class="small-link">« <%= Page.ImageBrowser.Album.Delete.Title|l10n|html></a></div>
+                                       <div class="delete-album">
+                                               <form id="delete-album" action="deleteAlbum.html" method="get">
+                                                       <input type="hidden" name="album" value="<%album.id|html>" />
+                                                       <button type="submit"><%= Page.ImageBrowser.Album.Button.Delete|l10n|html></button>
+                                               </form>
+                                       </div>
+                               <%/if>
+
+                       <%/if>
+
+               <%/if>
+
+       <%elseif imageRequested>
+
+               <h1 class="backlink"><%image.title|html></h1>
+
+               <div class="backlinks">
+                       <%foreach image.album.backlinks backlink backlinks>
+                               <div class="backlink">
+                                       <a href="<% backlink.target|html>"><% backlink.name|html></a>
+                               </div>
+                               <%if ! backlinks.last>
+                                       <div class="separator">&gt;</div>
+                               <%/if>
+                       <%/foreach>
+               </div>
+
+               <%ifnull image>
+
+               <%else>
+
+                       <%if image.sone.local>
+                               <script language="javascript">
+                                       $(function() {
+                                               getTranslation("WebInterface.DefaultText.EditImage.Title", function(text) {
+                                                       $("#edit-image input[name='title']").each(function() {
+                                                               registerInputTextareaSwap(this, text, "title", false, true);
+                                                       });
+                                               });
+                                               getTranslation("WebInterface.DefaultText.EditImage.Description", function(text) {
+                                                       $("#edit-image :input[name='description']").each(function() {
+                                                               registerInputTextareaSwap(this, text, "description", true, false);
+                                                       });
+                                               });
+                                               $("#edit-image label").hide();
+
+                                               hideAndShowBlock(".edit-image", ".show-edit-image", ".hide-edit-image");
+                                               hideAndShowBlock(".delete-image", ".show-delete-image", ".hide-delete-image");
+                                       });
+                               </script>
+                       <%/if>
+
+                       <div class="single-image">
+                               <%ifnull !image.key>
+                                       <a href="/<%image.key|html>"><% image|image-link max-width=640 max-height=480></a>
+                               <%else>
+                                       <a href="imageBrowser.html?image=<%image.id|html>"><% image|image-link max-width=640 max-height=480></a>
+                               <%/if>
+                       </div>
+
+                       <p class="parsed"><%image.description|parse sone=image.sone></p>
+
+                       <%if image.sone.local>
+
+                               <div class="show-edit-image hidden toggle-link"><a class="small-link">» <%= Page.ImageBrowser.Image.Edit.Title|l10n|html></a></div>
+                               <div class="hide-edit-image hidden toggle-link"><a class="small-link">« <%= Page.ImageBrowser.Image.Edit.Title|l10n|html></a></div>
+                               <div class="edit-image">
+                                       <h2><%= Page.ImageBrowser.Image.Edit.Title|l10n|html></h2>
+
+                                       <form id="edit-image" action="editImage.html" method="post">
+                                               <input type="hidden" name="formPassword" value="<%formPassword|html>" />
+                                               <input type="hidden" name="returnPage" value="<%request.uri|html>" />
+                                               <input type="hidden" name="image" value="<%image.id|html>" />
+
+                                               <div>
+                                                       <label for="title"><%= Page.ImageBrowser.Image.Title.Label|l10n|html></label>
+                                                       <input type="text" name="title" value="<%image.title|html>" />
+                                               </div>
+                                               <div>
+                                                       <label for="description"><%= Page.ImageBrowser.Image.Description.Label|l10n|html></label>
+                                                       <textarea name="description"><%image.description|html></textarea>
+                                               </div>
+                                               <div>
+                                                       <button type="submit"><%= Page.ImageBrowser.Image.Button.Save|l10n|html></button>
+                                               </div>
+                                       </form>
+                               </div>
+
+                               <div class="show-delete-image hidden toggle-link"><a class="small-link">» <%= Page.ImageBrowser.Image.Delete.Title|l10n|html></a></div>
+                               <div class="hide-delete-image hidden toggle-link"><a class="small-link">« <%= Page.ImageBrowser.Image.Delete.Title|l10n|html></a></div>
+                               <div class="delete-image">
+                                       <h2><%= Page.ImageBrowser.Image.Delete.Title|l10n|html></h2>
+
+                                       <form id="delete-image" action="deleteImage.html" method="get">
+                                               <input type="hidden" name="image" value="<%image.id|html>" />
+                                               <button type="submit"><%= Page.ImageBrowser.Image.Button.Delete|l10n|html></button>
+                                       </form>
+                               </div>
+
+                       <%/if>
+
+               <%/if>
+
+       <%elseif soneRequested>
+
+               <%if sone.local>
+                       <script language="javascript">
+                               $(function() {
+                                       getTranslation("WebInterface.DefaultText.CreateAlbum.Name", function(text) {
+                                               $("#create-album input[name='name']").each(function() {
+                                                       registerInputTextareaSwap(this, text, "name", false, true);
+                                               });
+                                       });
+                                       getTranslation("WebInterface.DefaultText.CreateAlbum.Description", function(text) {
+                                               $("#create-album input[name='description']").each(function() {
+                                                       registerInputTextareaSwap(this, text, "description", true, true);
+                                               });
+                                       });
+                                       $("#create-album label").hide();
+
+                                       hideAndShowBlock(".create-album", ".show-create-album", ".hide-create-album");
+
+                                       prepareAlbums();
+                               });
+                       </script>
+               <%/if>
+
+               <%ifnull sone>
+
+                       <p><%= Page.ImageBrowser.Sone.Error.NotFound.Text|l10n|html></p>
+
+               <%else>
+
+                       <h1><%= Page.ImageBrowser.Sone.Title|l10n|replace needle='{sone}' replacementKey=sone.niceName|html></h1>
+
+                       <%include include/browseAlbums.html albums=sone.albums>
+
+                       <%if sone.local>
+                               <div class="show-create-album hidden toggle-link"><a class="small-link">» <%= View.CreateAlbum.Title|l10n|html></a></div>
+                               <div class="hide-create-album hidden toggle-link"><a class="small-link">« <%= View.CreateAlbum.Title|l10n|html></a></div>
+                               <div class="create-album">
+                                       <%include include/createAlbum.html>
+                               </div>
+                       <%/if>
+
+               <%/if>
+
+       <%/if>
+
+<%include include/tail.html>
diff --git a/src/main/resources/templates/include/browseAlbums.html b/src/main/resources/templates/include/browseAlbums.html
new file mode 100644 (file)
index 0000000..b77bcd1
--- /dev/null
@@ -0,0 +1,45 @@
+<%foreach albums album>
+       <%first><h2><%= Page.ImageBrowser.Header.Albums|l10n|html></h2><%/first>
+       <%if loop.count|mod divisor=3><div class="album-row"><%/if>
+       <div id="album-<% album.id|html>" class="album">
+               <div class="album-id hidden"><% album.id|html></div>
+               <div class="album-container">
+                       <a href="imageBrowser.html?album=<% album.id|html>" title="<% album.title|html>">
+                               <%ifnull album.albumImage>
+                                       <img src="images/unknown-image-0.png" width="333" height="250" alt="<% album.title|html>" title="<% album.title|html>" style="position: relative; top: 0px; left: -41px;" />
+                               <%else><!-- TODO -->
+                                       <% album.albumImage|image-link max-width=250 max-height=250 mode=enlarge title==album.title>
+                               <%/if>
+                       </a>
+               </div>
+               <div class="show-data">
+                       <div class="album-title"><% album.title|html></div>
+                       <div class="album-description"><% album.description|html></div>
+               </div>
+               <%if album.sone.local>
+                       <form class="edit-album" action="editAlbum.html" method="post">
+                               <input type="hidden" name="formPassword" value="<%formPassword|html>" />
+                               <input type="hidden" name="returnPage" value="<%request.uri|html>" />
+                               <input type="hidden" name="album" value="<%album.id|html>" />
+
+                               <div class="edit-data hidden">
+                                       <div>
+                                               <input type="text" name="title" value="<%album.title|html>" />
+                                       </div>
+                                       <div>
+                                               <textarea name="description"><%album.description|html></textarea>
+                                       </div>
+                                       <div>
+                                               <button <%first>class="hidden" <%/first>type="submit" name="moveLeft" value="true"><%= Page.ImageBrowser.Image.Button.MoveLeft|l10n|html></button>
+                                               <button type="submit" name="submit"><%= Page.ImageBrowser.Album.Button.Save|l10n|html></button>
+                                               <button <%last>class="hidden" <%/last>type="submit" name="moveRight" value="true"><%= Page.ImageBrowser.Image.Button.MoveRight|l10n|html></button>
+                                       </div>
+                               </div>
+                       </form>
+               <%/if>
+       </div>
+       <%= false|store key=endRow>
+       <%if loop.count|mod divisor=3 offset=1><%= true|store key=endRow><%/if>
+       <%last><%= true|store key=endRow><%/last>
+       <%if endRow></div><%/if>
+<%/foreach>
diff --git a/src/main/resources/templates/include/createAlbum.html b/src/main/resources/templates/include/createAlbum.html
new file mode 100644 (file)
index 0000000..4887199
--- /dev/null
@@ -0,0 +1,11 @@
+<h2><%= View.CreateAlbum.Title|l10n|html></h2>
+
+<form id="create-album" method="post" action="createAlbum.html">
+       <input type="hidden" name="formPassword" value="<% formPassword|html>" />
+       <input type="hidden" name="parent" value="<%ifnull ! album><% album.id|html><%/if>" />
+       <label for="album"><%= View.CreateAlbum.Label.Name|l10n|html></label>
+       <input type="text" name="name" value="" />
+       <label for="description"><%= View.CreateAlbum.Label.Description|l10n|html></label>
+       <input type="text" name="description" value="" />
+       <button type="submit" name="uploadImage" value="1"><%= Page.ImageBrowser.CreateAlbum.Button.CreateAlbum|l10n|html></button>
+</form>
index 49237ba..a1dd944 100644 (file)
@@ -1,16 +1,21 @@
 <div class="sone-menu <% class|css|html>">
-       <div class="sone-id hidden"><%sone.id|html></div>
-       <img class="avatar" src="/WebOfTrust/GetIdenticon?identity=<%sone.id|html>&amp;width=64&height=64" width="64" height="64" alt="Avatar Image" />
+       <div class="sone-menu-id hidden"><%sone.id|html></div>
+       <img class="avatar" src="/WebOfTrust/GetIdenticon?identity=<%sone.id|html>&amp;width=64&amp;height=64" width="64" height="64" alt="Avatar Image" />
        <div class="inner-menu">
                <div>
                        <a class="author" href="viewSone.html?sone=<%sone.id|html>"><%sone.niceName|html></a>
-                       <span class="author-wot-link">(<a href="/WebOfTrust/ShowIdentity?id=<%sone.id|html>"><% =View.Post.WebOfTrustLink|l10n|html></a>)</span>
+                       (<%= View.Sone.Stats.Posts|l10n 0=sone.posts.size>, <%= View.Sone.Stats.Replies|l10n 0=sone.replies.size>)
                </div>
-               <div><%= View.Sone.Stats.Posts|l10n 0=sone.posts.size>, <%= View.Sone.Stats.Replies|l10n 0=sone.replies.size></div>
+               <div><a href="/WebOfTrust/ShowIdentity?id=<%sone.id|html>">» <% =View.Post.WebOfTrustLink|l10n|html></a></div>
+               <%foreach sone.albums album>
+                       <%first>
+                               <div><a href="imageBrowser.html?sone=<% sone.id|html>">» <% =View.SoneMenu.Link.AllAlbums|l10n|html></a></div>
+                       <%/first>
+               <%/foreach>
                <%if !sone.local>
                        <div>
-                               <a class="follow<%if sone.friend> hidden<%/if>"><%= View.Sone.Button.FollowSone|l10n|html></a>
-                               <a class="unfollow<%if !sone.friend> hidden<%/if>"><%= View.Sone.Button.UnfollowSone|l10n|html></a>
+                               <button class="follow<%if sone.friend> hidden<%/if>"><%= View.Sone.Button.FollowSone|l10n|html></button>
+                               <button class="unfollow<%if !sone.friend> hidden<%/if>"><%= View.Sone.Button.UnfollowSone|l10n|html></buton>
                        </div>
                <%/if>
        </div>
diff --git a/src/main/resources/templates/include/uploadImage.html b/src/main/resources/templates/include/uploadImage.html
new file mode 100644 (file)
index 0000000..fcee2f2
--- /dev/null
@@ -0,0 +1,12 @@
+<h2><%= View.UploadImage.Title|l10n|html></h2>
+
+<form id="upload-image" method="post" action="uploadImage.html" enctype="multipart/form-data">
+       <input type="hidden" name="formPassword" value="<% formPassword|html>" />
+       <input type="hidden" name="parent" value="<% album.id|html>" />
+       <label for="title"><%= View.UploadImage.Label.Title|l10n|html></label>
+       <input type="text" name="title" value="" />
+       <label for="description"><%= View.UploadImage.Label.Description|l10n|html></label>
+       <textarea name="description"></textarea>
+       <input type="file" name="image" />
+       <button type="submit" name="uploadImage" value="1"><%= View.UploadImage.Button.UploadImage|l10n|html></button>
+</form>
index ec1c299..89a8ba5 100644 (file)
@@ -1,4 +1,4 @@
-<div id="<% post.id|html>" class="post<%if loop.last> last<%/if><%if post.new> new<%/if>">
+<div id="<% post.id|html>" class="post<%if loop.last> last<%/if><%if !post.sone.local><%if post.new> new<%/if><%/if>">
        <a name="post-<% post.id|html>"></a>
        <div class="post-time hidden"><% post.time|html></div>
        <div class="post-author hidden"><% post.sone.id|html></div>
index abf3463..ee7299e 100644 (file)
@@ -1,4 +1,4 @@
-<div id="<% reply.id|html>" class="reply<%if reply.new> new<%/if>">
+<div id="<% reply.id|html>" class="reply<%if !reply.sone.local><%if reply.new> new<%/if><%/if>">
        <a name="reply-<% reply.id|html>"></a>
        <div class="reply-time hidden"><% reply.time|html></div>
        <div class="reply-author hidden"><% reply.sone.id|html></div>
diff --git a/src/main/resources/templates/insert/include/album.xml b/src/main/resources/templates/insert/include/album.xml
new file mode 100644 (file)
index 0000000..cd845da
--- /dev/null
@@ -0,0 +1,23 @@
+<album>
+       <id><% album.id|xml></id>
+       <name><% album.name|xml></name>
+       <description><% album.description|xml></description>
+       <albums>
+               <%foreach album.albums album>
+               <%include insert/include/album.xml>
+               <%/foreach>
+       </albums>
+       <images>
+               <%foreach album.images image>
+               <image>
+                       <id><% image.id|xml></id>
+                       <creation-time><% image.creationTime|xml></creation-time>
+                       <key><% image.key|xml></key>
+                       <width><% image.width|xml></width>
+                       <height><% image.height|xml></height>
+                       <title><% image.title|xml></title>
+                       <description><% image.description|xml></description>
+               </image>
+               <%/foreach>
+       </images>
+</album>
index e25cac9..2e4f298 100644 (file)
                <%/foreach>
        </reply-likes>
 
+       <%foreach currentSone.albums album>
+       <%first>
+       <albums>
+               <%/first>
+               <album>
+                       <id><%album.id|xml></id>
+                       <%ifnull !album.parent>
+                       <parent><%album.parent.id|xml></parent>
+                       <%/if>
+                       <title><%album.title|xml></title>
+                       <description><%album.description|xml></description>
+                       <album-image><%album.albumImage.id|xml></album-image>
+                       <%foreach album.images image>
+                       <%first>
+                       <images>
+                               <%/first>
+                               <image>
+                                       <id><%image.id|xml></id>
+                                       <creation-time><%image.creationTime|xml></creation-time>
+                                       <key><%image.key|xml></key>
+                                       <title><%image.title|xml></title>
+                                       <description><%image.description|xml></description>
+                                       <width><%image.width|xml></width>
+                                       <height><%image.height|xml></height>
+                               </image>
+                               <%last>
+                       </images>
+                       <%/last>
+                       <%/foreach>
+               </album>
+               <%last>
+       </albums>
+       <%/last>
+       <%/foreach>
+
 </sone>
index d838134..ea97d00 100644 (file)
@@ -2,6 +2,14 @@
 
        <h1><%= Page.Invalid.Page.Title|l10n|html></h1>
 
-       <p><%= Page.Invalid.Text|l10n|html|replace needle="{link}" replacement='<a href="index.html">'|replace needle="{/link}" replacement='</a>'></p>
+       <%foreach messages message>
+               <%if message|substring start=0 length=1|match value='!'>
+                       <p class="error"><% message|substring start=1|parse></p>
+               <%else>
+                       <p><% message|parse></p>
+               <%/if>
+       <%foreachelse>
+               <p><%= Page.Invalid.Text|l10n|html|replace needle="{link}" replacement='<a href="index.html">'|replace needle="{/link}" replacement='</a>'></p>
+       <%/foreach>
 
 <%include include/tail.html>
diff --git a/src/main/resources/templates/notify/image-insert-failed-notification.html b/src/main/resources/templates/notify/image-insert-failed-notification.html
new file mode 100644 (file)
index 0000000..c4b3c67
--- /dev/null
@@ -0,0 +1,6 @@
+<div class="text">
+       <%= Notification.ImageInsertFailed.Text|l10n|html>
+       <%foreach images image>
+               <a href="imageBrowser.html?image=<%image.id|html>" title="<%image.title|html>"><%image.title|html></a><%notlast>,<%/notlast><%last>.<%/last>
+       <%/foreach>
+</div>
diff --git a/src/main/resources/templates/notify/inserted-images-notification.html b/src/main/resources/templates/notify/inserted-images-notification.html
new file mode 100644 (file)
index 0000000..f388d59
--- /dev/null
@@ -0,0 +1,6 @@
+<div class="text">
+       <%= Notification.InsertedImages.Text|l10n|html>
+       <%foreach images image>
+               <a href="imageBrowser.html?image=<%image.id|html>" title="<%image.title|html>"><%image.title|html></a><%notlast>,<%/notlast><%last>.<%/last>
+       <%/foreach>
+</div>
diff --git a/src/main/resources/templates/notify/inserting-images-notification.html b/src/main/resources/templates/notify/inserting-images-notification.html
new file mode 100644 (file)
index 0000000..efe930d
--- /dev/null
@@ -0,0 +1,6 @@
+<div class="text">
+       <%= Notification.InsertingImages.Text|l10n|html>
+       <%foreach images image>
+               <a href="imageBrowser.html?image=<%image.id|html>" title="<%image.title|html>"><%image.title|html></a><%notlast>,<%/notlast><%last>.<%/last>
+       <%/foreach>
+</div>
index 55cc2a8..1d87e46 100644 (file)
                                <div class="value"><% sone.niceName|html> (<a href="/WebOfTrust/ShowIdentity?id=<% sone.id|html>"><%= Page.ViewSone.Profile.Name.WoTLink|l10n|html></a>)</div>
                        </div>
 
+                       <%foreach sone.albums album>
+                               <%first>
+                                       <div class="profile-field">
+                                               <div class="name"><%= Page.ViewSone.Profile.Label.Albums|l10n|html></div>
+                                               <div class="value">
+                                                       <a href="imageBrowser.html?sone=<% sone.id|html>"><% =Page.ViewSone.Profile.Albums.Text.All|l10n|html></a>,
+                               <%/first>
+                                       <a href="imageBrowser.html?album=<%album.id|html>"><%album.title|html></a><%notlast>, <%/notlast>
+                               <%last>
+                                               </div>
+                                       </div>
+                               <%/last>
+                       <%/foreach>
+
                        <%foreach sone.profile.fields field>
                                <div class="profile-field">
                                        <div class="name"><% field.name|html></div>
index 0343f05..d98d4ec 100644 (file)
@@ -21,6 +21,8 @@ import java.io.IOException;
 import java.io.StringReader;
 
 import junit.framework.TestCase;
+import net.pterodactylus.sone.core.SoneProvider;
+import net.pterodactylus.sone.data.Sone;
 
 /**
  * JUnit test case for {@link SoneTextParser}.
@@ -85,6 +87,23 @@ public class SoneTextParserTest extends TestCase {
                assertEquals("Part Text", "Link is [KSK@gpl.txt|gpl.txt|gpl.txt]\n[KSK@test.dat|test.dat|test.dat]", convertText(parts, PlainTextPart.class, FreenetLinkPart.class));
        }
 
+       /**
+        * Test case for a bug that was discovered in 0.6.7.
+        *
+        * @throws IOException
+        *             if an I/O error occurs
+        */
+       @SuppressWarnings("synthetic-access")
+       public void testEmptyLinesAndSoneLinks() throws IOException {
+               SoneTextParser soneTextParser = new SoneTextParser(new TestSoneProvider(), null);
+               Iterable<Part> parts;
+
+               /* check basic links. */
+               parts = soneTextParser.parse(null, new StringReader("Some text.\n\nLink to sone://DAxKQzS48mtaQc7sUVHIgx3fnWZPQBz0EueBreUVWrU and stuff."));
+               assertNotNull("Parts", parts);
+               assertEquals("Part Text", "Some text.\n\nLink to [Sone|DAxKQzS48mtaQc7sUVHIgx3fnWZPQBz0EueBreUVWrU] and stuff.", convertText(parts, PlainTextPart.class, SonePart.class));
+       }
+
        //
        // PRIVATE METHODS
        //
@@ -122,9 +141,38 @@ public class SoneTextParserTest extends TestCase {
                        } else if (part instanceof LinkPart) {
                                LinkPart linkPart = (LinkPart) part;
                                text.append('[').append(linkPart.getLink()).append('|').append(linkPart.getTitle()).append('|').append(linkPart.getText()).append(']');
+                       } else if (part instanceof SonePart) {
+                               SonePart sonePart = (SonePart) part;
+                               text.append("[Sone|").append(sonePart.getSone().getId()).append(']');
                        }
                }
                return text.toString();
        }
 
+       /**
+        * Mock Sone provider.
+        *
+        * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+        */
+       private static class TestSoneProvider implements SoneProvider {
+
+               /**
+                * {@inheritDoc}
+                */
+               @Override
+               public Sone getSone(final String soneId, boolean create) {
+                       return new Sone(soneId) {
+
+                               /**
+                                * {@inheritDoc}
+                                */
+                               @Override
+                               public String getName() {
+                                       return soneId;
+                               }
+                       };
+               }
+
+       }
+
 }