Bring image-management up to speed.
authorDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Wed, 13 Apr 2011 04:46:32 +0000 (06:46 +0200)
committerDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Wed, 13 Apr 2011 04:46:32 +0000 (06:46 +0200)
Conflicts:
src/main/java/net/pterodactylus/sone/core/Core.java
src/main/java/net/pterodactylus/sone/data/Sone.java
src/main/java/net/pterodactylus/sone/web/WebInterface.java
src/main/resources/i18n/sone.en.properties
src/main/resources/static/css/sone.css

38 files changed:
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/SoneException.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/template/AlbumAccessor.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/template/ImageLinkFilter.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/text/FreenetLinkParser.java
src/main/java/net/pterodactylus/sone/web/CreateAlbumPage.java [new file with mode: 0644]
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/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/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/UploadImagePage.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/web/WebInterface.java
src/main/java/net/pterodactylus/sone/web/page/Page.java
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/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/createAlbum.html [new file with mode: 0644]
src/main/resources/templates/include/uploadImage.html [new file with mode: 0644]
src/main/resources/templates/insert/include/album.xml [new file with mode: 0644]
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]

index e18fabc..a67177b 100644 (file)
@@ -31,12 +31,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.freenet.wot.Identity;
 import net.pterodactylus.sone.freenet.wot.IdentityListener;
 import net.pterodactylus.sone.freenet.wot.IdentityManager;
@@ -57,7 +60,7 @@ import freenet.keys.FreenetURI;
  *
  * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
  */
-public class Core implements IdentityListener, UpdateListener {
+public class Core implements IdentityListener, UpdateListener, ImageInsertListener {
 
        /**
         * Enumeration for the possible states of a {@link Sone}.
@@ -106,6 +109,9 @@ public class Core implements IdentityListener, UpdateListener {
        /** The Sone downloader. */
        private final SoneDownloader soneDownloader;
 
+       /** The image inserter. */
+       private final ImageInserter imageInserter;
+
        /** The update checker. */
        private final UpdateChecker updateChecker;
 
@@ -165,6 +171,15 @@ public class Core implements IdentityListener, UpdateListener {
        /** 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>();
+
        /**
         * Creates a new core.
         *
@@ -180,6 +195,7 @@ public class Core implements IdentityListener, UpdateListener {
                this.freenetInterface = freenetInterface;
                this.identityManager = identityManager;
                this.soneDownloader = new SoneDownloader(this, freenetInterface);
+               this.imageInserter = new ImageInserter(this, freenetInterface);
                this.updateChecker = new UpdateChecker(freenetInterface);
        }
 
@@ -744,6 +760,89 @@ public class Core implements IdentityListener, UpdateListener {
                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
        //
@@ -1270,6 +1369,64 @@ public class Core implements IdentityListener, UpdateListener {
                        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);
+                       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);
+                       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().addBooleanOption("AutoFollow", new DefaultOption<Boolean>(false));
                sone.getOptions().getBooleanOption("AutoFollow").set(configuration.getBooleanValue(sonePrefix + "/Options/AutoFollow").getValue(null));
@@ -1283,6 +1440,7 @@ public class Core implements IdentityListener, UpdateListener {
                        sone.setLikePostIds(likedPostIds);
                        sone.setLikeReplyIds(likedReplyIds);
                        sone.setFriends(friends);
+                       sone.setAlbums(topLevelAlbums);
                        soneInserters.get(sone).setLastInsertFingerprint(lastInsertFingerprint);
                }
                synchronized (newSones) {
@@ -1389,6 +1547,48 @@ public class Core implements IdentityListener, UpdateListener {
                        }
                        configuration.getStringValue(sonePrefix + "/Friends/" + friendCounter + "/ID").setValue(null);
 
+                       /* save albums. first, collect in a flat structure, top-level first. */
+                       List<Album> albums = new ArrayList<Album>();
+                       albums.addAll(sone.getAlbums());
+                       int lastAlbumIndex = 0;
+                       while (lastAlbumIndex < albums.size()) {
+                               int previousAlbumCount = albums.size();
+                               for (Album album : new ArrayList<Album>(albums.subList(lastAlbumIndex, albums.size()))) {
+                                       albums.addAll(album.getAlbums());
+                               }
+                               lastAlbumIndex = previousAlbumCount;
+                       }
+
+                       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(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());
 
@@ -1650,6 +1850,150 @@ public class Core implements IdentityListener, UpdateListener {
        }
 
        /**
+        * 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);
+               }
+       }
+
+       /**
         * Starts the core.
         */
        public void start() {
@@ -1930,6 +2274,49 @@ public class Core implements IdentityListener, UpdateListener {
                coreListenerManager.fireUpdateFound(version, releaseTime, latestEdition);
        }
 
+       //
+       // INTERFACE ImageInsertListener
+       //
+
+       /**
+        * {@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 fb83c9f..96a754f 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;
@@ -140,4 +141,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 6dbdc58..1e050ea 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;
@@ -215,4 +216,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 e22f407..90383e1 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 74f257d..efe28f5 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. */
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..d7df845
--- /dev/null
@@ -0,0 +1,333 @@
+/*
+ * 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.List;
+import java.util.UUID;
+
+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 images in this album. */
+       private final List<Image> images = new ArrayList<Image>();
+
+       /** The parent album. */
+       private Album parent;
+
+       /** The title of this album. */
+       private String title;
+
+       /** The description of this album. */
+       private String description;
+
+       /** The index of the album picture. */
+       private int albumImage = -1;
+
+       /**
+        * 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().isNull("Current Album Owner", this.sone).isNotNull("New Album Owner", 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).isNull("Album Parent", album.parent).check();
+               albums.add(album);
+               album.setParent(this);
+       }
+
+       /**
+        * 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();
+       }
+
+       /**
+        * Returns the images in this album.
+        *
+        * @return The images in this album
+        */
+       public List<Image> getImages() {
+               return new ArrayList<Image>(images);
+       }
+
+       /**
+        * 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();
+               image.setAlbum(this);
+               images.add(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();
+               images.remove(image);
+       }
+
+       /**
+        * 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 == -1) {
+                       return null;
+               }
+               return images.get(albumImage);
+       }
+
+       /**
+        * 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() {
+               Validation.begin().isNotNull("Album Parent", parent).check();
+               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(')');
+
+               /* add nested albums. */
+               fingerprint.append("Albums(");
+               for (Album album : albums) {
+                       fingerprint.append(album.getFingerprint());
+               }
+               fingerprint.append(')');
+
+               /* add images. */
+               fingerprint.append("Images(");
+               for (Image image : images) {
+                       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..d6e7114
--- /dev/null
@@ -0,0 +1,303 @@
+/*
+ * 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().isNull("Current Image Owner", this.sone).isNotNull("New Image Owner", sone);
+               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().isNull("Current Album", this.album).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().isNull("Current Image Key", this.key).isNotNull("New Image Key", 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().isEqual("Current Image Creation Time", this.creationTime, 0).isGreater("New Image Creation Time", creationTime, 0).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().isEqual("Current Image Width", this.width, 0).isGreater("New Image Width", width, 0).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().isEqual("Current Image Height", this.height, 0).isGreater("New Image Height", height, 0);
+               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();
+       }
+
+}
index 2dd4bc1..aa4a155 100644 (file)
@@ -32,6 +32,7 @@ import net.pterodactylus.sone.freenet.wot.Identity;
 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;
 
 /**
@@ -110,6 +111,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();
 
@@ -247,7 +251,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;
@@ -597,6 +601,51 @@ 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);
+       }
+
+       /**
         * Returns Sone-specific options.
         *
         * @return The options of this Sone
@@ -647,6 +696,12 @@ 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();
        }
 
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;
+       }
+
+}
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;
+       }
+
+}
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..3d6c731
--- /dev/null
@@ -0,0 +1,89 @@
+/*
+ * 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.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>\" />"));
+
+       /** 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);
+
+               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();
+               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", image.getDescription());
+               linkTemplateContext.set("title", image.getTitle());
+
+               StringWriter stringWriter = new StringWriter();
+               linkTemplate.render(linkTemplateContext, stringWriter);
+               return stringWriter.toString();
+       }
+
+}
index cea4452..7c30b76 100644 (file)
@@ -212,7 +212,7 @@ public class FreenetLinkParser implements Parser<FreenetLinkParserContext> {
                                                        if (name == null) {
                                                                name = link.substring(0, Math.min(9, link.length()));
                                                        }
-                                                       boolean fromPostingSone = ((linkType == LinkType.SSK) || (linkType == LinkType.USK)) && link.substring(4, Math.min(link.length(), 47)).equals(context.getPostingSone().getId());
+                                                       boolean fromPostingSone = (context.getPostingSone() != null) && ((linkType == LinkType.SSK) || (linkType == LinkType.USK)) && link.substring(4, Math.min(link.length(), 47)).equals(context.getPostingSone().getId());
                                                        parts.add(fromPostingSone ? createTrustedFreenetLinkPart(link, name) : createFreenetLinkPart(link, name));
                                                } catch (MalformedURLException mue1) {
                                                        /* not a valid link, insert as plain text. */
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..afe95b7
--- /dev/null
@@ -0,0 +1,72 @@
+/*
+ * 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.Page.Request.Method;
+import net.pterodactylus.util.template.Template;
+import net.pterodactylus.util.template.TemplateContext;
+
+/**
+ * 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(Request 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().saveSone(currentSone);
+                       throw new RedirectException("imageBrowser.html?album=" + album.getId());
+               }
+       }
+
+}
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..6855e38
--- /dev/null
@@ -0,0 +1,77 @@
+/*
+ * 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.Page.Request.Method;
+import net.pterodactylus.util.template.Template;
+import net.pterodactylus.util.template.TemplateContext;
+
+/**
+ * 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(Request 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..999a38a
--- /dev/null
@@ -0,0 +1,72 @@
+/*
+ * 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.Page.Request.Method;
+import net.pterodactylus.util.template.Template;
+import net.pterodactylus.util.template.TemplateContext;
+
+/**
+ * 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(Request 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);
+       }
+
+}
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..eaf8bf6
--- /dev/null
@@ -0,0 +1,69 @@
+/*
+ * 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.Page.Request.Method;
+import net.pterodactylus.util.template.Template;
+import net.pterodactylus.util.template.TemplateContext;
+
+/**
+ * TODO
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class EditAlbumPage extends SoneTemplatePage {
+
+       /**
+        * TODO
+        *
+        * @param template
+        * @param webInterface
+        */
+       public EditAlbumPage(Template template, WebInterface webInterface) {
+               super("editAlbum.html", template, "Page.EditAlbum.Title", webInterface, true);
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected void processTemplate(Request 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 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().saveSone(album.getSone());
+                       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..eab3bb9
--- /dev/null
@@ -0,0 +1,75 @@
+/*
+ * 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.Page.Request.Method;
+import net.pterodactylus.util.template.Template;
+import net.pterodactylus.util.template.TemplateContext;
+
+/**
+ * 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(Request request, TemplateContext templateContext) throws RedirectException {
+               super.processTemplate(request, templateContext);
+               if (request.getMethod() == Method.POST) {
+                       String imageId = request.getHttpRequest().getPartAsStringFailsafe("image", 36);
+                       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");
+                       }
+                       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().saveSone(image.getSone());
+                       throw new RedirectException("imageBrowser.html?image=" + image.getId());
+               }
+       }
+
+}
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..fdd72a8
--- /dev/null
@@ -0,0 +1,64 @@
+/*
+ * 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 net.pterodactylus.sone.data.TemporaryImage;
+import net.pterodactylus.sone.web.page.Page;
+
+/**
+ * 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 {
+
+       /** 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 Response handleRequest(Request request) {
+               String imageId = request.getHttpRequest().getParam("image");
+               TemporaryImage temporaryImage = webInterface.getCore().getTemporaryImage(imageId);
+               if (temporaryImage == null) {
+                       return new Response(404, "Not found.", "text/plain; charset=utf-8", "");
+               }
+               return new Response(200, "OK", temporaryImage.getMimeType(), temporaryImage.getImageData()).setHeader("Content-Disposition", "attachment; filename=" + temporaryImage.getId() + "." + temporaryImage.getMimeType().substring(temporaryImage.getMimeType().lastIndexOf("/") + 1));
+       }
+
+}
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..778b429
--- /dev/null
@@ -0,0 +1,78 @@
+/*
+ * 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.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(Request 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);
+       }
+
+}
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..4d98179
--- /dev/null
@@ -0,0 +1,160 @@
+/*
+ * 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.Page.Request.Method;
+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 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(Request 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 b3772f9..f5b111f 100644 (file)
@@ -36,6 +36,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;
@@ -44,10 +46,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.NotificationManagerAccessor;
 import net.pterodactylus.sone.template.ParserFilter;
@@ -174,6 +178,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.
         *
@@ -191,6 +204,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(NotificationManager.class, new NotificationManagerAccessor());
                templateContextFactory.addAccessor(Trust.class, new TrustAccessor());
@@ -210,6 +224,7 @@ 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.addProvider(Provider.TEMPLATE_CONTEXT_PROVIDER);
                templateContextFactory.addProvider(new ClassPathTemplateProvider());
@@ -236,6 +251,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);
        }
 
        //
@@ -536,6 +560,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 aboutTemplate = TemplateParser.parse(createReader("/templates/about.html"));
@@ -564,6 +592,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)));
@@ -584,6 +619,7 @@ public class WebInterface implements CoreListener {
                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 GetImagePage(this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new GetTranslationPage(this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new GetStatusAjaxPage(this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new DismissNotificationAjaxPage(this)));
@@ -793,6 +829,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 dd54f41..d26da79 100644 (file)
@@ -316,9 +316,11 @@ public interface Page {
                 *            The name of the header
                 * @param value
                 *            The value of the header
+                * @return This response
                 */
-               public void setHeader(String name, String value) {
+               public Response setHeader(String name, String value) {
                        headers.put(name, value);
+                       return this;
                }
 
                /**
index a73432d..a5f423e 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
@@ -164,6 +166,47 @@ 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.CreateAlbum.Button.CreateAlbum=Create Album
+Page.ImageBrowser.Album.Edit.Title=Edit Album
+Page.ImageBrowser.Album.Label.Title=Title:
+Page.ImageBrowser.Album.Label.Description=Description:
+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.Save=Save Image
+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
@@ -240,6 +283,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
@@ -268,11 +320,19 @@ 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.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…
@@ -299,3 +359,6 @@ 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:
index f6bb62d..9470850 100644 (file)
@@ -93,6 +93,10 @@ textarea {
        border: none;
 }
 
+#sone .parsed {
+       white-space: pre-wrap;
+}
+
 #sone #notification-area {
        margin-top: 1em;
 }
@@ -549,6 +553,36 @@ textarea {
        position: relative;
 }
 
+#sone .backlinks {
+       font-size: 80%;
+}
+
+#sone .backlinks .backlink {
+       display: inline;
+}
+
+#sone .album {
+}
+
+#sone .image {
+       width: 200px;
+       height: 150px;
+       display: table-cell;
+       vertical-align: middle;
+       text-align: center;
+       padding: 0.5ex;
+}
+
+#sone .image img, #sone .single-image img {
+       padding: 1ex;
+       border: solid 1px #000;
+       background-color: #fff;
+}
+
+#sone .backlinks .separator {
+       display: inline;
+}
+
 #sone #search {
        text-align: right;
 }
@@ -635,7 +669,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
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..9bcaa9d
--- /dev/null
@@ -0,0 +1,251 @@
+<%include include/head.html>
+
+       <div class="page-id hidden">image-browser</div>
+
+       <%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();
+                                       });
+                               </script>
+                       <%/if>
+
+                       <h1><%= 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>
+
+                       <%foreach album.albums album>
+                               <div class="album image">
+                                       <a href="imageBrowser.html?album=<% album.id|html>">
+                                               <%ifnull album.image>
+                                                       <img src="images/unknown-image-0.png" width="200" height="150" alt="<% album.title|html>" title="<% album.title|html>" />
+                                               <%else><!-- TODO -->
+                                                       <img src="images/unknown-image-0.png" width="200" height="150" alt="<% album.title|html>" title="<% album.title|html>" />
+                                               <%/if>
+                                       </a>
+                               </div>
+                       <%/foreach>
+
+                       <%foreach album.images image>
+                               <div class="image">
+                                       <a href="imageBrowser.html?image=<%image.id|html>"><% image|image-link max-width=200 max-height=150></a>
+                               </div>
+                       <%/foreach>
+
+                       <div id="description">
+                               <% album.description|html>
+                       </div>
+
+                       <%if album.sone.current>
+                               <%include include/uploadImage.html>
+                               <%include include/createAlbum.html>
+
+                               <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>" />
+
+                                       <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>
+
+                               <%if album.empty>
+                                       <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>
+                               <%/if>
+
+                       <%/if>
+
+               <%/if>
+
+       <%elseif imageRequested>
+
+               <h1><%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();
+                                       });
+                               </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>
+
+                               <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="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>
+
+                               <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>
+
+                       <%/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();
+                               });
+                       </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>
+
+                       <%foreach sone.albums album>
+                               <div class="album image">
+                                       <a href="imageBrowser.html?album=<% album.id|html>">
+                                               <%ifnull album.image>
+                                                       <img src="images/unknown-image-0.png" width="200" height="150" alt="<% album.title|html>" title="<% album.title|html>" />
+                                               <%else><!-- TODO -->
+                                                       <img src="images/unknown-image-0.png" width="200" height="150" alt="<% album.title|html>" title="<% album.title|html>" />
+                                               <%/if>
+                                       </a>
+                               </div>
+                       <%/foreach>
+
+                       <%if sone.current>
+                               <%include include/createAlbum.html>
+                       <%/if>
+
+               <%/if>
+
+       <%/if>
+
+<%include include/tail.html>
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>
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>
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 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>