Working upload of photos (individual and zip)
authorErik Brakkee <erik@brakkee.org>
Wed, 25 Sep 2013 19:33:24 +0000 (21:33 +0200)
committerErik Brakkee <erik@brakkee.org>
Wed, 25 Sep 2013 19:33:24 +0000 (21:33 +0200)
Now streaming the images instead of reading all data when
constructing the page.

13 files changed:
src/main/java/org/wamblee/photos/model/authorization/AuthorizedAlbum.java
src/main/java/org/wamblee/photos/model/filesystem/FileSystemAlbum.java
src/main/java/org/wamblee/photos/servlet/ImageSender.java [new file with mode: 0644]
src/main/java/org/wamblee/photos/wicket/AlbumPanel.html
src/main/java/org/wamblee/photos/wicket/AlbumPanel.java
src/main/java/org/wamblee/photos/wicket/HomePage.java
src/main/java/org/wamblee/photos/wicket/PhotoPanel.java
src/main/java/org/wamblee/photos/wicket/UploadPanel.html [new file with mode: 0644]
src/main/java/org/wamblee/photos/wicket/UploadPanel.java [new file with mode: 0644]
src/main/webapp/WEB-INF/web.xml
src/main/webapp/images/folder.png [moved from src/main/java/org/wamblee/photos/wicket/folder.png with 100% similarity]
src/test/java/org/wamblee/photos/model/AlbumTest.java
src/test/java/org/wamblee/photos/model/AuthorizedAlbumTest.java [new file with mode: 0644]

index 48fb47ace6be662593ceb477ad20e399278120aa..f76fd09a1730005ef7364522f47a002f2e96d653 100644 (file)
@@ -48,8 +48,7 @@ import org.wamblee.security.authorization.WriteOperation;
 @AuthorizedPhotos
 public class AuthorizedAlbum extends AuthorizedPhotoEntry implements Album {
 
-    private static final Logger LOGGER = Logger.getLogger(AuthorizedAlbum.class
-        .getName());
+    private static final Logger LOGGER = Logger.getLogger(AuthorizedAlbum.class.getName());
 
     private AuthorizationService _authorizer;
 
@@ -57,37 +56,29 @@ public class AuthorizedAlbum extends AuthorizedPhotoEntry implements Album {
 
     private HttpSession _session;
 
-    protected AuthorizedAlbum() {
-        super(null);
-        // for CDI
-    }
-
     /**
      * Constructs concurrent album as a decorator for an album implementation.
-     * 
-     * @param aAlbum
-     *            Album to decorate.
+     *
+     * @param aAlbum Album to decorate.
      */
     @Inject
-    public AuthorizedAlbum(@AllPhotos Album aAlbum,
-        AuthorizationService aService,
-        @PhotoCache Cache<String, ArrayList<PhotoEntry>> aCache,
-        HttpSession aSession) {
+    public AuthorizedAlbum(@AllPhotos Album aAlbum, AuthorizationService aService,
+            @PhotoCache Cache<String, ArrayList<PhotoEntry>> aCache, HttpSession aSession) {
         super(aAlbum);
         _authorizer = aService;
-        _authorizedEntries = new CachedObject<String, ArrayList<PhotoEntry>>(
-            aCache, aSession.getId() + "/" + aAlbum.getPath(),
-            new CachedObject.Computation<String, ArrayList<PhotoEntry>>() {
-                public ArrayList<PhotoEntry> getObject(String aObjectKey) {
-                    return AuthorizedAlbum.this.compute();
-                }
-            });
+        _authorizedEntries = new CachedObject<String, ArrayList<PhotoEntry>>(aCache,
+                "session:" + aSession.getId() + "/" + aAlbum.getPath(),
+                new CachedObject.Computation<String, ArrayList<PhotoEntry>>() {
+                    public ArrayList<PhotoEntry> getObject(String aObjectKey) {
+                        return AuthorizedAlbum.this.compute();
+                    }
+                });
         _session = aSession;
     }
 
     /**
      * Computes the cache of photo entries to which read access is allowed.
-     * 
+     *
      * @return Photo entries to which read access is allowed.
      */
     private synchronized ArrayList<PhotoEntry> compute() {
@@ -100,6 +91,9 @@ public class AuthorizedAlbum extends AuthorizedPhotoEntry implements Album {
                 // automatically.
             }
         }
+        if (result == null) {
+            throw new RuntimeException("Result is null");
+        }
         return result;
     }
 
@@ -110,9 +104,8 @@ public class AuthorizedAlbum extends AuthorizedPhotoEntry implements Album {
     /**
      * Creates a decorate for the photo entry to make it safe for concurrent
      * access.
-     * 
-     * @param aEntry
-     *            Entry to decorate
+     *
+     * @param aEntry Entry to decorate
      * @return Decorated photo.
      */
     private <T extends PhotoEntry> T decorate(T aEntry) {
@@ -121,11 +114,9 @@ public class AuthorizedAlbum extends AuthorizedPhotoEntry implements Album {
         } else if (aEntry instanceof Photo) {
             return (T) new AuthorizedPhoto((Photo) aEntry);
         } else if (aEntry instanceof Album) {
-            return (T) new AuthorizedAlbum((Album) aEntry, _authorizer,
-                _authorizedEntries.getCache(), _session);
+            return (T) new AuthorizedAlbum((Album) aEntry, _authorizer, _authorizedEntries.getCache(), _session);
         } else {
-            throw new IllegalArgumentException(
-                "Entry is neither a photo nor an album: " + aEntry);
+            throw new IllegalArgumentException("Entry is neither a photo nor an album: " + aEntry);
         }
     }
 
@@ -157,7 +148,7 @@ public class AuthorizedAlbum extends AuthorizedPhotoEntry implements Album {
                 } else {
                     if (!(entry instanceof Album)) {
                         throw new IllegalArgumentException(getPath() + " " +
-                            aPath);
+                                aPath);
                     }
                     return ((Album) entry).getEntry(remainder);
                 }
@@ -192,8 +183,13 @@ public class AuthorizedAlbum extends AuthorizedPhotoEntry implements Album {
      */
     public void addImage(String aId, InputStream aImage) throws IOException {
         _authorizer.check(this, new WriteOperation());
+        int oldsize = _authorizedEntries.get().size();
         _authorizedEntries.invalidate();
         decorated().addImage(aId, aImage);
+        int newsize = _authorizedEntries.get().size();
+        if (newsize != oldsize + 1) {
+            throw new RuntimeException("cache was not refreshed property");
+        }
     }
 
     /*
@@ -214,16 +210,14 @@ public class AuthorizedAlbum extends AuthorizedPhotoEntry implements Album {
      */
     public void removeEntry(String aId) throws IOException {
         // Check whether deletion is allowed.
-        PhotoEntry entry = _authorizer.check(decorated().getEntry("/" + aId),
-            new DeleteOperation());
+        PhotoEntry entry = _authorizer.check(decorated().getEntry("/" + aId), new DeleteOperation());
         _authorizedEntries.invalidate();
         decorated().removeEntry(aId);
     }
 
     public Photo findPhotoBefore(String aId) {
         Photo entry = decorated().findPhotoBefore(aId);
-        while (entry != null &&
-            !_authorizer.isAllowed(entry, new AllOperation())) {
+        while (entry != null && !_authorizer.isAllowed(entry, new AllOperation())) {
             entry = decorated().findPhotoBefore(entry.getId());
         }
         return decorate(entry);
@@ -231,8 +225,7 @@ public class AuthorizedAlbum extends AuthorizedPhotoEntry implements Album {
 
     public Photo findPhotoAfter(String aId) {
         Photo entry = decorated().findPhotoAfter(aId);
-        while (entry != null &&
-            !_authorizer.isAllowed(entry, new AllOperation())) {
+        while (entry != null && !_authorizer.isAllowed(entry, new AllOperation())) {
             entry = decorated().findPhotoAfter(entry.getId());
         }
         return decorate(entry);
index c08288b7e028090799124c84e5c9e3ddcb2c51c1..fcf4695311ae4e1ea4a03e0f6a0abe37ea31e530 100644 (file)
@@ -129,7 +129,7 @@ public class FileSystemAlbum implements Album {
         }
         _dir = aDir;
         _path = aPath;
-        _entries = new CachedObject<String, ArrayList<PhotoEntry>>(aCache, aPath, new AlbumComputation(this));
+        _entries = new CachedObject<String, ArrayList<PhotoEntry>>(aCache, "fs:" + aPath, new AlbumComputation(this));
     }
 
     /**
diff --git a/src/main/java/org/wamblee/photos/servlet/ImageSender.java b/src/main/java/org/wamblee/photos/servlet/ImageSender.java
new file mode 100644 (file)
index 0000000..a734184
--- /dev/null
@@ -0,0 +1,147 @@
+/*
+ * Copyright 2005 the original author or authors.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.wamblee.photos.servlet;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import javax.inject.Inject;
+import javax.servlet.ServletException;
+import javax.servlet.ServletOutputStream;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.wamblee.photos.model.Album;
+import org.wamblee.photos.model.Photo;
+import org.wamblee.photos.model.PhotoEntry;
+import org.wamblee.photos.model.plumbing.AuthorizedPhotos;
+
+/**
+ * Sends an image (either thumbnail or full size) from the photo album.
+ * <p/>
+ * The returned picture is defined based on
+ * {@link javax.servlet.http.HttpServletRequest#getPathInfo}. as follows. If the
+ * path info starts with the string defined by {@link #THUMBNAIL_NAME}, then a
+ * thumbnail image is served and the rest of the path info is the relative path
+ * of the picture. Otherwise the path info is identical to the relative path of
+ * the picture.
+ */
+public class ImageSender extends HttpServlet {
+
+    @Inject
+    @AuthorizedPhotos
+    private Album root;
+
+    static final long serialVersionUID = 4069997717483260853L;
+
+    enum ImageType {
+        thumbnail,
+        photo,
+        resource
+    }
+
+    /*
+     * (non-Javadoc)
+     *
+     * @see
+     * javax.servlet.http.HttpServlet#doGet(javax.servlet.http.HttpServletRequest
+     * , javax.servlet.http.HttpServletResponse)
+     */
+    protected void doGet(HttpServletRequest request, HttpServletResponse response)
+            throws ServletException, IOException {
+        doPost(request, response);
+    }
+
+    /*
+     * (non-Javadoc)
+     *
+     * @see
+     * javax.servlet.http.HttpServlet#doPost(javax.servlet.http.HttpServletRequest
+     * , javax.servlet.http.HttpServletResponse)
+     */
+    protected void doPost(HttpServletRequest aRequest, HttpServletResponse aResponse)
+            throws ServletException, IOException {
+
+        String entryPath = aRequest.getPathInfo();
+        if (entryPath == null) {
+            entryPath = "/";
+        }
+
+        ImageType type = null;
+        for (ImageType t : ImageType.values()) {
+            if (entryPath.startsWith("/" + t + "/")) {
+                entryPath = entryPath.substring(t.toString().length() + 1);
+                type = t;
+                break;
+            }
+        }
+
+        if (type == null) {
+            throw new RuntimeException("unsupported URL " + aRequest.getPathInfo());
+        }
+
+        switch (type) {
+            case thumbnail:
+            case photo: {
+                if (entryPath.endsWith(".jpg")) {
+                    entryPath = entryPath.substring(0, entryPath.length() - ".jpg".length());
+                }
+                PhotoEntry entry = root.getEntry(entryPath);
+
+                Photo photo = (Photo) entry;
+                InputStream is = null;
+                if (type == ImageType.thumbnail) {
+                    is = photo.getThumbNail();
+                } else {
+                    is = photo.getPhoto();
+                }
+                sendImage(aResponse, is);
+                break;
+            }
+            case resource: {
+                try (InputStream is = getServletContext().getResourceAsStream("/images/" + entryPath)) {
+                    ServletOutputStream os = aResponse.getOutputStream();
+                    byte[] buffer = new byte[4096];
+                    int n;
+                    while ((n = is.read(buffer)) > 0) {
+                        os.write(buffer, 0, n);
+                    }
+                }
+                break;
+            }
+
+            default: {
+                //throw new RuntimeException("Unknown type " + type);
+            }
+        }
+    }
+
+    /**
+     * Sends the image.
+     *
+     * @param response Response
+     * @param is       Input stream of the image.
+     * @throws IOException In case an IO problem occurs.
+     */
+
+    private void sendImage(HttpServletResponse response, InputStream is) throws IOException {
+        int c;
+        while ((c = is.read()) >= 0) {
+            response.getOutputStream().write(c);
+        }
+    }
+}
index 0df1af42ee18b0f067267999100264ea036423cf..0786ab4b853aa0255144353199e9c14045b70486 100644 (file)
@@ -48,6 +48,9 @@
             </tr>
         </table>
     </div>
+    <div id="upload">
+        <span wicket:id="uploadPanel"/>
+    </div>
 </wicket:panel>
 
 </body>
index 0c7841c7c32da4de8afc308dac7915e9a2bd2279..70fc097f4717d05c0957d8aec219c5ef1a3f7ec0 100644 (file)
@@ -18,17 +18,20 @@ package org.wamblee.photos.wicket;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
+import java.io.Serializable;
 import java.util.logging.Logger;
 import javax.inject.Inject;
+import javax.servlet.ServletContext;
 
 import org.apache.wicket.PageParameters;
+import org.apache.wicket.markup.ComponentTag;
 import org.apache.wicket.markup.html.WebMarkupContainer;
 import org.apache.wicket.markup.html.basic.Label;
 import org.apache.wicket.markup.html.image.Image;
 import org.apache.wicket.markup.html.link.Link;
 import org.apache.wicket.markup.html.panel.Panel;
 import org.apache.wicket.markup.repeater.RepeatingView;
-import org.apache.wicket.resource.ByteArrayResource;
+import org.wamblee.general.ValueHolder;
 import org.wamblee.photos.model.Album;
 import org.wamblee.photos.model.Photo;
 import org.wamblee.photos.model.PhotoEntry;
@@ -39,6 +42,16 @@ import org.wamblee.photos.model.plumbing.AuthorizedPhotos;
  */
 public class AlbumPanel extends Panel {
 
+    public static class MyValueHolder<T extends Serializable> extends ValueHolder<T> implements Serializable {
+        public MyValueHolder(T aValue) {
+            super(aValue);
+        }
+
+        public MyValueHolder() {
+            super();
+        }
+    }
+
     private static final Logger LOGGER = Logger.getLogger(AlbumPanel.class.getName());
 
     private static final long serialVersionUID = 1L;
@@ -49,6 +62,9 @@ public class AlbumPanel extends Panel {
     @AuthorizedPhotos
     private transient Album authorized;
 
+    @Inject
+    private ServletContext context;
+
     private class SerializableEntryLink extends Link {
 
         private String path;
@@ -60,7 +76,6 @@ public class AlbumPanel extends Panel {
 
         @Override
         public void onClick() {
-            System.out.println("Entry " + path + " was clicked");
             PageParameters pars = new PageParameters();
             pars.put("path", path);
 
@@ -98,19 +113,7 @@ public class AlbumPanel extends Panel {
             index = 0;
         }
 
-        PhotoEntry current = authorized.getEntry(path);
-        if (current instanceof Photo) {
-            throw new RuntimeException("AlbumPanel can only show album: " + current.getClass().getName());
-        }
-        final Album album = (Album) current;
-
         Link prevLink = new Link("prevLink") {
-            {
-                if (index - MAX_ROWS * MAX_COLUMNS < 0) {
-                    setEnabled(false);
-                }
-            }
-
             @Override
             public void onClick() {
                 PageParameters pars = new PageParameters();
@@ -118,19 +121,18 @@ public class AlbumPanel extends Panel {
                 pars.put("index", index - MAX_ROWS * MAX_COLUMNS);
                 setResponsePage(HomePage.class, pars);
             }
+
+            @Override
+            public boolean isEnabled() {
+                return index - MAX_ROWS * MAX_COLUMNS >= 0;
+            }
         };
         add(prevLink);
 
         // Avoid implicit references to the album to keep the link objects
         // small and serializable.
-        final int albumSize = album.size();
+        final int albumSize = getAlbum().size();
         Link nextLink = new Link("nextLink") {
-            {
-                if (index + MAX_ROWS * MAX_COLUMNS >= albumSize) {
-                    setEnabled(false);
-                }
-            }
-
             @Override
             public void onClick() {
                 PageParameters pars = new PageParameters();
@@ -138,6 +140,11 @@ public class AlbumPanel extends Panel {
                 pars.put("index", index + MAX_ROWS * MAX_COLUMNS);
                 setResponsePage(HomePage.class, pars);
             }
+
+            @Override
+            public boolean isEnabled() {
+                return index + MAX_ROWS * MAX_COLUMNS < albumSize;
+            }
         };
         add(nextLink);
 
@@ -164,6 +171,7 @@ public class AlbumPanel extends Panel {
 
         RepeatingView pageLinks = new RepeatingView("pageLinks");
         add(pageLinks);
+        Album album = getAlbum();
         for (int i = 0; i < album.size() / MAX_ROWS / MAX_COLUMNS; i++) {
             final int istart = i * MAX_ROWS * MAX_COLUMNS;
             Link pageLink = new Link("pageLink") {
@@ -188,34 +196,91 @@ public class AlbumPanel extends Panel {
             pageLinks.add(container);
         }
 
-        int ientry = index;
-        int irow = 0;
-        RepeatingView row = new RepeatingView("row");
-        add(row);
-        while (irow < MAX_ROWS && ientry < album.size()) {
-            int icolumn = 0;
-            WebMarkupContainer columns = new WebMarkupContainer(row.newChildId());
-            row.add(columns);
-            RepeatingView column = new RepeatingView("column");
-            columns.add(column);
-            while (icolumn < MAX_COLUMNS && ientry < album.size()) {
-                WebMarkupContainer thumbnail = new WebMarkupContainer(column.newChildId());
-                column.add(thumbnail);
-
-                final PhotoEntry entry = album.getEntry(ientry);
-                Link link = new SerializableEntryLink("thumbnail", entry.getPath());
-                thumbnail.add(link);
-                ImageData data = getData(entry);
-
-                // TODO very inefficient. all data is loaded when generating the page.
-                link.add(new Image("image", new ByteArrayResource(data.getContentType(), data.getData())));
-
-                link.add(new Label("name", album.getEntry(ientry).getId()));
-                icolumn++;
-                ientry++;
+        RepeatingView row = new RepeatingView("row") {
+            @Override
+            protected void onPopulate() {
+                removeAll();
+                final ValueHolder<Integer> ientry = new MyValueHolder<Integer>(index);
+                int irow = 0;
+                Album album = getAlbum();
+                while (irow < MAX_ROWS && ientry.getValue() < album.size()) {
+                    WebMarkupContainer columns = new WebMarkupContainer(newChildId());
+                    add(columns);
+                    RepeatingView column = new RepeatingView("column") {
+                        @Override
+                        protected void onPopulate() {
+                            removeAll();
+                            int icolumn = 0;
+                            Album album = getAlbum();
+                            while (icolumn < MAX_COLUMNS && ientry.getValue() < album.size()) {
+                                WebMarkupContainer thumbnail = new WebMarkupContainer(newChildId());
+                                add(thumbnail);
+
+                                final PhotoEntry entry = album.getEntry(ientry.getValue());
+                                Link link = new SerializableEntryLink("thumbnail", entry.getPath());
+                                thumbnail.add(link);
+                                //ImageData data = getData(entry);
+
+                                // TODO very inefficient. all data is loaded when generating the page.
+                                //link.add(new Image("image",
+                                //        new ByteArrayResource(data.getContentType(), data.getData())));
+
+                                //link.add(new Image("image",
+                                //        new ContextRelativeResource("image/thumbnail" + entry.getPath())));
+
+                                //final String url = "/image/thumbnail/" + entry.getPath();
+
+                                if (entry instanceof Photo) {
+                                    link.add(new Image("image") {
+                                        @Override
+                                        protected void onComponentTag(ComponentTag tag) {
+                                            tag.put("src",
+                                                    context.getContextPath() + "/image/thumbnail/" + entry.getPath());
+                                        }
+                                    });
+                                } else {
+                                    link.add(new Image("image") {
+                                        @Override
+                                        protected void onComponentTag(ComponentTag tag) {
+                                            //tag.put("src", context.getContextPath() + "/resources/folder.png" +
+                                            //        entry.getPath());
+                                            tag.put("src", context.getContextPath() + "/image/resource/folder.png");
+                                        }
+                                    });
+                                }
+
+                                link.add(new Label("name", album.getEntry(ientry.getValue()).getId()));
+                                icolumn++;
+                                ientry.setValue(ientry.getValue() + 1);
+                            }
+                        }
+                    };
+                    columns.add(column);
+                    irow++;
+                }
             }
-            irow++;
+        };
+
+        add(row);
+
+        // upload panel
+        if (path.equals("/"))
+
+        {
+            add(new WebMarkupContainer("uploadPanel"));
+        } else
+
+        {
+            add(new UploadPanel("uploadPanel", path));
+        }
+    }
+
+    private Album getAlbum() {
+        PhotoEntry current = authorized.getEntry(path);
+        if (current instanceof Photo) {
+            throw new RuntimeException("AlbumPanel can only show album: " + current.getClass().getName());
         }
+        return (Album) current;
     }
 
     public static final class ImageData {
index 95290d34dd93dd021180864ef7afd2e70d95ebc0..4a34bc0da750792de94ece9fb53d7a0605f90e0d 100644 (file)
@@ -65,7 +65,6 @@ public class HomePage extends BasePage {
 
         @Override
         public void onClick() {
-            System.out.println("Entry " + path + " was clicked");
             PageParameters pars = new PageParameters();
             pars.put("path", path);
             setResponsePage(HomePage.class, pars);
index a0fb7c1d4b1453806da1c5087fe6e12c69828e4b..e47b19a59a2ef8bf5bdaa2fccc30f9fab9ea84f0 100644 (file)
@@ -64,30 +64,12 @@ public class PhotoPanel extends Panel {
         }
         add(new Label("path", path));
 
-        PhotoEntry current = authorized.getEntry(path);
-        if (current instanceof Album) {
-            throw new RuntimeException("PhotoPanel can only show a photo: " + current.getClass().getName());
-        }
-        final Photo photo = (Photo) current;
-
-        String parentPath_ = path.substring(0, path.lastIndexOf("/"));
-        if (parentPath_.length() == 0) {
-            parentPath_ = "/";
-        }
-        final String parentPath = parentPath_;
-        final Album parent = (Album) authorized.getEntry(parentPath);
-        final Photo before = parent.findPhotoBefore(photo.getId());
-        final Photo after = parent.findPhotoAfter(photo.getId());
+        String parentPath = getParentPath();
 
         Link prevLink = new Link("prevLink") {
-            {
-                if (before == null) {
-                    setEnabled(false);
-                }
-            }
-
             @Override
             public void onClick() {
+                Photo before = getPrevPhoto();
                 if (before == null) {
                     return;
                 }
@@ -95,19 +77,19 @@ public class PhotoPanel extends Panel {
                 pars.put("path", before.getPath());
                 setResponsePage(HomePage.class, pars);
             }
+
+            @Override
+            public boolean isEnabled() {
+                return getPrevPhoto() != null;
+            }
         };
 
         add(prevLink);
 
         Link nextLink = new Link("nextLink") {
-            {
-                if (after == null) {
-                    setEnabled(false);
-                }
-            }
-
             @Override
             public void onClick() {
+                Photo after = getNextPhoto();
                 if (after == null) {
                     return;
                 }
@@ -115,6 +97,11 @@ public class PhotoPanel extends Panel {
                 pars.put("path", after.getPath());
                 setResponsePage(HomePage.class, pars);
             }
+
+            @Override
+            public boolean isEnabled() {
+                return getNextPhoto() != null;
+            }
         };
 
         add(nextLink);
@@ -130,7 +117,7 @@ public class PhotoPanel extends Panel {
             public void onClick() {
                 PageParameters pars = new PageParameters();
 
-                pars.put("path", parentPath);
+                pars.put("path", getParentPath());
                 pars.put("index", 0);
                 setResponsePage(HomePage.class, pars);
             }
@@ -138,10 +125,38 @@ public class PhotoPanel extends Panel {
 
         add(parentLink);
 
-        Image image = new Image("photo", new ByteArrayResource("image/jpeg", getData(photo)));
+        Image image = new Image("photo", new ByteArrayResource("image/jpeg", getData(getPhoto())));
         add(image);
     }
 
+    private Photo getPhoto() {
+        PhotoEntry current = authorized.getEntry(path);
+        if (current instanceof Album) {
+            throw new RuntimeException("PhotoPanel can only show a photo: " + current.getClass().getName());
+        }
+        return (Photo) current;
+    }
+
+    private Photo getPrevPhoto() {
+        return getAlbum().findPhotoBefore(getPhoto().getId());
+    }
+
+    private Photo getNextPhoto() {
+        return getAlbum().findPhotoAfter(getPhoto().getId());
+    }
+
+    private Album getAlbum() {
+        return (Album) getAuthorizedPhotos().getEntry(getParentPath());
+    }
+
+    private String getParentPath() {
+        String parentPath = path.substring(0, path.lastIndexOf("/"));
+        if (parentPath.length() == 0) {
+            parentPath = "/";
+        }
+        return parentPath;
+    }
+
     private byte[] getData(Photo aPhoto) {
         try (InputStream is = aPhoto.getPhoto()) {
             return getBytes(is);
@@ -162,4 +177,8 @@ public class PhotoPanel extends Panel {
         }
         return bos.toByteArray();
     }
+
+    private Album getAuthorizedPhotos() {
+        return authorized;
+    }
 }
\ No newline at end of file
diff --git a/src/main/java/org/wamblee/photos/wicket/UploadPanel.html b/src/main/java/org/wamblee/photos/wicket/UploadPanel.html
new file mode 100644 (file)
index 0000000..773e257
--- /dev/null
@@ -0,0 +1,28 @@
+<html
+        xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.4-strict.dtd">
+<head>
+    <title>Wicket Quickstart Archetype Homepage</title>
+</head>
+<body>
+
+<wicket:panel>
+    <form wicket:id="uploadForm">
+        <br/>
+        Upload individual jpg photos or a zip of individual jpg photos.
+        <table>
+            <tbody>
+            <tr>
+                <td>
+                    <input wicket:id="file" type="file" size="40"/>
+                </td>
+                <td>
+                    <input type="submit" value="Upload photo(s)"/>
+                </td>
+            </tr>
+            </tbody>
+        </table>
+    </form>
+</wicket:panel>
+
+</body>
+</html>
diff --git a/src/main/java/org/wamblee/photos/wicket/UploadPanel.java b/src/main/java/org/wamblee/photos/wicket/UploadPanel.java
new file mode 100644 (file)
index 0000000..8a0e8d4
--- /dev/null
@@ -0,0 +1,138 @@
+/*
+ * Copyright 2005-2011 the original author or authors.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.wamblee.photos.wicket;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+import javax.inject.Inject;
+
+import org.apache.wicket.markup.html.form.Form;
+import org.apache.wicket.markup.html.form.upload.FileUpload;
+import org.apache.wicket.markup.html.form.upload.FileUploadField;
+import org.apache.wicket.markup.html.panel.Panel;
+import org.apache.wicket.util.lang.Bytes;
+import org.wamblee.photos.model.Album;
+import org.wamblee.photos.model.plumbing.AuthorizedPhotos;
+
+/**
+ * Created with IntelliJ IDEA.
+ * User: erik
+ * Date: 9/23/13
+ * Time: 8:33 PM
+ * To change this template use File | Settings | File Templates.
+ */
+public class UploadPanel extends Panel {
+
+    private static final Logger LOGGER = Logger.getLogger(UploadPanel.class.getName());
+
+    /**
+     * Extension to use for JPEGs.
+     */
+    private static final String JPG_EXTENSION = ".jpg";
+
+    /**
+     * Extension to use for ZIP files.
+     */
+    private static final String ZIP_EXTENSION = ".zip";
+
+    @Inject
+    @AuthorizedPhotos
+    private transient Album _authorized;
+
+    private String _path;
+
+    /**
+     * Upload field.
+     */
+    private FileUploadField _uploadField;
+
+    public UploadPanel(String aId, String aPath) {
+        super(aId);
+
+        _path = aPath;
+
+        Form form = new Form("uploadForm") {
+            protected void onSubmit() {
+                final FileUpload upload = _uploadField.getFileUpload();
+
+                if (upload == null) {
+                    return;
+                }
+
+                try {
+                    String filename = upload.getClientFileName();
+                    InputStream is = upload.getInputStream();
+
+                    if (filename.trim().length() == 0) {
+                        return;
+                    }
+                    Album album = (Album) _authorized.getEntry(_path);
+                    if (filename.toLowerCase().endsWith(JPG_EXTENSION)) {
+                        insertPhoto(album, is, filename);
+                    } else if (filename.toLowerCase().endsWith(ZIP_EXTENSION)) {
+
+                        try (ZipInputStream zip = new ZipInputStream(is)) {
+                            insertPhotosFromZipFile(album, zip);
+                        }
+                    } else {
+                        warn("Skipping entry with unknown file type '" + filename + "'");
+                    }
+                }
+                catch (Exception e) {
+                    LOGGER.log(Level.WARNING, e.getMessage(), e);
+                    error("ERROR:" + e.getMessage());
+                }
+            }
+        };
+        add(form);
+        _uploadField = new FileUploadField("file");
+        form.add(_uploadField);
+        form.setMultiPart(true);
+        form.setMaxSize(Bytes.megabytes(500));
+    }
+
+    private void insertPhotosFromZipFile(Album aAlbum, ZipInputStream aZipFile) throws IOException {
+        // zip extension
+        ZipEntry entry;
+        while ((entry = aZipFile.getNextEntry()) != null) {
+            try {
+                if (!entry.isDirectory() && entry.getName().toLowerCase().endsWith(JPG_EXTENSION)) {
+                    insertPhoto(aAlbum, aZipFile, new File(entry.getName()).getName());
+                } else {
+                    warn("Skipping entry '" + entry.getName() + "' from zip file.");
+                }
+            } finally {
+                aZipFile.closeEntry();
+            }
+        }
+    }
+
+    private void insertPhoto(Album aAlbum, InputStream aPhotoInputStream, String aFilename) throws IOException {
+        String photoName = aFilename.substring(0, aFilename.length() - JPG_EXTENSION.length());
+
+        if (aAlbum.getEntry("/" + photoName) != null) {
+            error("Photo '" + photoName + "' already exists in this album");
+            return;
+        }
+        aAlbum.addImage(photoName, aPhotoInputStream);
+        info("Photo '" + photoName + "' uploaded");
+    }
+}
index eaa1fd7eefb04c179d1b5472348079a9b777ef85..96f44530581640addda3b6ca7d7263f377d511d6 100644 (file)
           or "deployment". If no configuration is found, "development" is the default.
     -->
 
+    <!-- The image sender servlet has the sole task of sending
+               images to the client, either a thumbnail or full image -->
+    <servlet>
+        <description>Servlet which sends the raw images.</description>
+        <display-name>ImageSender</display-name>
+        <servlet-name>ImageSender</servlet-name>
+        <servlet-class>org.wamblee.photos.servlet.ImageSender</servlet-class>
+        <load-on-startup>1</load-on-startup>
+    </servlet>
+    <servlet-mapping>
+        <servlet-name>ImageSender</servlet-name>
+        <url-pattern>/image/*</url-pattern>
+    </servlet-mapping>
+
+
     <filter>
         <filter-name>authentication</filter-name>
         <filter-class>org.wamblee.photos.security.AuthenticationFilter</filter-class>
index a0c4d07dbdf2306574f4b7efa21755fe8ac6e59c..5d4c7aacf9507b7c94e15db3605451de32af18d3 100644 (file)
@@ -40,9 +40,6 @@ import static org.junit.Assert.*;
  */
 public class AlbumTest {
 
-    private static final String ADDED_METHOD = "photoAdded";
-    private static final String REMOVED_METHOD = "photoRemoved";
-
     private static final String TEST_RESOURCES = "src/test/resources/albumdata";
 
     private String resourcesPath;
@@ -68,7 +65,6 @@ public class AlbumTest {
         assertTrue(path.startsWith("file:"));
         path = path.substring("file:".length());
         resourcesPath = path + "../../" + TEST_RESOURCES;
-        System.out.println(resourcesPath);
     }
 
     private void copyDir(File aSource) {
@@ -99,6 +95,11 @@ public class AlbumTest {
         return new File(_testData.getRoot(), "data");
     }
 
+    protected Album createAlbum(File aDir, String aPath, Cache<String, ArrayList<PhotoEntry>> aCache)
+            throws IOException {
+        return new FileSystemAlbum(aDir, aPath, aCache);
+    }
+
     /**
      * Verifies that a non-existing album cannot be opened.
      */
@@ -106,7 +107,7 @@ public class AlbumTest {
     public void testNonExistingAlbum() {
         File dir = new File(_testData.getRoot(), "NonExisting");
         try {
-            Album album = new FileSystemAlbum(dir, "", _cache);
+            Album album = createAlbum(dir, "", _cache);
         }
         catch (IOException e) {
             return; // ok
@@ -124,7 +125,7 @@ public class AlbumTest {
 
         File dir = getTestOutputDir();
         copyDir(_dir);
-        Album album = new FileSystemAlbum(dir, "/", _cache);
+        Album album = createAlbum(dir, "/", _cache);
 
         int nentries = album.size();
         assertEquals(3, nentries);
@@ -177,7 +178,7 @@ public class AlbumTest {
 
         File dir = getTestOutputDir();
         copyDir(_dir);
-        Album album = new FileSystemAlbum(dir, "/", _cache);
+        Album album = createAlbum(dir, "/", _cache);
         assertEquals(0, album.size());
     }
 
@@ -190,7 +191,7 @@ public class AlbumTest {
 
         File dir = getTestOutputDir();
         copyDir(_dir);
-        Album album = new FileSystemAlbum(dir, "/", _cache);
+        Album album = createAlbum(dir, "/", _cache);
         assertEquals(0, album.size());
     }
 
@@ -202,7 +203,7 @@ public class AlbumTest {
         File _dir = new File(getTestAlbumData(), "AlbumPhotoMissing");
         File dir = getTestOutputDir();
         copyDir(_dir);
-        Album album = new FileSystemAlbum(dir, "/", _cache);
+        Album album = createAlbum(dir, "/", _cache);
         assertEquals(0, album.size());
     }
 
@@ -222,13 +223,13 @@ public class AlbumTest {
         assertTrue(new File(dir, FileSystemAlbum.PHOTOS_DIR).mkdir());
         assertTrue(new File(dir, FileSystemAlbum.THUMBNAILS_DIR).mkdir());
 
-        Album album = new FileSystemAlbum(dir, "/", _cache);
+        Album album = createAlbum(dir, "/", _cache);
 
         File image = new File(getTestAlbumData(), "a.jpg");
         album.addImage("xyz", getImage(image));
         assertTrue(album.getEntry("/xyz") != null);
 
-        Album album2 = new FileSystemAlbum(dir, "/", new ForeverCache<String, ArrayList<PhotoEntry>>());
+        Album album2 = createAlbum(dir, "/", new ForeverCache<String, ArrayList<PhotoEntry>>());
         assertTrue(album2.getEntry("/xyz") != null);
     }
 
@@ -243,7 +244,7 @@ public class AlbumTest {
         assertTrue(new File(dir, FileSystemAlbum.PHOTOS_DIR).mkdir());
         assertTrue(new File(dir, FileSystemAlbum.THUMBNAILS_DIR).mkdir());
 
-        Album album = new FileSystemAlbum(dir, "/", _cache);
+        Album album = createAlbum(dir, "/", _cache);
 
         File image = new File(getTestAlbumData(), "a.jpg");
         album.addImage("xyz", getImage(image));
@@ -269,7 +270,7 @@ public class AlbumTest {
         assertTrue(new File(dir, FileSystemAlbum.PHOTOS_DIR).mkdir());
         assertTrue(new File(dir, FileSystemAlbum.THUMBNAILS_DIR).mkdir());
 
-        Album album = new FileSystemAlbum(dir, "/", _cache);
+        Album album = createAlbum(dir, "/", _cache);
 
         File sub = new File(dir, "sub");
         album.addAlbum("sub");
@@ -278,7 +279,7 @@ public class AlbumTest {
         assertTrue(new File(sub, FileSystemAlbum.PHOTOS_DIR).isDirectory());
         assertTrue(new File(sub, FileSystemAlbum.THUMBNAILS_DIR).isDirectory());
 
-        Album album2 = new FileSystemAlbum(dir, "/", new ForeverCache<String, ArrayList<PhotoEntry>>());
+        Album album2 = createAlbum(dir, "/", new ForeverCache<String, ArrayList<PhotoEntry>>());
         PhotoEntry entry = album2.getEntry("/sub");
 
         assertTrue(entry != null);
@@ -296,7 +297,7 @@ public class AlbumTest {
         assertTrue(new File(dir, FileSystemAlbum.PHOTOS_DIR).mkdir());
         assertTrue(new File(dir, FileSystemAlbum.THUMBNAILS_DIR).mkdir());
 
-        Album album = new FileSystemAlbum(dir, "/", _cache);
+        Album album = createAlbum(dir, "/", _cache);
 
         File sub = new File(dir, "sub");
         album.addAlbum("sub");
@@ -321,7 +322,7 @@ public class AlbumTest {
         File dir = getTestOutputDir();
         File origDir = new File(getTestAlbumData(), "AlbumRemove");
         copyDir(origDir);
-        Album album = new FileSystemAlbum(dir, "/", _cache);
+        Album album = createAlbum(dir, "/", _cache);
         try {
             album.removeEntry("Nested");
         }
diff --git a/src/test/java/org/wamblee/photos/model/AuthorizedAlbumTest.java b/src/test/java/org/wamblee/photos/model/AuthorizedAlbumTest.java
new file mode 100644 (file)
index 0000000..549bf13
--- /dev/null
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2005-2011 the original author or authors.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.wamblee.photos.model;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import javax.servlet.http.HttpSession;
+
+import org.wamblee.cache.Cache;
+import org.wamblee.cache.EhCache;
+import org.wamblee.io.ClassPathResource;
+import org.wamblee.photos.model.authorization.AuthorizedAlbum;
+import org.wamblee.security.authorization.AuthorizationService;
+import org.wamblee.security.authorization.Operation;
+import static org.mockito.Mockito.*;
+
+/**
+ * Created with IntelliJ IDEA.
+ * User: erik
+ * Date: 9/23/13
+ * Time: 9:08 PM
+ * To change this template use File | Settings | File Templates.
+ */
+public class AuthorizedAlbumTest extends AlbumTest {
+
+    @Override
+    protected Album createAlbum(File aDir, String aPath, Cache<String, ArrayList<PhotoEntry>> aCache)
+            throws IOException {
+        Album fileSystemAlbum = super.createAlbum(aDir, aPath,
+                aCache);    //To change body of overridden methods use File | Settings | File Templates.
+        AuthorizationService service = mock(AuthorizationService.class);
+        when(service.isAllowed(anyObject(), any(Operation.class))).thenReturn(true);
+        HttpSession session = mock(HttpSession.class);
+        when(session.getId()).thenReturn("myid");
+        Album authorized = new AuthorizedAlbum(fileSystemAlbum, service, createCache(), session);
+        return authorized;
+    }
+
+    @Override
+    protected Cache createCache() {
+        try {
+            return new EhCache(new ClassPathResource("META-INF/ehcache.xml"), "test");
+        }
+        catch (Exception e) {
+            throw new RuntimeException(e.getMessage(), e);
+        }
+    }
+}