just before adding authorization service.
authorErik Brakkee <erik@brakkee.org>
Sat, 14 Sep 2013 16:25:23 +0000 (18:25 +0200)
committerErik Brakkee <erik@brakkee.org>
Sat, 14 Sep 2013 16:25:23 +0000 (18:25 +0200)
50 files changed:
jaccmagic.txt [new file with mode: 0644]
pom.xml
src/main/java/META-INF/ehcache.xml [new file with mode: 0644]
src/main/java/META-INF/org.wamblee.photos.properties [new file with mode: 0644]
src/main/java/META-INF/persistence.xml [new file with mode: 0644]
src/main/java/META-INF/services/javax.enterprise.inject.spi.Extension [new file with mode: 0644]
src/main/java/org/wamblee/photos/concurrent/ConcurrentAlbum.java [new file with mode: 0644]
src/main/java/org/wamblee/photos/concurrent/ConcurrentPhoto.java [new file with mode: 0644]
src/main/java/org/wamblee/photos/concurrent/ConcurrentPhotoEntry.java [new file with mode: 0644]
src/main/java/org/wamblee/photos/model/Album.java [new file with mode: 0644]
src/main/java/org/wamblee/photos/model/Path.java [new file with mode: 0644]
src/main/java/org/wamblee/photos/model/Photo.java [new file with mode: 0644]
src/main/java/org/wamblee/photos/model/PhotoEntry.java [new file with mode: 0644]
src/main/java/org/wamblee/photos/model/authorization/AuthorizedAlbum.java [new file with mode: 0644]
src/main/java/org/wamblee/photos/model/authorization/AuthorizedPhoto.java [new file with mode: 0644]
src/main/java/org/wamblee/photos/model/authorization/AuthorizedPhotoEntry.java [new file with mode: 0644]
src/main/java/org/wamblee/photos/model/authorization/CreateAlbumOperation.java [new file with mode: 0644]
src/main/java/org/wamblee/photos/model/authorization/UploadPhotosOperation.java [new file with mode: 0644]
src/main/java/org/wamblee/photos/model/filesystem/EntryFoundCallback.java [new file with mode: 0644]
src/main/java/org/wamblee/photos/model/filesystem/FileSystemAlbum.java [new file with mode: 0644]
src/main/java/org/wamblee/photos/model/filesystem/FileSystemPhoto.java [new file with mode: 0644]
src/main/java/org/wamblee/photos/model/filesystem/FindByIdCallback.java [new file with mode: 0644]
src/main/java/org/wamblee/photos/model/plumbing/AllPhotos.java [new file with mode: 0644]
src/main/java/org/wamblee/photos/model/plumbing/AuthorizedPhotos.java [new file with mode: 0644]
src/main/java/org/wamblee/photos/model/plumbing/Configuration.java [new file with mode: 0644]
src/main/java/org/wamblee/photos/model/plumbing/Eager.java [new file with mode: 0644]
src/main/java/org/wamblee/photos/model/plumbing/EagerExtension.java [new file with mode: 0644]
src/main/java/org/wamblee/photos/model/plumbing/Initializer.java [new file with mode: 0644]
src/main/java/org/wamblee/photos/model/plumbing/PhotoApp.java [new file with mode: 0644]
src/main/java/org/wamblee/photos/model/plumbing/Producer.java [new file with mode: 0644]
src/main/java/org/wamblee/photos/security/AuthenticationFilter.java [new file with mode: 0644]
src/main/java/org/wamblee/photos/security/PhotoAuthorizationRule.java [new file with mode: 0644]
src/main/java/org/wamblee/photos/utils/JpegUtils.java [new file with mode: 0644]
src/main/java/org/wamblee/photos/wicket/BasePage.html [new file with mode: 0644]
src/main/java/org/wamblee/photos/wicket/BasePage.java [new file with mode: 0644]
src/main/java/org/wamblee/photos/wicket/ErrorPage.html [new file with mode: 0644]
src/main/java/org/wamblee/photos/wicket/ErrorPage.java [new file with mode: 0644]
src/main/java/org/wamblee/photos/wicket/HomePage.html [new file with mode: 0644]
src/main/java/org/wamblee/photos/wicket/HomePage.java [new file with mode: 0644]
src/main/java/org/wamblee/photos/wicket/WicketApplication.java [new file with mode: 0644]
src/main/java/org/wamblee/photos/wicket/banner.png [new file with mode: 0644]
src/main/java/org/wamblee/photos/wicket/photos-print.css [new file with mode: 0644]
src/main/java/org/wamblee/photos/wicket/photos.css [new file with mode: 0644]
src/main/java/org/wamblee/photos/wicket/wamblee_logo.png [new file with mode: 0644]
src/main/webapp/WEB-INF/beans.xml [new file with mode: 0644]
src/main/webapp/WEB-INF/web.xml [new file with mode: 0644]
src/main/webapp/login.jsp [new file with mode: 0644]
src/main/webapp/loginError.jsp [new file with mode: 0644]
src/main/webapp/logout.jsp [new file with mode: 0644]
todo.txt [new file with mode: 0644]

diff --git a/jaccmagic.txt b/jaccmagic.txt
new file mode 100644 (file)
index 0000000..decf99e
--- /dev/null
@@ -0,0 +1,33 @@
+  // https://blogs.oracle.com/monzillo/entry/using_jacc_to_determine_a
+
+        Subject subject = (Subject) PolicyContext
+            .getContext("javax.security.auth.Subject.container");
+        CodeSource cs = new CodeSource(null,
+            (java.security.cert.Certificate[]) null);
+        Principal principals[] = (subject == null ? new Principal[0] : subject
+            .getPrincipals().toArray(new Principal[0]));
+        for (Principal principal : principals) {
+            System.out
+                .println(principal + " " + principal.getClass().getName());
+        }
+
+        ProtectionDomain pd = new ProtectionDomain(cs, null, null, principals);
+        Policy policy = Policy.getPolicy();
+        PermissionCollection pc = policy.getPermissions(pd);
+        pc.implies(new WebRoleRefPermission(null, null));
+        Set<String> roleSet = new HashSet<String>();
+        Enumeration<Permission> e = pc.elements();
+        while (e.hasMoreElements()) {
+            Permission p = e.nextElement();
+            if (p instanceof WebRoleRefPermission) {
+                String roleRef = p.getActions();
+                // confirm roleRef via isUserInRole to ensure proper scoping to
+                // Servlet Name
+                // if (request.isUserInRole(roleRef)) {
+                roleSet.add(p.getActions());
+                System.out.println("Role actions: " + p.getActions());
+                System.out.println("Role name:    " + p.getName());
+                // }
+            }
+        }
+        System.out.println("Roles in the context of this page: " + roleSet);
diff --git a/pom.xml b/pom.xml
index 971a5a4c6899c761ff108aef7c6cac76c31f81a2..6a00ea2c25dc5e9a13654e34363bf0fc8ed8627a 100644 (file)
--- a/pom.xml
+++ b/pom.xml
@@ -1,2 +1,331 @@
-janse
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+       <modelVersion>4.0.0</modelVersion>
+       <groupId>org.wamblee.photos</groupId>
+       <artifactId>wamblee-photos</artifactId>
+       <packaging>war</packaging>
+       <version>0.1-SNAPSHOT</version>
+       <name>photos</name>
+       <description>
+         Photos application.
+       </description>
 
+       <properties>
+               <wicket.version>1.4.9</wicket.version>
+               <jetty.version>6.1.4</jetty.version>
+               <utils.version>0.6</utils.version>
+       </properties>
+
+       <!--
+               TODO <organization> <name>company name</name> <url>company url</url>
+               </organization>
+       -->
+       <url>http://wamblee.org</url>
+       <licenses>
+               <license>
+                       <name>The Apache Software License, Version 2.0</name>
+                       <url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
+                       <distribution>repo</distribution>
+               </license>
+       </licenses>
+       <scm>
+               <url>http://wamblee.org/gitweb/photos</url>
+               <connection>scm:git:https://wamblee.org/git/public/photos</connection>
+               <developerConnection>scm:git:https://wamblee.org/git/public/photos</developerConnection>
+       </scm>
+       <developers>
+               <developer>
+                       <email>erik@wamblee.org</email>
+                       <name>Erik Brakkee</name>
+                       <url>http://brakkee.org</url>
+               </developer>
+       </developers>
+
+       <dependencies>
+
+               <dependency>
+                       <groupId>javax.portlet</groupId>
+                       <artifactId>portlet-api</artifactId>
+                       <version>2.0</version>
+               </dependency>
+
+
+               <dependency>
+                       <groupId>dom4j</groupId>
+                       <artifactId>dom4j</artifactId>
+                       <version>1.6</version>
+                       <exclusions>
+                               <exclusion>
+                                       <groupId>xml-apis</groupId>
+                                       <artifactId>xml-apis</artifactId>
+                               </exclusion>
+                       </exclusions>
+               </dependency>
+               <dependency>
+                       <groupId>jaxen</groupId>
+                       <artifactId>jaxen</artifactId>
+                       <version>1.1-beta-9</version>
+                       <exclusions>
+                               <exclusion>
+                                       <groupId>xom</groupId>
+                                       <artifactId>xom</artifactId>
+                               </exclusion>
+                               <exclusion>
+                                       <groupId>xerces</groupId>
+                                       <artifactId>xmlParserAPIs</artifactId>
+                               </exclusion>
+                       </exclusions>
+               </dependency>
+
+
+
+               <dependency>
+                       <groupId>org.wamblee</groupId>
+                       <artifactId>wamblee-wicket-joe</artifactId>
+                       <version>${utils.version}</version>
+               </dependency>
+               <dependency>
+                       <groupId>org.wamblee</groupId>
+                       <artifactId>wamblee-wicket-components</artifactId>
+                       <version>${utils.version}</version>
+               </dependency>
+               <dependency>
+                       <groupId>org.wamblee</groupId>
+                       <artifactId>wamblee-support-cdi</artifactId>
+                       <version>${utils.version}</version>
+               </dependency>
+               <dependency>
+                       <groupId>org.wamblee</groupId>
+                       <artifactId>wamblee-support-cdi</artifactId>
+                       <type>test-jar</type>
+                       <scope>test</scope>
+                       <version>${utils.version}</version>
+               </dependency>
+               <dependency>
+                       <groupId>org.wamblee</groupId>
+                       <artifactId>wamblee-security-usermgt</artifactId>
+                       <version>${utils.version}</version>
+               </dependency>
+
+
+               <!--  WICKET DEPENDENCIES -->
+               <dependency>
+                       <groupId>org.apache.wicket</groupId>
+                       <artifactId>wicket</artifactId>
+                       <version>${wicket.version}</version>
+               </dependency>
+               <!--
+                       OPTIONAL <dependency> <groupId>org.apache.wicket</groupId>
+                       <artifactId>wicket-extensions</artifactId>
+                       <version>${wicket.version}</version> </dependency>
+               -->
+
+               <!-- LOGGING DEPENDENCIES - LOG4J -->
+               <dependency>
+                       <groupId>org.slf4j</groupId>
+                       <artifactId>slf4j-log4j12</artifactId>
+                       <version>1.4.2</version>
+               </dependency>
+               <dependency>
+                       <groupId>log4j</groupId>
+                       <artifactId>log4j</artifactId>
+                       <version>1.2.14</version>
+               </dependency>
+
+               <!--  JUNIT DEPENDENCY FOR TESTING -->
+               <dependency>
+                       <groupId>junit</groupId>
+                       <artifactId>junit</artifactId>
+                       <version>4.4</version>
+                       <scope>test</scope>
+               </dependency>
+               <dependency>
+                       <groupId>org.mockito</groupId>
+                       <artifactId>mockito-all</artifactId>
+                       <version>1.8.5</version>
+                       <scope>test</scope>
+               </dependency>
+
+
+               <!--  JETTY DEPENDENCIES FOR TESTING  -->
+               <dependency>
+                       <groupId>org.mortbay.jetty</groupId>
+                       <artifactId>jetty</artifactId>
+                       <version>${jetty.version}</version>
+                       <scope>provided</scope>
+               </dependency>
+               <dependency>
+                       <groupId>org.mortbay.jetty</groupId>
+                       <artifactId>jetty-util</artifactId>
+                       <version>${jetty.version}</version>
+                       <scope>provided</scope>
+               </dependency>
+               <dependency>
+                       <groupId>org.mortbay.jetty</groupId>
+                       <artifactId>jetty-management</artifactId>
+                       <version>${jetty.version}</version>
+                       <scope>provided</scope>
+               </dependency>
+
+               <!--  JPA testing -->
+               <dependency>
+                       <groupId>org.wamblee</groupId>
+                       <artifactId>wamblee-test-enterprise</artifactId>
+                       <version>${utils.version}</version>
+                       <scope>test</scope>
+               </dependency>
+               <dependency>
+                       <groupId>org.wamblee</groupId>
+                       <artifactId>wamblee-test-eclipselink</artifactId>
+                       <version>${utils.version}</version>
+                       <scope>test</scope>
+               </dependency>
+               <dependency>
+                       <groupId>org.eclipse.persistence</groupId>
+                       <artifactId>javax.persistence</artifactId>
+                       <version>2.0.0</version>
+                       <scope>provided</scope>
+               </dependency>
+
+               <dependency>
+                       <groupId>org.eclipse.persistence</groupId>
+                       <artifactId>eclipselink</artifactId>
+                       <version>2.0.2</version>
+                       <scope>test</scope>
+               </dependency>
+
+               <dependency>
+                       <groupId>javax</groupId>
+                       <artifactId>javaee-api</artifactId>
+                       <version>6.0</version>
+                       <scope>provided</scope>
+               </dependency>
+
+
+       </dependencies>
+       <build>
+               <resources>
+                       <resource>
+                               <filtering>false</filtering>
+                               <directory>src/main/resources</directory>
+                       </resource>
+                       <resource>
+                               <filtering>false</filtering>
+                               <directory>src/main/java</directory>
+                               <includes>
+                                       <include>**</include>
+                               </includes>
+                               <excludes>
+                                       <exclude>**/*.java</exclude>
+                               </excludes>
+                       </resource>
+               </resources>
+               <testResources>
+                       <testResource>
+                               <filtering>false</filtering>
+                               <directory>src/test/java</directory>
+                               <includes>
+                                       <include>**</include>
+                               </includes>
+                               <excludes>
+                                       <exclude>**/*.java</exclude>
+                               </excludes>
+                       </testResource>
+               </testResources>
+               <plugins>
+                       <plugin>
+                               <groupId>org.apache.maven.plugins</groupId>
+                               <artifactId>maven-war-plugin</artifactId>
+                               <version>2.0</version>
+                               <configuration> </configuration>
+                       </plugin>
+
+
+                       <plugin>
+                               <artifactId>maven-dependency-plugin</artifactId>
+                               <executions>
+                                       <execution>
+                                               <id>copy-dependencies</id>
+                                               <phase>package</phase>
+                                               <goals>
+                                                       <goal>copy-dependencies</goal>
+                                               </goals>
+                                       </execution>
+                               </executions>
+                       </plugin>
+
+                       <plugin>
+                               <inherited>true</inherited>
+                               <groupId>org.apache.maven.plugins</groupId>
+                               <artifactId>maven-compiler-plugin</artifactId>
+                               <configuration>
+                                       <source>1.6</source>
+                                       <target>1.6</target>
+                                       <optimize>true</optimize>
+                                       <debug>true</debug>
+                                        <compilerArgument>-XDignore.symbol.file</compilerArgument>
+                               </configuration>
+                       </plugin>
+                       <plugin>
+                               <groupId>org.mortbay.jetty</groupId>
+                               <artifactId>maven-jetty-plugin</artifactId>
+                       </plugin>
+
+               </plugins>
+       </build>
+
+
+       <repositories>
+               <repository>
+                       <id>EclipseLink Repo</id>
+                       <url>http://www.eclipse.org/downloads/download.php?r=1&amp;nf=1&amp;file=/rt/eclipselink/maven.repo</url>
+               </repository>
+       </repositories>
+
+       <profiles>
+               <profile>
+                       <id>release</id>
+                       <activation>
+                               <property>
+                                       <name>performRelease</name>
+                                       <value>true</value>
+                               </property>
+                       </activation>
+
+                       <build>
+                               <plugins>
+                                       <plugin>
+                                               <groupId>org.apache.maven.plugins</groupId>
+                                               <artifactId>maven-release-plugin</artifactId>
+                                               <version>2.0</version>
+                                               <configuration>
+                                                       <autoVersionSubmodules>true</autoVersionSubmodules>
+                                                       <goals>javadoc:jar deploy</goals>
+                                               </configuration>
+                                       </plugin>
+                                       <plugin>
+                                               <groupId>org.apache.maven.plugins</groupId>
+                                               <artifactId>maven-gpg-plugin</artifactId>
+                                               <executions>
+                                                       <execution>
+                                                               <id>sign-artifacts</id>
+                                                               <phase>verify</phase>
+                                                               <goals>
+                                                                       <goal>sign</goal>
+                                                               </goals>
+                                                       </execution>
+                                               </executions>
+                                       </plugin>
+                               </plugins>
+                       </build>
+               </profile>
+       </profiles>
+
+       <distributionManagement>
+               <repository>
+                       <id>sonatype-nexus-staging</id>
+                       <name>Nexus Release Repository</name>
+                       <url>http://oss.sonatype.org/service/local/staging/deploy/maven2/</url>
+               </repository>
+       </distributionManagement>
+
+</project>
diff --git a/src/main/java/META-INF/ehcache.xml b/src/main/java/META-INF/ehcache.xml
new file mode 100644 (file)
index 0000000..ee79950
--- /dev/null
@@ -0,0 +1,75 @@
+<ehcache>
+
+    <!-- Sets the path to the directory where cache .data files are created.
+
+         If the path is a Java System Property it is replaced by
+         its value in the running VM.
+
+         The following properties are translated:
+         user.home - User's home directory
+         user.dir - User's current working directory
+         java.io.tmpdir - Default temp file path -->
+    <diskStore path="java.io.tmpdir"/>
+
+
+    <!--Default Cache configuration. These will applied to caches programmatically created through
+        the CacheManager.
+
+        The following attributes are required:
+
+        maxElementsInMemory            - Sets the maximum number of objects that will be created in memory
+        eternal                        - Sets whether elements are eternal. If eternal,  timeouts are ignored and the
+                                         element is never expired.
+        overflowToDisk                 - Sets whether elements can overflow to disk when the in-memory cache
+                                         has reached the maxInMemory limit.
+
+        The following attributes are optional:
+        timeToIdleSeconds              - Sets the time to idle for an element before it expires.
+                                         i.e. The maximum amount of time between accesses before an element expires
+                                         Is only used if the element is not eternal.
+                                         Optional attribute. A value of 0 means that an Element can idle for infinity.
+                                         The default value is 0.
+        timeToLiveSeconds              - Sets the time to live for an element before it expires.
+                                         i.e. The maximum time between creation time and when an element expires.
+                                         Is only used if the element is not eternal.
+                                         Optional attribute. A value of 0 means that and Element can live for infinity.
+                                         The default value is 0.
+        diskPersistent                 - Whether the disk store persists between restarts of the Virtual Machine.
+                                         The default value is false.
+        diskExpiryThreadIntervalSeconds- The number of seconds between runs of the disk expiry thread. The default value
+                                         is 120 seconds.
+        -->
+
+    <defaultCache
+        maxElementsInMemory="10000"
+        eternal="false"
+        overflowToDisk="false"
+        timeToIdleSeconds="120"
+        timeToLiveSeconds="120"
+        diskPersistent="false"
+        diskExpiryThreadIntervalSeconds="120"
+        />
+        
+    <cache
+        name="users"
+        maxElementsInMemory="10000"
+        eternal="false"
+        overflowToDisk="false"
+        timeToIdleSeconds="120"
+        timeToLiveSeconds="120"
+        diskPersistent="false"
+        diskExpiryThreadIntervalSeconds="120"
+    />
+    
+    <cache
+        name="photos"
+        maxElementsInMemory="1000"
+        eternal="false"
+        overflowToDisk="false"
+        timeToIdleSeconds="120"
+        timeToLiveSeconds="120"
+        diskPersistent="false"
+        diskExpiryThreadIntervalSeconds="120"
+    />
+    
+</ehcache>
diff --git a/src/main/java/META-INF/org.wamblee.photos.properties b/src/main/java/META-INF/org.wamblee.photos.properties
new file mode 100644 (file)
index 0000000..75073f1
--- /dev/null
@@ -0,0 +1,6 @@
+
+##############################################################################
+# Full path name of the physical storage of the photo albums. 
+##############################################################################
+org.wamblee.photos.path=/home/erik/java/workspace/album
+
diff --git a/src/main/java/META-INF/persistence.xml b/src/main/java/META-INF/persistence.xml
new file mode 100644 (file)
index 0000000..0a4525e
--- /dev/null
@@ -0,0 +1,39 @@
+<persistence xmlns="http://java.sun.com/xml/ns/persistence"
+   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+   xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd"
+   version="2.0">
+   
+   <persistence-unit name="photos" transaction-type="JTA">
+      <jta-data-source>jdbc/PhotoXChange</jta-data-source>
+      
+      <!-- from the utiliities library --> 
+      <class>org.wamblee.security.authentication.User</class>
+      <class>org.wamblee.security.authentication.Group</class>
+      
+      <!--
+      <class>org.wamblee.security.authorization.AbstractUserCondition</class>
+      <class>org.wamblee.security.authorization.AnyUserCondition</class>
+      <class>org.wamblee.security.authorization.GroupUserCondition</class>
+      <class>org.wamblee.security.authorization.AbstractOperationCondition</class>
+      <class>org.wamblee.security.authorization.IsaOperationCondition</class>
+      <class>org.wamblee.security.authorization.AbstractPathCondition</class>
+      <class>org.wamblee.security.authorization.RegexpPathCondition</class>
+      <class>org.wamblee.security.authorization.StartsWithPathCondition</class>
+      
+      <class>org.wamblee.security.authorization.AbstractAuthorizationRule</class>
+      
+      
+      <class>org.wamblee.security.authorization.AbstractAuthorizationService</class>
+      <class>org.wamblee.security.authorization.DefaultAuthorizationService</class>
+      -->
+      
+      <!-- from the photo app, subclass of UrlAuthorizationRule --> 
+    
+      <!--
+      <class>org.wamblee.photos.authorizationrules.PhotoAuthorizationRule</class>
+      -->
+      
+      <exclude-unlisted-classes>true</exclude-unlisted-classes>
+    
+   </persistence-unit>
+</persistence>
\ No newline at end of file
diff --git a/src/main/java/META-INF/services/javax.enterprise.inject.spi.Extension b/src/main/java/META-INF/services/javax.enterprise.inject.spi.Extension
new file mode 100644 (file)
index 0000000..ee6fcf8
--- /dev/null
@@ -0,0 +1 @@
+org.wamblee.photos.model.plumbing.EagerExtension
diff --git a/src/main/java/org/wamblee/photos/concurrent/ConcurrentAlbum.java b/src/main/java/org/wamblee/photos/concurrent/ConcurrentAlbum.java
new file mode 100644 (file)
index 0000000..47520a2
--- /dev/null
@@ -0,0 +1,159 @@
+/*
+ * 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.concurrent;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import org.wamblee.photos.model.Album;
+import org.wamblee.photos.model.Path;
+import org.wamblee.photos.model.Photo;
+import org.wamblee.photos.model.PhotoEntry;
+
+/**
+ * Decorator for an album providing defined behavior when used in a concurrent setting. 
+ */
+public class ConcurrentAlbum extends ConcurrentPhotoEntry implements Album {
+    
+    /**
+     * Constructs concurrent album as a decorator for an album implementation. 
+     * @param aAlbum Album to decorate. 
+     */
+    public ConcurrentAlbum(Album aAlbum) {
+        super(aAlbum); 
+    }
+    
+    private Album decorated() {
+        return (Album)getEntry(); 
+    }
+    
+    /**
+     * Creates a decorate for the photo entry to make it safe for concurrent access.  
+     * @param aEntry Entry to decorate
+     * @return Decorated photo. 
+     */
+    private <T extends PhotoEntry> T decorate(T aEntry) {
+        if ( aEntry == null ) {
+            return null;    
+        }
+        else if ( aEntry instanceof Photo) {
+            return (T) new ConcurrentPhoto((Photo)aEntry); 
+        } else if ( aEntry instanceof Album) {
+            return (T) new ConcurrentAlbum((Album)aEntry); 
+        } else {
+            throw new IllegalArgumentException("Entry is neither a photo nor an album: " + aEntry); 
+        }
+    }
+
+    /* (non-Javadoc)
+     * @see org.wamblee.photos.model.Album#getEntry(java.lang.String)
+     */
+    public PhotoEntry getEntry(String aPath) {
+        lock().acquireRead(); 
+        try {
+            return decorate(decorated().getEntry(aPath));    
+        } finally {
+            lock().releaseRead(); 
+        }
+    }
+    
+    public PhotoEntry getEntry(Path aPath) {
+        lock().acquireRead(); 
+         try {
+             return decorate(decorated().getEntry(aPath));    
+         } finally {
+             lock().releaseRead(); 
+         }
+    }
+
+    /* (non-Javadoc)
+     * @see org.wamblee.photos.model.Album#getEntry(int)
+     */
+    public PhotoEntry getEntry(int aIndex) {
+        lock().acquireRead(); 
+        try {
+            return decorate(decorated().getEntry(aIndex)); 
+        } finally {
+            lock().releaseRead(); 
+        }
+    }
+
+    /* (non-Javadoc)
+     * @see org.wamblee.photos.model.Album#size()
+     */
+    public int size() {
+        lock().acquireRead(); 
+        try {
+            return decorated().size(); 
+        } finally {
+            lock().releaseRead(); 
+        }
+    }
+
+    /* (non-Javadoc)
+     * @see org.wamblee.photos.model.Album#addImage(java.lang.String, java.io.InputStream)
+     */
+    public void addImage(String aId, InputStream aImage) throws IOException {
+        lock().acquireWrite();
+        try {
+            decorated().addImage(aId, aImage); 
+        } finally {
+            lock().releaseWrite(); 
+        }
+    }
+
+    /* (non-Javadoc)
+     * @see org.wamblee.photos.model.Album#addAlbum(java.lang.String)
+     */
+    public void addAlbum(String aId) throws IOException {
+        lock().acquireWrite();
+        try {
+            decorated().addAlbum(aId);  
+        } finally {
+            lock().releaseWrite(); 
+        }
+    }
+
+    /* (non-Javadoc)
+     * @see org.wamblee.photos.model.Album#removeEntry(java.lang.String)
+     */
+    public void removeEntry(String aId) throws IOException {
+        lock().acquireWrite();
+        try {
+            decorated().removeEntry(aId);  
+        } finally {
+            lock().releaseWrite(); 
+        }
+    }
+    
+    public Photo findPhotoBefore(String aId) {
+        lock().acquireWrite();
+         try {
+             return decorate(decorated().findPhotoBefore(aId));  
+         } finally {
+             lock().releaseWrite(); 
+         }
+    }
+    
+    public Photo findPhotoAfter(String aId) {
+        lock().acquireWrite();
+         try {
+             return decorate(decorated().findPhotoAfter(aId));  
+         } finally {
+             lock().releaseWrite(); 
+         }
+    }
+}
diff --git a/src/main/java/org/wamblee/photos/concurrent/ConcurrentPhoto.java b/src/main/java/org/wamblee/photos/concurrent/ConcurrentPhoto.java
new file mode 100644 (file)
index 0000000..bf2357b
--- /dev/null
@@ -0,0 +1,63 @@
+/*
+ * 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.concurrent;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import org.wamblee.photos.model.Photo;
+
+/**
+ * Decorator for a photo to make it thread-safe.
+ */
+public class ConcurrentPhoto extends ConcurrentPhotoEntry implements Photo {
+
+    public ConcurrentPhoto(Photo aPhoto) {
+        super(aPhoto);
+    }
+
+    private Photo decorated() {
+        return (Photo)getEntry();
+    }
+
+    /*
+     * (non-Javadoc)
+     * 
+     * @see org.wamblee.photos.model.Photo#getThumbNail()
+     */
+    public InputStream getThumbNail() throws IOException {
+        lock().acquireRead();
+        try {
+            return decorated().getThumbNail();
+        } finally {
+            lock().releaseRead();
+        }
+    }
+
+    /*
+     * (non-Javadoc)
+     * 
+     * @see org.wamblee.photos.model.Photo#getPhoto()
+     */
+    public InputStream getPhoto() throws IOException {
+        lock().acquireRead();
+        try {
+            return decorated().getPhoto();
+        } finally {
+            lock().releaseRead();
+        }
+    }
+}
diff --git a/src/main/java/org/wamblee/photos/concurrent/ConcurrentPhotoEntry.java b/src/main/java/org/wamblee/photos/concurrent/ConcurrentPhotoEntry.java
new file mode 100644 (file)
index 0000000..0691a76
--- /dev/null
@@ -0,0 +1,86 @@
+/*
+ * 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.concurrent;
+
+import org.wamblee.concurrency.ReadWriteLock;
+import org.wamblee.photos.model.PhotoEntry;
+
+/**
+ * Decorator base class for a photo entry. 
+ */
+public class ConcurrentPhotoEntry implements PhotoEntry {
+
+    private PhotoEntry _entry;
+
+    private ReadWriteLock _lock;
+
+    protected ConcurrentPhotoEntry(PhotoEntry aEntry) {
+        _entry = aEntry;
+        _lock = new ReadWriteLock();
+    }
+    
+    protected PhotoEntry getEntry() {
+        return _entry; 
+    }
+    
+    protected ReadWriteLock lock() {
+        return _lock; 
+    }
+
+    /*
+     * (non-Javadoc)
+     * 
+     * @see org.wamblee.photos.model.PhotoEntry#getId()
+     */
+    public String getId() {
+        _lock.acquireRead();
+        try {
+            return _entry.getId();
+        } finally {
+            _lock.releaseRead();
+        }
+    }
+
+    /*
+     * (non-Javadoc)
+     * 
+     * @see org.wamblee.photos.model.PhotoEntry#getPath()
+     */
+    public String getPath() {
+        _lock.acquireRead(); 
+        try {
+            return _entry.getPath(); 
+        } finally {
+            _lock.releaseRead(); 
+        }
+    }
+
+    /*
+     * (non-Javadoc)
+     * 
+     * @see java.lang.Comparable#compareTo(java.lang.Object)
+     */
+    public int compareTo(PhotoEntry aEntry) {
+        _lock.acquireRead();
+        try {
+            return _entry.compareTo(aEntry); // TODO: is this safe? 
+        }
+        finally {
+            _lock.releaseRead(); 
+        }
+    }
+
+}
diff --git a/src/main/java/org/wamblee/photos/model/Album.java b/src/main/java/org/wamblee/photos/model/Album.java
new file mode 100644 (file)
index 0000000..126f161
--- /dev/null
@@ -0,0 +1,100 @@
+/*
+ * 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.model;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+
+/**
+ * Represents a collection of photo albums for one speci 
+ */
+public interface Album extends PhotoEntry {
+
+       /**
+        * Returns a photo entry with the given path.
+        * A path consists of a number of ids of photo entries 
+        * separated by a forward slash. The path should wtart with
+        * a forward slash. The path is relative to the current 
+        * album. 
+        * @param aPath Photo entry path. 
+        * @return PhotoEntry if found or null otherwise.  
+        */
+       PhotoEntry getEntry(String aPath);
+       
+       /**
+        * Returns a photo entry with the given path.
+        * A path consists of a number of ids of photo entries 
+        * separated by a forward slash. The path should wtart with
+        * a forward slash. The path is relative to the current 
+        * album. 
+        * @param aPath Photo entry path. 
+        * @return PhotoEntry if found or null otherwise.  
+        */
+       PhotoEntry getEntry(Path aPath);
+       
+       
+       /**
+        * Returns the photo entry with the given index. 
+        * 0 <= index < size()
+        * @param aIndex Index of the photo. 
+        * @return Photo at given index. 
+        */
+       PhotoEntry getEntry(int aIndex); 
+       
+       /**
+        * Returns the number of entries in the album. 
+        * @return Number of entries. 
+        */
+       int size(); 
+       
+       /**
+        * Adds an image to the album with the given id. 
+        * @param aId Id of the image (excluding file extension). 
+        * @param aImage Image.
+        * @throws IOException In case the image cannot be added.  
+        */
+       void addImage(String aId, InputStream aImage) throws IOException;
+       
+       /**
+        * Adds a new album if this is allowed and no album with the
+        * given 
+        * @param aId Album id. 
+        * @throws IOException In case the album cannot be added. 
+        */
+       void addAlbum(String aId) throws IOException;
+       
+       /**
+        * Removes the given entry if it can be removed. 
+        * @param aId Entry id. 
+        * @throws IOException In case the entry cannot be removed. 
+        */
+       void removeEntry(String aId) throws IOException;
+       
+       /**
+        * Gets the first photo before the entry with the given id.  
+        * @param aId Id of the given entry.   
+        * @return Photo or null if no such photo exists. 
+        */
+       Photo findPhotoBefore(String aId); 
+       
+       /**
+        * Gets the first photo after a given entry. 
+        * @param aId Id of the given entry.  
+        * @return Photo or null if no such photo exists. 
+        */
+       Photo findPhotoAfter(String aId);
+}
diff --git a/src/main/java/org/wamblee/photos/model/Path.java b/src/main/java/org/wamblee/photos/model/Path.java
new file mode 100644 (file)
index 0000000..974e47a
--- /dev/null
@@ -0,0 +1,161 @@
+/*
+ * 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.model;
+
+import java.io.Serializable;
+
+/**
+ * Represents a path to a photo. 
+ *
+ */
+public class Path implements Serializable {
+
+       static final long serialVersionUID = 1287597039089552731L;
+       
+    /**
+     * The parts of the path.
+     */
+       private String[] _path; 
+       
+    /**
+     * Constructs an empty path. 
+     *
+     */
+       public Path() {
+               _path = new String[0];
+       }
+
+    /**
+     * Construcst the path based on a path string. 
+     * @param aPath Path, must start with a / 
+     */
+       public Path(String aPath) {
+               if ( aPath == null ) {
+                       throw new IllegalArgumentException("path is null"); 
+               }
+               if ( !aPath.startsWith("/")) {
+                       throw new IllegalArgumentException("path must start with / '" + aPath + "'");
+               }
+               if ( aPath.length() == 1 ) { 
+                       _path = new String[0];
+                       return;
+               }
+               _path = aPath.substring(1).split("/");
+       }
+       
+    /**
+     * Constructs the path based on an array of the parts. 
+     * @param aPath Path. 
+     */
+       private Path(String[] aPath) {
+               _path = aPath; 
+       }
+       
+    /**
+     * Returns the child of a path by appending a part. 
+     * @param aId Id to append.  
+     * @return Child. 
+     * @throws IllegalArgumentException In case the id contains a / 
+     */
+       public Path child(String aId) {
+        if ( aId.matches("/")) { 
+            throw new IllegalArgumentException("Id '" + aId + "' contains a /");
+        }
+           String[] newpath = new String[_path.length + 1];
+           System.arraycopy(_path, 0, newpath, 0, _path.length); 
+           newpath[_path.length] = aId; 
+           return new Path(newpath);
+       }
+       
+    /**
+     * Checks if the path is the root path. 
+     * @return True if the path is the root. 
+     */
+       public boolean isRoot() {
+               return _path.length == 0; 
+       }
+       
+    /**
+     * Gets the parent of the path. 
+     * @return Parent of the path or the root if the path is already at the root. 
+     */
+       public Path parent() {
+               if ( isRoot() ) {
+                       return this;
+               }
+               String[] newpath = new String[_path.length -1];
+               System.arraycopy(_path, 0, newpath, 0, _path.length - 1);
+               return new Path(newpath);
+       }
+       
+    /**
+     * Converts the path to a regular path.
+     * @return A path, always starting with a /
+     */
+       public String toString() {
+               if ( _path.length == 0 ) {
+                       return "/";
+               }
+               String result = "";
+               for (String part: _path) {
+                       result += "/" + part; 
+               }
+        return result; 
+       }
+
+    /**
+     * Gets the id (last part) of the path. 
+     * @return Id or null if the path has no parts (root)
+     */
+       public String getId() {
+               if ( _path.length == 0 ) {
+                       return null; 
+               }
+               return _path[_path.length -1];
+       }
+
+    /**
+     * Returns the number of components in the path. 
+     * @return Size. 
+     */
+    public int size() { 
+        return _path.length; 
+    }
+   
+    /**
+     * Returns a part of the path. 
+     * @param aPart Part number (starts at 0).
+     * @return Part. 
+     */
+    public String getPart(int aPart) { 
+        return _path[aPart];
+    }
+    
+    /**
+     * Strips the first part of the path and returns the remainder.
+     * @return Remainder.
+     */
+    public Path remainder() { 
+        if ( _path.length == 0 ) { 
+            return null; 
+        }
+        String[] result = new String[_path.length-1];
+        for (int i = 0; i < _path.length-1; i++) { 
+            result[i] = _path[i+1]; 
+        }
+        return new Path(result);
+    }
+}
diff --git a/src/main/java/org/wamblee/photos/model/Photo.java b/src/main/java/org/wamblee/photos/model/Photo.java
new file mode 100644 (file)
index 0000000..8405c23
--- /dev/null
@@ -0,0 +1,39 @@
+/*
+ * 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.model;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Represents a single photo. 
+ */
+public interface Photo extends PhotoEntry {
+
+       /**
+        * Returns an opened input stream to the thumbnail JPEG
+        * picture for the given photo. 
+        * @return Input stream. 
+        */
+    InputStream getThumbNail() throws IOException; 
+    
+    /**
+     * Returns an opened input stream to the full JPEG 
+     * picture. 
+     * @return Input stream. 
+     */
+    InputStream getPhoto() throws IOException;
+}
diff --git a/src/main/java/org/wamblee/photos/model/PhotoEntry.java b/src/main/java/org/wamblee/photos/model/PhotoEntry.java
new file mode 100644 (file)
index 0000000..b360eaf
--- /dev/null
@@ -0,0 +1,34 @@
+/*
+ * 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.model;
+
+/**
+ * Represents an entry inside a photo album. 
+ */
+public interface PhotoEntry extends Comparable<PhotoEntry> {
+       
+       /**
+        * Photo entry id. 
+        * @return Id of the entry. 
+        */
+       String getId(); 
+       
+       /**
+        * Gets the path of the given photo with respect to the 
+        * root album. The path will start with a "/".  
+        */
+       String getPath(); 
+}
diff --git a/src/main/java/org/wamblee/photos/model/authorization/AuthorizedAlbum.java b/src/main/java/org/wamblee/photos/model/authorization/AuthorizedAlbum.java
new file mode 100644 (file)
index 0000000..7406d99
--- /dev/null
@@ -0,0 +1,233 @@
+/*
+ * 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.model.authorization;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.log4j.Logger;
+import org.wamblee.cache.Cache;
+import org.wamblee.cache.CachedObject;
+import org.wamblee.photos.model.Album;
+import org.wamblee.photos.model.Path;
+import org.wamblee.photos.model.Photo;
+import org.wamblee.photos.model.PhotoEntry;
+import org.wamblee.security.authorization.AllOperation;
+import org.wamblee.security.authorization.AuthorizationService;
+import org.wamblee.security.authorization.DeleteOperation;
+import org.wamblee.security.authorization.ReadOperation;
+import org.wamblee.security.authorization.WriteOperation;
+
+/**
+ * Decorator for an album providing defined behavior when used in a concurrent
+ * setting.
+ */
+public class AuthorizedAlbum extends AuthorizedPhotoEntry implements Album {
+
+    private static final Logger LOGGER = Logger
+            .getLogger(AuthorizedAlbum.class);
+
+    private AuthorizationService _authorizer;
+
+    private CachedObject<String, ArrayList<PhotoEntry>> _authorizedEntries;
+
+    private String _sessionId;
+
+    /**
+     * Constructs concurrent album as a decorator for an album implementation.
+     * 
+     * @param aAlbum
+     *            Album to decorate.
+     */
+    public AuthorizedAlbum(Album aAlbum, AuthorizationService aService,
+            Cache aCache, String aSessionId) {
+        super(aAlbum);
+        _authorizer = aService;
+        _authorizedEntries = new CachedObject<String, ArrayList<PhotoEntry>>(
+                aCache, aSessionId + "/" + aAlbum.getPath(),
+                new CachedObject.Computation<String, ArrayList<PhotoEntry>>() {
+                    public ArrayList<PhotoEntry> getObject(String aObjectKey) {
+                        return AuthorizedAlbum.this.compute();
+                    }
+                });
+        _sessionId = aSessionId;
+    }
+
+    /**
+     * 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() {
+        LOGGER.info("Refreshing cache " + getPath());
+        ArrayList<PhotoEntry> result = new ArrayList<PhotoEntry>();
+        for (int i = 0; i < decorated().size(); i++) {
+            PhotoEntry entry = decorated().getEntry(i);
+            if (_authorizer.isAllowed(entry, new ReadOperation())) {
+                result.add(decorate(entry)); // subscription will take place
+                // automatically.
+            }
+        }
+        return result;
+    }
+
+    private Album decorated() {
+        return (Album) getEntry();
+    }
+
+    /**
+     * Creates a decorate for the photo entry to make it safe for concurrent
+     * access.
+     * 
+     * @param aEntry
+     *            Entry to decorate
+     * @return Decorated photo.
+     */
+    private <T extends PhotoEntry> T decorate(T aEntry) {
+        if (aEntry == null) {
+            return null;
+        } 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(), _sessionId);
+        } else {
+            throw new IllegalArgumentException(
+                    "Entry is neither a photo nor an album: " + aEntry);
+        }
+    }
+
+    /*
+     * (non-Javadoc)
+     * 
+     * @see org.wamblee.photos.model.Album#getEntry(java.lang.String)
+     */
+    public PhotoEntry getEntry(String aPath) {
+        return getEntry(new Path(aPath));
+    }
+
+    /*
+     * (non-Javadoc)
+     * 
+     * @see org.wamblee.photos.model.Album#getEntry(org.wamblee.photos.model.Path)
+     */
+    public PhotoEntry getEntry(Path aPath) {
+        if (aPath.isRoot()) {
+            return this;
+        }
+        List<PhotoEntry> cache = _authorizedEntries.get();
+        String id = aPath.getPart(0);
+        Path remainder = aPath.remainder();
+        for (PhotoEntry entry : cache) {
+            if (entry.getId().equals(id)) {
+                if (remainder.isRoot()) {
+                    return entry;
+                } else {
+                    if (!(entry instanceof Album)) {
+                        throw new IllegalArgumentException(getPath() + " "
+                                + aPath);
+                    }
+                    return ((Album) entry).getEntry(remainder);
+                }
+            }
+        }
+        return null;
+    }
+
+    /*
+     * (non-Javadoc)
+     * 
+     * @see org.wamblee.photos.model.Album#getEntry(int)
+     */
+    public PhotoEntry getEntry(int aIndex) {
+        return _authorizedEntries.get().get(aIndex);
+    }
+
+    /*
+     * (non-Javadoc)
+     * 
+     * @see org.wamblee.photos.model.Album#size()
+     */
+    public int size() {
+        return _authorizedEntries.get().size();
+    }
+
+    /*
+     * (non-Javadoc)
+     * 
+     * @see org.wamblee.photos.model.Album#addImage(java.lang.String,
+     *      java.io.InputStream)
+     */
+    public void addImage(String aId, InputStream aImage) throws IOException {
+        _authorizer.check(this, new WriteOperation());
+        _authorizedEntries.invalidate();
+        decorated().addImage(aId, aImage);
+    }
+
+    /*
+     * (non-Javadoc)
+     * 
+     * @see org.wamblee.photos.model.Album#addAlbum(java.lang.String)
+     */
+    public void addAlbum(String aId) throws IOException {
+        _authorizer.check(this, new WriteOperation());
+        _authorizedEntries.invalidate();
+        decorated().addAlbum(aId);
+    }
+
+    /*
+     * (non-Javadoc)
+     * 
+     * @see org.wamblee.photos.model.Album#removeEntry(java.lang.String)
+     */
+    public void removeEntry(String aId) throws IOException {
+        // Check whether deletion is allowed.
+        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())) {
+            entry = decorated().findPhotoBefore(entry.getId());
+        }
+        return decorate(entry);
+    }
+
+    public Photo findPhotoAfter(String aId) {
+        Photo entry = decorated().findPhotoAfter(aId);
+        while (entry != null
+                && !_authorizer.isAllowed(entry, new AllOperation())) {
+            entry = decorated().findPhotoAfter(entry.getId());
+        }
+        return decorate(entry);
+    }
+
+    /*
+     * (non-Javadoc)
+     * 
+     * @see java.lang.Object#toString()
+     */
+    @Override
+    public String toString() {
+        return "AuthorizedAlbum(path = '" + decorated().getPath() + "')";
+    }
+}
diff --git a/src/main/java/org/wamblee/photos/model/authorization/AuthorizedPhoto.java b/src/main/java/org/wamblee/photos/model/authorization/AuthorizedPhoto.java
new file mode 100644 (file)
index 0000000..cda649d
--- /dev/null
@@ -0,0 +1,53 @@
+/*
+ * 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.model.authorization;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import org.wamblee.photos.model.Photo;
+
+/**
+ * Decorator for a photo to make it thread-safe.
+ */
+public class AuthorizedPhoto extends AuthorizedPhotoEntry implements Photo {
+
+    public AuthorizedPhoto(Photo aPhoto) {
+        super(aPhoto);
+    }
+
+    private Photo decorated() {
+        return (Photo) getEntry();
+    }
+
+    /*
+     * (non-Javadoc)
+     * 
+     * @see org.wamblee.photos.model.Photo#getThumbNail()
+     */
+    public InputStream getThumbNail() throws IOException {
+        return decorated().getThumbNail();
+    }
+
+    /*
+     * (non-Javadoc)
+     * 
+     * @see org.wamblee.photos.model.Photo#getPhoto()
+     */
+    public InputStream getPhoto() throws IOException {
+        return decorated().getPhoto();
+    }
+}
diff --git a/src/main/java/org/wamblee/photos/model/authorization/AuthorizedPhotoEntry.java b/src/main/java/org/wamblee/photos/model/authorization/AuthorizedPhotoEntry.java
new file mode 100644 (file)
index 0000000..e260b44
--- /dev/null
@@ -0,0 +1,62 @@
+/*
+ * 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.model.authorization;
+
+import org.wamblee.photos.model.PhotoEntry;
+
+/**
+ * Decorator base class for a photo entry.
+ */
+public class AuthorizedPhotoEntry implements PhotoEntry {
+
+    private PhotoEntry _entry;
+
+    protected AuthorizedPhotoEntry(PhotoEntry aEntry) {
+        _entry = aEntry;
+    }
+
+    protected PhotoEntry getEntry() {
+        return _entry;
+    }
+
+    /*
+     * (non-Javadoc)
+     * 
+     * @see org.wamblee.photos.model.PhotoEntry#getId()
+     */
+    public String getId() {
+        return _entry.getId();
+    }
+
+    /*
+     * (non-Javadoc)
+     * 
+     * @see org.wamblee.photos.model.PhotoEntry#getPath()
+     */
+    public String getPath() {
+        return _entry.getPath();
+    }
+
+    /*
+     * (non-Javadoc)
+     * 
+     * @see java.lang.Comparable#compareTo(java.lang.Object)
+     */
+    public int compareTo(PhotoEntry aEntry) {
+        return _entry.compareTo(aEntry); // TODO: is this safe?
+    }
+
+}
diff --git a/src/main/java/org/wamblee/photos/model/authorization/CreateAlbumOperation.java b/src/main/java/org/wamblee/photos/model/authorization/CreateAlbumOperation.java
new file mode 100644 (file)
index 0000000..75fb9f9
--- /dev/null
@@ -0,0 +1,44 @@
+/*
+ * 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.model.authorization;
+
+import org.wamblee.security.authorization.Operation;
+
+/**
+ * Operation representing creation of an album. 
+ */
+public class CreateAlbumOperation implements Operation {
+    
+    
+    private static final String OPERATION = "createAlbum";
+    
+    /**
+     * Constructs the operation. 
+     *
+     */
+    public CreateAlbumOperation() { 
+        // Empty
+    }
+
+    /* (non-Javadoc)
+     * @see org.wamblee.security.authorization.Operation#getName()
+     */
+    public String getName() {
+        return OPERATION; 
+    }
+
+}
diff --git a/src/main/java/org/wamblee/photos/model/authorization/UploadPhotosOperation.java b/src/main/java/org/wamblee/photos/model/authorization/UploadPhotosOperation.java
new file mode 100644 (file)
index 0000000..5d8907d
--- /dev/null
@@ -0,0 +1,42 @@
+/*
+ * 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.model.authorization;
+
+import org.wamblee.security.authorization.Operation;
+
+/**
+ * Operation representing upload of photos.  
+ */
+public class UploadPhotosOperation implements Operation {
+    
+    private static final String OPERATION = "uploadPhotos"; 
+
+    /**
+     * Constructs the operation. 
+     */
+    public UploadPhotosOperation() { 
+        // Empty. 
+    }
+    
+    /* (non-Javadoc)
+     * @see org.wamblee.security.authorization.Operation#getName()
+     */
+    public String getName() {
+        return OPERATION;
+    }
+
+}
diff --git a/src/main/java/org/wamblee/photos/model/filesystem/EntryFoundCallback.java b/src/main/java/org/wamblee/photos/model/filesystem/EntryFoundCallback.java
new file mode 100644 (file)
index 0000000..6669f32
--- /dev/null
@@ -0,0 +1,47 @@
+/*
+ * 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.model.filesystem;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+
+import org.wamblee.photos.model.PhotoEntry;
+
+/**
+ * Callback for the traverse method call of the filesystem album. 
+ */
+public interface EntryFoundCallback {
+    /**
+     * Callback executed when a photo entry has been found.
+     * @param aEntries Current list of photo entries.  
+     * @param aThumbnail Path of the thumbnail image. 
+     * @param aPhoto Path of the full-size image. 
+     * @param aPath Path in the photo album. 
+     * @return True if the search should continue, false otherwise. 
+     */
+       boolean photoFound(List<PhotoEntry> aEntries, File aThumbnail, File aPhoto, String aPath);
+    
+    /**
+     * Callback executed when a photo album has been found.
+     * @param aEntries Current list of photo entries.  
+     * @param aAlbum Path of the photo album directory. 
+     * @param aPath Path in the photo album. 
+     * @return True if the search should continue, false otherwise. 
+     * @throws IOException In case of IO problems. 
+     */
+       boolean albumFound(List<PhotoEntry> aEntries, File aAlbum, String aPath) throws IOException;
+}
diff --git a/src/main/java/org/wamblee/photos/model/filesystem/FileSystemAlbum.java b/src/main/java/org/wamblee/photos/model/filesystem/FileSystemAlbum.java
new file mode 100644 (file)
index 0000000..68a8faf
--- /dev/null
@@ -0,0 +1,731 @@
+/*
+ * 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.model.filesystem;
+
+import java.awt.image.BufferedImage;
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.wamblee.cache.Cache;
+import org.wamblee.cache.CachedObject;
+import org.wamblee.general.Pair;
+import org.wamblee.photos.model.Album;
+import org.wamblee.photos.model.Path;
+import org.wamblee.photos.model.Photo;
+import org.wamblee.photos.model.PhotoEntry;
+import org.wamblee.photos.utils.JpegUtils;
+
+/**
+ * Represents a photo album stored in a directory structure on the file system.
+ */
+public class FileSystemAlbum implements Album {
+
+    private static final Log LOG = LogFactory.getLog(FileSystemAlbum.class);
+
+    /**
+     * Subdirectory where the thumbnails are stored.
+     */
+    public static final String THUMBNAILS_DIR = "thumbnails";
+
+    /**
+     * Subdirectory where the photos are stored in their full size.
+     */
+    public static final String PHOTOS_DIR = "fotos";
+
+    /**
+     * Extension used for JPEG pictures.
+     */
+    private static final String JPG_EXTENSION = ".jpg";
+
+    /**
+     * Last part of the file name that a thumbnail must end with.
+     */
+    private static final String THUMBNAIL_ENDING = "_thumb.jpg";
+
+    private static final int THUMBNAIL_WIDTH = 100;
+
+    private static final int THUMBNAIL_HEIGHT = 100;
+
+    private static final int JPG_QUALITY = 75;
+
+    /**
+     * Array of photo entries.
+     */
+    private CachedObject<String, ArrayList<PhotoEntry>> _entries;
+
+    /**
+     * Storage directory for this album.
+     */
+    private File _dir;
+
+    /**
+     * Relative path with respect to the root album.
+     */
+    private String _path;
+
+    private class CreateEntryCallback implements EntryFoundCallback {
+        public boolean photoFound(List<PhotoEntry> aEntries, File aThumbnail,
+            File aPhoto, String aPath) {
+            PhotoEntry entry = new FileSystemPhoto(aThumbnail, aPhoto, aPath);
+            aEntries.add(entry);
+            return true;
+        }
+
+        public boolean albumFound(List<PhotoEntry> aEntries, File aAlbum,
+            String aPath) throws IOException {
+            PhotoEntry entry = new FileSystemAlbum(aAlbum, aPath,
+                _entries.getCache());
+            aEntries.add(entry);
+            return true;
+        }
+    }
+
+    /**
+     * Creates the album.
+     * 
+     * @param aDir
+     *            Directory where the album is located.
+     * @param aPath
+     *            Path that this album represents.
+     * @param aCache
+     *            Cache to use.
+     * @throws IOException
+     */
+    public FileSystemAlbum(File aDir, String aPath,
+        Cache<String, ArrayList<PhotoEntry>> aCache) throws IOException {
+        if (!aDir.isDirectory()) {
+            throw new IOException("Directory '" + aDir.getPath() +
+                "' does not exist.");
+        }
+        _dir = aDir;
+        _path = aPath;
+        _entries = new CachedObject<String, ArrayList<PhotoEntry>>(aCache,
+            aPath,
+            new CachedObject.Computation<String, ArrayList<PhotoEntry>>() {
+                public ArrayList<PhotoEntry> getObject(String aObjectKey) {
+                    return FileSystemAlbum.this.compute();
+                }
+            });
+
+    }
+
+    /**
+     * Computes the photo entries for this album based on the file system.
+     * 
+     * @return List of photo entries.
+     */
+    private ArrayList<PhotoEntry> compute() {
+        ArrayList<PhotoEntry> result = new ArrayList<PhotoEntry>();
+        try {
+            LOG.info("Initializing album for directory " + _dir);
+            traverse(getPath(), result, _dir, new CreateEntryCallback());
+        } catch (IOException e) {
+            LOG.fatal("IOException occurred: " + e.getMessage(), e);
+        }
+        return result;
+    }
+
+    /**
+     * Initializes the album.
+     * 
+     * @param aPath
+     *            Path of the album
+     * @param aEntries
+     *            Photo entries for the album.
+     * @param aDir
+     *            Directory where the album is stored.
+     * @throws IOException
+     */
+    static boolean traverse(String aPath, List<PhotoEntry> aEntries, File aDir,
+        EntryFoundCallback aCallback) throws IOException {
+        File[] files = listFilesInDir(aDir);
+        for (int i = 0; i < files.length; i++) {
+            File file = files[i];
+            if (file.isDirectory()) {
+
+                if (file.getName().equals(THUMBNAILS_DIR)) {
+                    if (!traverseThumbnails(aPath, aEntries, aDir, aCallback,
+                        file)) {
+                        return false;
+                    }
+                } else if (file.getName().equals(PHOTOS_DIR)) {
+                    // Skip the photos directory.
+                } else {
+                    // A nested album.
+                    String newPath = aPath + "/" + file.getName();
+                    newPath = newPath.replaceAll("//", "/");
+                    if (!aCallback.albumFound(aEntries, file, newPath)) {
+                        return false; // finished.
+                    }
+                }
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Traverse the thumnails directory.
+     * 
+     * @param aPath
+     *            Path of the photo album.
+     * @param aEntries
+     *            Entries of the album.
+     * @param aDir
+     *            Directory of the album.
+     * @param aCallback
+     *            Callback to call when a thumbnail has been found.
+     * @param aFile
+     *            Directory of the photo album.
+     */
+    private static boolean traverseThumbnails(String aPath,
+        List<PhotoEntry> aEntries, File aDir, EntryFoundCallback aCallback,
+        File aFile) {
+        // Go inside the thumbnails directory to scan
+        // for available photos.
+
+        // Check if the corresponding photos directory
+        // exists
+        File photosDir = new File(aDir, PHOTOS_DIR);
+        if (photosDir.isDirectory()) {
+            if (!buildAlbum(aPath, aEntries, aFile, photosDir, aCallback)) {
+                return false;
+            }
+        } else {
+            LOG.info("Thumbnails director " + aFile.getPath() +
+                " exists but corresponding photo directory " +
+                photosDir.getPath() + " not found");
+        }
+        return true;
+    }
+
+    /**
+     * Builds up the photo album for a given thumbnails and photo directory.
+     * 
+     * @param aPath
+     *            Path of the album.
+     * @param aEntries
+     *            Photo entries of the album.
+     * @param aThumbnailsDir
+     *            Directory containing thumbnail pictures.
+     * @param aPhotosDir
+     *            Directory containing full size photos.
+     */
+    private static boolean buildAlbum(String aPath, List<PhotoEntry> aEntries,
+        File aThumbnailsDir, File aPhotosDir, EntryFoundCallback aCallback) {
+
+        File[] thumbnails = listFilesInDir(aThumbnailsDir);
+        for (int i = 0; i < thumbnails.length; i++) {
+            File thumbnail = thumbnails[i];
+            if (!isThumbNail(thumbnail)) {
+                LOG.info("Skipping " + thumbnail.getPath() +
+                    " because it is not a thumbnail file.");
+                continue;
+            }
+            File photo = new File(aPhotosDir, photoName(thumbnail.getName()));
+            if (!photo.isFile()) {
+                LOG.info("Photo file " + photo.getPath() + " for thumbnail " +
+                    thumbnail.getPath() + " does not exist.");
+                continue;
+            }
+            String photoPath = photo.getName();
+            photoPath = photoPath.substring(0, photoPath.length() -
+                JPG_EXTENSION.length());
+            photoPath = aPath + "/" + photoPath;
+            photoPath = photoPath.replaceAll("//", "/");
+            if (!aCallback.photoFound(aEntries, thumbnail, photo, photoPath)) {
+                return false;
+            }
+        }
+        return true; // continue.
+    }
+
+    /*
+     * (non-Javadoc)
+     * 
+     * @see org.wamblee.photos.database.PhotoEntry#getPath()
+     */
+    public String getPath() {
+        return _path;
+    }
+
+    /**
+     * Checks if the file represents a thumbnail.
+     * 
+     * @param aFile
+     *            File to check.
+     * @return True iff the file is a thumbnail.
+     */
+    private static boolean isThumbNail(File aFile) {
+        return aFile.getName().endsWith(THUMBNAIL_ENDING);
+    }
+
+    /**
+     * Constructs the photo name based on the thumbnail name.
+     * 
+     * @param aThumbnailName
+     *            Thumbnail name.
+     * @return Photo name.
+     */
+    private static String photoName(String aThumbnailName) {
+        return aThumbnailName.substring(0, aThumbnailName.length() -
+            THUMBNAIL_ENDING.length()) +
+            JPG_EXTENSION;
+    }
+
+    /*
+     * (non-Javadoc)
+     * 
+     * @see org.wamblee.photos.database.Album#getEntry(java.lang.String)
+     */
+    public PhotoEntry getEntry(String aPath) {
+        if (!aPath.startsWith("/")) {
+            throw new IllegalArgumentException(
+                "Path must start with / character");
+        }
+        if (aPath.equals("/")) {
+            return this;
+        }
+        String[] fields = aPath.substring(1).split("/");
+        return getEntry(fields, 0);
+    }
+
+    /**
+     * Gets the entry for the given path.
+     */
+    public PhotoEntry getEntry(Path aPath) {
+        return getEntry(aPath.toString());
+    }
+
+    /**
+     * Gets the entry at a given path.
+     * 
+     * @param aPath
+     *            Array of components of the path.
+     * @param aLevel
+     *            Current level in the path.
+     * @return Entry.
+     */
+    private PhotoEntry getEntry(String[] aPath, int aLevel) {
+        String id = aPath[aLevel];
+        PhotoEntry entry = find(id);
+        if (entry == null) {
+            return entry;
+        }
+        if (aLevel < aPath.length - 1) {
+            if (!(entry instanceof Album)) {
+                return null;
+            }
+            return ((FileSystemAlbum) entry).getEntry(aPath, aLevel + 1);
+        } else {
+            return entry; // end of the path.
+        }
+    }
+
+    /**
+     * Finds an entry in the album with the given id.
+     * 
+     * @param aId
+     *            Photo entry id.
+     * @return Photo entry.
+     */
+    private PhotoEntry find(String aId) {
+        return findInMap(aId).getFirst();
+    }
+
+    /**
+     * Finds a photo entry in the map.
+     * 
+     * @param aId
+     *            Id of the photo entry.
+     * @return Pair of a photo entry and the index of the item.
+     */
+    private Pair<PhotoEntry, Integer> findInMap(String aId) {
+        List<PhotoEntry> entries = _entries.get();
+        for (int i = 0; i < entries.size(); i++) {
+            PhotoEntry entry = entries.get(i);
+            if (entry.getId().equals(aId)) {
+                return new Pair<PhotoEntry, Integer>(entry, i);
+            }
+        }
+        return new Pair<PhotoEntry, Integer>(null, -1);
+    }
+
+    /*
+     * (non-Javadoc)
+     * 
+     * @see org.wamblee.photos.database.Album#getEntry(int)
+     */
+    public PhotoEntry getEntry(int aIndex) {
+        return _entries.get().get(aIndex);
+    }
+
+    /*
+     * (non-Javadoc)
+     * 
+     * @see org.wamblee.photos.database.Album#size()
+     */
+    public int size() {
+        return _entries.get().size();
+    }
+
+    /*
+     * (non-Javadoc)
+     * 
+     * @see org.wamblee.photos.database.PhotoEntry#getId()
+     */
+    public String getId() {
+        return _dir.getName();
+    }
+
+    public String toString() {
+        return print("");
+    }
+
+    /**
+     * Prints the album with a given indentation.
+     * 
+     * @param aIndent
+     *            Indentation string.
+     * @return String representation of the album.
+     */
+    private String print(String aIndent) {
+        String res = aIndent + "ALBUM: " + getId() + "\n";
+        aIndent += "  ";
+        for (int i = 0; i < size(); i++) {
+            PhotoEntry entry = getEntry(i);
+            if (entry instanceof FileSystemAlbum) {
+                res += ((FileSystemAlbum) entry).print(aIndent);
+            } else {
+                res += ((FileSystemPhoto) entry).print(aIndent);
+            }
+            res += "\n";
+        }
+        return res;
+    }
+
+    /*
+     * (non-Javadoc)
+     * 
+     * @see org.wamblee.photos.database.Album#addImage(java.lang.String,
+     *      java.awt.Image)
+     */
+    public void addImage(String aId, InputStream aImageStream)
+        throws IOException {
+        File photoDir = getPhotoDir();
+        File thumbnailDir = getThumbnailDir();
+
+        checkPhotoDirs(aId, photoDir, thumbnailDir);
+
+        BufferedImage image;
+        try {
+            image = JpegUtils.loadJpegImage(aImageStream);
+        } catch (InterruptedException e) {
+            throw new IOException("Loading image interruptedS: " +
+                e.getMessage());
+        }
+        BufferedImage thumbnailImage = JpegUtils.scaleImage(THUMBNAIL_WIDTH,
+            THUMBNAIL_HEIGHT, image);
+
+        File thumbnail = new File(thumbnailDir, aId + THUMBNAIL_ENDING);
+        File photo = new File(photoDir, aId + JPG_EXTENSION);
+
+        writeImage(photo, image);
+        writeImage(thumbnail, thumbnailImage);
+
+        Photo photoEntry = new FileSystemPhoto(thumbnail, photo, (getPath()
+            .equals("/") ? "" : getPath()) + "/" + aId);
+        addEntry(photoEntry);
+    }
+
+    /**
+     * Checks if a given entry can be added to the album.
+     * 
+     * @param aId
+     *            Id of the new entry.
+     * @param aPhotoDir
+     *            Photo directory.
+     * @param aThumbnailDir
+     *            Thumbnail directory.
+     * @throws IOException
+     */
+    private void checkPhotoDirs(String aId, File aPhotoDir, File aThumbnailDir)
+        throws IOException {
+        // Create directories if they do not already exist.
+        aPhotoDir.mkdir();
+        aThumbnailDir.mkdir();
+
+        if (!aPhotoDir.isDirectory() || !aThumbnailDir.isDirectory()) {
+            throw new IOException("Album " + getId() +
+                " does not accept new images.");
+        }
+
+        if (getEntry("/" + aId) != null) {
+            throw new IOException("An entry with the name '" + aId +
+                "' already exists in the album " + getPath());
+        }
+    }
+
+    /**
+     * Gets the thumbnail directory for the album.
+     * 
+     * @return Directory.
+     */
+    private File getThumbnailDir() {
+        File thumbnailDir = new File(_dir, THUMBNAILS_DIR);
+        return thumbnailDir;
+    }
+
+    /**
+     * Gets the photo directory for the album.
+     * 
+     * @return Photo directory
+     */
+    private File getPhotoDir() {
+        File photoDir = new File(_dir, PHOTOS_DIR);
+        return photoDir;
+    }
+
+    /**
+     * Writes an image to a file.
+     * 
+     * @param aFile
+     *            File to write to.
+     * @param aImage
+     *            Image.
+     * @throws IOException
+     */
+    private static void writeImage(File aFile, BufferedImage aImage)
+        throws IOException {
+        OutputStream output = new BufferedOutputStream(new FileOutputStream(
+            aFile));
+        JpegUtils.writeJpegImage(output, JPG_QUALITY, aImage);
+        output.close();
+    }
+
+    /*
+     * (non-Javadoc)
+     * 
+     * @see org.wamblee.photos.database.Album#addAlbum(java.lang.String)
+     */
+    public void addAlbum(String aId) throws IOException {
+        PhotoEntry entry = find(aId);
+        if (entry != null) {
+            throw new IOException("Entry already exists in album " + getId() +
+                " : " + aId);
+        }
+        // Entry not yet found. Try to create it.
+        File albumDir = new File(_dir, aId);
+        if (!albumDir.mkdir()) {
+            throw new IOException("Could not create album: " + aId);
+        }
+        File photosDir = new File(albumDir, PHOTOS_DIR);
+        if (!photosDir.mkdir()) {
+            throw new IOException("Could  not create photo storage dir: " +
+                photosDir.getPath());
+        }
+        File thumbnailsDir = new File(albumDir, THUMBNAILS_DIR);
+        if (!thumbnailsDir.mkdir()) {
+            throw new IOException("Coul dnot create thumbnails storage dir: " +
+                thumbnailsDir.getPath());
+        }
+        String newPath = _path + "/" + aId;
+        newPath = newPath.replaceAll("//", "/");
+        FileSystemAlbum album = new FileSystemAlbum(albumDir, newPath,
+            _entries.getCache());
+        addEntry(album);
+    }
+
+    /*
+     * (non-Javadoc)
+     * 
+     * @see org.wamblee.photos.database.Album#removeAlbum(java.lang.String)
+     */
+    public void removeEntry(String aId) throws IOException {
+        PhotoEntry entry = find(aId);
+        if (entry == null) {
+            throw new IOException("Entry " + aId + " not found.");
+        }
+        if (entry instanceof FileSystemAlbum) {
+            // album.
+            removeAlbum((FileSystemAlbum) entry);
+        } else {
+            // regular photo
+            removePhoto(entry);
+        }
+    }
+
+    /**
+     * Removes a photo entry.
+     * 
+     * @param aEntry
+     *            Photo entry to remove.
+     * @throws IOException
+     */
+    private void removePhoto(PhotoEntry aEntry) throws IOException {
+        File photo = new File(_dir, PHOTOS_DIR);
+        photo = new File(photo, aEntry.getId() + JPG_EXTENSION);
+        File thumbnail = new File(_dir, THUMBNAILS_DIR);
+        thumbnail = new File(thumbnail, aEntry.getId() + THUMBNAIL_ENDING);
+        if (!photo.isFile()) {
+            throw new IOException("Photo file not found: " + photo.getPath());
+        }
+        if (!thumbnail.isFile()) {
+            throw new IOException("Thumbnail file not found: " +
+                thumbnail.getPath());
+        }
+        if (!thumbnail.delete()) {
+            throw new IOException("Could not delete thumbnail file: " +
+                thumbnail.getPath());
+        }
+        _entries.get().remove(aEntry);
+        if (!photo.delete()) {
+            throw new IOException("Could not delete photo file: " +
+                photo.getPath());
+        }
+    }
+
+    /**
+     * Removes the album entry from this album.
+     * 
+     * @param aAlbum
+     *            Album to remove
+     * @throws IOException
+     */
+    private void removeAlbum(FileSystemAlbum aAlbum) throws IOException {
+        if (aAlbum.size() > 0) {
+            throw new IOException("Album " + aAlbum.getId() + " not empty.");
+        }
+        // album is empty, safe to remove.
+        File photosDir = new File(aAlbum._dir, PHOTOS_DIR);
+        File thumbnailsDir = new File(aAlbum._dir, THUMBNAILS_DIR);
+        if (!photosDir.delete() || !thumbnailsDir.delete() ||
+            !aAlbum._dir.delete()) {
+            throw new IOException("Could not delete directories for album:" +
+                aAlbum.getId());
+        }
+        _entries.get().remove(aAlbum);
+    }
+
+    /**
+     * Adds an entry.
+     * 
+     * @param aEntry
+     *            Entry to add.
+     */
+    private void addEntry(PhotoEntry aEntry) {
+        int i = 0;
+        List<PhotoEntry> entries = _entries.get();
+        int nentries = entries.size();
+        while (i < nentries && aEntry.compareTo(entries.get(i)) > 0) {
+            i++;
+        }
+        entries.add(i, aEntry);
+    }
+
+    /* (non-Javadoc)
+     * @see java.lang.Object#equals(java.lang.Object)
+     */
+    @Override
+    public boolean equals(Object obj) {
+        if (!(obj instanceof Album)) {
+            return false;
+        }
+        return toString().equals(obj.toString());
+    }
+
+    /* (non-Javadoc)
+     * @see java.lang.Object#hashCode()
+     */
+    @Override
+    public int hashCode() {
+        return toString().hashCode();
+    }
+
+    /*
+     * (non-Javadoc)
+     * 
+     * @see java.lang.Comparable#compareTo(java.lang.Object)
+     */
+    public int compareTo(PhotoEntry aEntry) {
+        return getId().compareTo(((PhotoEntry) aEntry).getId());
+    }
+
+    /**
+     * Returns and alphabetically sorted list of files.
+     * 
+     * @param aDir
+     * @return Sorted list of files.
+     */
+    private static File[] listFilesInDir(File aDir) {
+        File[] lResult = aDir.listFiles();
+        Arrays.sort(lResult);
+        return lResult;
+    }
+
+    /*
+     *  (non-Javadoc)
+     * @see org.wamblee.photos.model.Album#findPhotoBefore(java.lang.String)
+     */
+    public Photo findPhotoBefore(String aId) {
+        Pair<PhotoEntry, Integer> entry = findInMap(aId);
+        if (entry.getFirst() == null) {
+            throw new IllegalArgumentException("Entry with id '" + aId +
+                "' not found in album '" + getPath() + "'");
+        }
+
+        int i = entry.getSecond() - 1;
+        List<PhotoEntry> entries = _entries.get();
+        while (i >= 0 && i < entries.size() &&
+            !(entries.get(i) instanceof Photo)) {
+            i--;
+        }
+        if (i >= 0) {
+            return (Photo) entries.get(i);
+        }
+        return null;
+    }
+
+    /*
+     *  (non-Javadoc)
+     * @see org.wamblee.photos.model.Album#findPhotoAfter(java.lang.String)
+     */
+    public Photo findPhotoAfter(String aId) {
+        Pair<PhotoEntry, Integer> entry = findInMap(aId);
+        if (entry.getFirst() == null) {
+            throw new IllegalArgumentException("Entry with id '" + aId +
+                "' not found in album '" + getPath() + "'");
+        }
+        int i = entry.getSecond() + 1;
+        List<PhotoEntry> entries = _entries.get();
+        while (i >= 0 && i < entries.size() &&
+            !(entries.get(i) instanceof Photo)) {
+            i++;
+        }
+        if (i < entries.size()) {
+            return (Photo) entries.get(i);
+        }
+        return null;
+    }
+}
diff --git a/src/main/java/org/wamblee/photos/model/filesystem/FileSystemPhoto.java b/src/main/java/org/wamblee/photos/model/filesystem/FileSystemPhoto.java
new file mode 100644 (file)
index 0000000..ec1452b
--- /dev/null
@@ -0,0 +1,123 @@
+/*
+ * 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.model.filesystem;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+
+import org.wamblee.photos.model.Photo;
+import org.wamblee.photos.model.PhotoEntry;
+
+/**
+ * Represents a photo stored on the file system.
+ */
+public class FileSystemPhoto implements Photo {
+
+       private File _thumbnail;
+       private File _photo;
+       private String _path; 
+
+       public FileSystemPhoto(File aThumbnail, File aPhoto, String aPath) {
+               _thumbnail = aThumbnail;
+               _photo = aPhoto;
+               _path = aPath;
+       }
+
+       /*
+        * (non-Javadoc)
+        * 
+        * @see org.wamblee.photos.database.PhotoEntry#getPath()
+        */
+       public String getPath() {
+               return _path;
+       }
+
+       /*
+        * (non-Javadoc)
+        * 
+        * @see org.wamblee.photos.database.Photo#getThumbNail()
+        */
+       public InputStream getThumbNail() throws IOException {
+               return openFile(_thumbnail);
+       }
+
+       /*
+        * (non-Javadoc)
+        * 
+        * @see org.wamblee.photos.database.Photo#getFile()
+        */
+       public InputStream getPhoto() throws IOException {
+               return openFile(_photo);
+       }
+
+       private InputStream openFile(File aFile) throws FileNotFoundException {
+               return new FileInputStream(aFile);
+       }
+
+       /*
+        * (non-Javadoc)
+        * 
+        * @see org.wamblee.photos.database.PhotoEntry#getId()
+        */
+       public String getId() {
+               return getId(_path);
+       }
+    
+    public static String getId(String aPath) {
+        int index = aPath.lastIndexOf("/");
+        if (index < 0) {
+            return aPath;
+        }
+        return aPath.substring(index + 1);
+    }
+
+       public String toString() {
+               return print("");
+       }
+
+       String print(String aIndent) {
+               return aIndent + "IMG: " + _photo.getName();
+       }
+    
+    /* (non-Javadoc)
+     * @see java.lang.Object#equals(java.lang.Object)
+     */
+    @Override
+    public boolean equals(Object obj) {
+        if ( !(obj instanceof Photo) ) { 
+            return false; 
+        }
+        return getPath().equals( ((Photo)obj).getPath()); 
+    }
+    
+    /* (non-Javadoc)
+     * @see java.lang.Object#hashCode()
+     */
+    @Override
+    public int hashCode() {
+        return getPath().hashCode();
+    }
+    
+    /* (non-Javadoc)
+     * @see java.lang.Comparable#compareTo(java.lang.Object)
+     */
+    public int compareTo(PhotoEntry aEntry) {
+        return getId().compareTo(((PhotoEntry)aEntry).getId()); 
+    }
+}
diff --git a/src/main/java/org/wamblee/photos/model/filesystem/FindByIdCallback.java b/src/main/java/org/wamblee/photos/model/filesystem/FindByIdCallback.java
new file mode 100644 (file)
index 0000000..054ba5f
--- /dev/null
@@ -0,0 +1,86 @@
+/*
+ * 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.model.filesystem;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+
+import org.wamblee.cache.Cache;
+import org.wamblee.photos.model.PhotoEntry;
+
+/**
+ * Callback interface used to find a photo entry by id. 
+ */
+class FindByIdCallback implements EntryFoundCallback {
+    /**
+     * Id of the entry to look for. 
+     */
+       private String _id;
+    
+    /**
+     * Search result or null if not found yet.  
+     */
+       private PhotoEntry _entry;
+    
+    /**
+     * Cache to use. 
+     */
+    private Cache _cache; 
+       
+    /**
+     * Constructs the callback. 
+     * @param aId Photo entry id to look for. 
+     */
+       public FindByIdCallback(String aId, Cache aCache) {
+               _id = aId;    
+               _entry = null; 
+        _cache = aCache; 
+       }
+    
+    /**
+     * Gets the entry that was found. 
+     * @return Photo entry or null if not found. 
+     */
+       public PhotoEntry getEntry() {
+               return _entry;    
+       }
+    
+    /*
+     *  (non-Javadoc)
+     * @see org.wamblee.photos.model.file.EntryFoundCallback#photoFound(java.io.File, java.io.File, java.lang.String)
+     */
+       public boolean photoFound(List<PhotoEntry> aEntries, File aThumbnail, File aPhoto, String aPath) {
+               if ( FileSystemPhoto.getId(aPath).equals(_id)) {
+                       _entry = new FileSystemPhoto(aThumbnail, aPhoto, aPath);
+                       return false; 
+               }
+               return true; // continue search.
+       }
+       
+    /*
+     *  (non-Javadoc)
+     * @see org.wamblee.photos.model.file.EntryFoundCallback#albumFound(java.io.File, java.lang.String)
+     */
+       public boolean albumFound(List<PhotoEntry> aEntries, File aAlbum, String aPath) throws IOException {
+               FileSystemAlbum albumEntry = new FileSystemAlbum(aAlbum, aPath, _cache); 
+               if ( aAlbum.getName().equals(_id)) {
+                       _entry = albumEntry; 
+                       return false; 
+               }
+               return albumEntry.traverse(albumEntry.getPath(), aEntries, aAlbum, this); // continue search.
+       }
+}
diff --git a/src/main/java/org/wamblee/photos/model/plumbing/AllPhotos.java b/src/main/java/org/wamblee/photos/model/plumbing/AllPhotos.java
new file mode 100644 (file)
index 0000000..4da6b55
--- /dev/null
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2013 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.plumbing;
+
+import static java.lang.annotation.ElementType.*;
+import static java.lang.annotation.RetentionPolicy.*;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+import javax.inject.Qualifier;
+
+@Qualifier
+@Retention(RUNTIME)
+@Target({ METHOD, PARAMETER, FIELD })
+public @interface AllPhotos {
+    // Empty.
+}
diff --git a/src/main/java/org/wamblee/photos/model/plumbing/AuthorizedPhotos.java b/src/main/java/org/wamblee/photos/model/plumbing/AuthorizedPhotos.java
new file mode 100644 (file)
index 0000000..935a79e
--- /dev/null
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2013 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.plumbing;
+
+import static java.lang.annotation.ElementType.*;
+import static java.lang.annotation.RetentionPolicy.*;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+import javax.inject.Qualifier;
+
+@Qualifier
+@Retention(RUNTIME)
+@Target({ METHOD, PARAMETER, FIELD })
+public @interface AuthorizedPhotos {
+    // Empty.
+}
diff --git a/src/main/java/org/wamblee/photos/model/plumbing/Configuration.java b/src/main/java/org/wamblee/photos/model/plumbing/Configuration.java
new file mode 100644 (file)
index 0000000..e84c5d4
--- /dev/null
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2005-2013 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.plumbing;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Properties;
+
+/**
+ * @author Erik Brakkee
+ * 
+ */
+public class Configuration {
+
+    private String path;
+
+    protected Configuration() {
+        // for cdi
+    }
+
+    public Configuration(InputStream aIs) throws IOException {
+        Properties props = new Properties();
+        try {
+            props.load(aIs);
+        } finally {
+            aIs.close();
+        }
+        path = props.getProperty("org.wamblee.photos.path");
+    }
+
+    /**
+     * @return the path
+     */
+    public String getPath() {
+        return path;
+    }
+
+}
diff --git a/src/main/java/org/wamblee/photos/model/plumbing/Eager.java b/src/main/java/org/wamblee/photos/model/plumbing/Eager.java
new file mode 100644 (file)
index 0000000..0c081a2
--- /dev/null
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2005-2013 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.plumbing;
+
+import static java.lang.annotation.ElementType.*;
+import static java.lang.annotation.RetentionPolicy.*;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+@Retention(RUNTIME)
+@Target({ TYPE, METHOD })
+public @interface Eager {
+    // Empty.
+}
diff --git a/src/main/java/org/wamblee/photos/model/plumbing/EagerExtension.java b/src/main/java/org/wamblee/photos/model/plumbing/EagerExtension.java
new file mode 100644 (file)
index 0000000..e3fee74
--- /dev/null
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2005-2013 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.plumbing;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.enterprise.context.ApplicationScoped;
+import javax.enterprise.context.spi.Context;
+import javax.enterprise.context.spi.CreationalContext;
+import javax.enterprise.event.Observes;
+import javax.enterprise.inject.spi.AfterDeploymentValidation;
+import javax.enterprise.inject.spi.Bean;
+import javax.enterprise.inject.spi.BeanManager;
+import javax.enterprise.inject.spi.Extension;
+import javax.enterprise.inject.spi.ProcessBean;
+
+public class EagerExtension implements Extension {
+
+    private List<Bean<?>> eagerBeansList = new ArrayList<Bean<?>>();
+
+    public <T> void collect(@Observes ProcessBean<T> event) {
+        if (event.getAnnotated().isAnnotationPresent(Eager.class) &&
+            event.getAnnotated().isAnnotationPresent(ApplicationScoped.class)) {
+            eagerBeansList.add(event.getBean());
+        }
+    }
+
+    public void load(@Observes AfterDeploymentValidation event,
+        BeanManager beanManager) {
+        for (Bean<?> bean : eagerBeansList) {
+            // note: toString() is important to instantiate the bean
+            /*
+            Class main = Object.class;
+            for (Type type : bean.getTypes()) {
+                if (type instanceof Class) {
+                    Class clazz = (Class) type;
+                    if (main.isAssignableFrom(clazz)) {
+                        main = clazz;
+                    }
+                }
+            }*/
+            /*
+            bean.create((CreationalContext) beanManager
+                .createCreationalContext(bean));*/
+            /* beanManager.getReference(bean, main,
+                 beanManager.createCreationalContext(null)).toString();
+             beanManager.getReference(bean, main,
+                 beanManager.createCreationalContext(null)).toString(); */
+            /*
+                        beanManager.getReference(bean, bean.getBeanClass(),
+                            beanManager.createCreationalContext(bean)).toString();
+                            */
+
+            Context context = beanManager.getContext(ApplicationScoped.class);
+            context.get(bean,
+                (CreationalContext) beanManager.createCreationalContext(bean))
+                .toString();
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/org/wamblee/photos/model/plumbing/Initializer.java b/src/main/java/org/wamblee/photos/model/plumbing/Initializer.java
new file mode 100644 (file)
index 0000000..2509641
--- /dev/null
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2005-2013 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.plumbing;
+
+import javax.annotation.PostConstruct;
+import javax.ejb.Singleton;
+import javax.ejb.Startup;
+import javax.inject.Inject;
+
+import org.wamblee.photos.model.Album;
+import org.wamblee.security.authentication.UserAdministration;
+
+/**
+ * @author Erik Brakkee
+ * 
+ */
+@Singleton
+@Startup
+public class Initializer {
+
+    @Inject
+    private UserAdministration userAdmin;
+
+    @Inject
+    @AllPhotos
+    private Album album;
+
+    @PostConstruct
+    public void init() {
+        System.out.println("Photo application initializing");
+
+        userAdmin.getUserCount();
+        album.size();
+    }
+}
diff --git a/src/main/java/org/wamblee/photos/model/plumbing/PhotoApp.java b/src/main/java/org/wamblee/photos/model/plumbing/PhotoApp.java
new file mode 100644 (file)
index 0000000..2ba0a6d
--- /dev/null
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2013 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.plumbing;
+
+import static java.lang.annotation.ElementType.*;
+import static java.lang.annotation.RetentionPolicy.*;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+import javax.inject.Qualifier;
+
+@Qualifier
+@Retention(RUNTIME)
+@Target({ METHOD, PARAMETER, FIELD })
+public @interface PhotoApp {
+    // Empty.
+}
diff --git a/src/main/java/org/wamblee/photos/model/plumbing/Producer.java b/src/main/java/org/wamblee/photos/model/plumbing/Producer.java
new file mode 100644 (file)
index 0000000..9ad3c14
--- /dev/null
@@ -0,0 +1,172 @@
+/*
+ * Copyright 2005-2013 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.plumbing;
+
+import java.io.File;
+import java.io.IOException;
+import java.security.Principal;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.enterprise.context.ApplicationScoped;
+import javax.enterprise.context.SessionScoped;
+import javax.enterprise.inject.Produces;
+import javax.inject.Inject;
+import javax.persistence.EntityManager;
+import javax.persistence.PersistenceContext;
+import javax.servlet.http.HttpServletRequest;
+
+import org.apache.log4j.Logger;
+import org.wamblee.cache.Cache;
+import org.wamblee.cache.EhCache;
+import org.wamblee.io.ClassPathResource;
+import org.wamblee.io.InputResource;
+import org.wamblee.photos.concurrent.ConcurrentAlbum;
+import org.wamblee.photos.model.Album;
+import org.wamblee.photos.model.PhotoEntry;
+import org.wamblee.photos.model.filesystem.FileSystemAlbum;
+import org.wamblee.security.authentication.GroupSet;
+import org.wamblee.security.authentication.Md5HexMessageDigester;
+import org.wamblee.security.authentication.MessageDigester;
+import org.wamblee.security.authentication.NameValidator;
+import org.wamblee.security.authentication.RegexpNameValidator;
+import org.wamblee.security.authentication.User;
+import org.wamblee.security.authentication.UserAdminInitializer;
+import org.wamblee.security.authentication.UserAdministration;
+import org.wamblee.security.authentication.UserAdministrationImpl;
+import org.wamblee.security.authentication.UserSet;
+import org.wamblee.security.authentication.jpa.JpaGroupSet;
+import org.wamblee.security.authentication.jpa.JpaUserSet;
+
+/**
+ * @author Erik Brakkee
+ * 
+ */
+public class Producer {
+
+    private static final Logger LOGGER = Logger.getLogger(Producer.class
+        .getName());
+
+    private static final String APP_CONFIG_RESOURCE = "META-INF/org.wamblee.photos.properties";
+
+    @Inject
+    private HttpServletRequest request;
+
+    @PersistenceContext
+    private EntityManager entityManager;
+
+    private Configuration getCOnfiguration() {
+        LOGGER.info("Initializing configuration");
+        Configuration config;
+        try {
+            config = new Configuration(new ClassPathResource(
+                APP_CONFIG_RESOURCE).getInputStream());
+        } catch (IOException e) {
+            throw new RuntimeException(
+                "Could not read application configuration property classpath resource " +
+                    APP_CONFIG_RESOURCE, e);
+        }
+        return config;
+    }
+
+    @Produces
+    @ApplicationScoped
+    public UserAdministration getUserAdmin() {
+        LOGGER.info("Initializing user administration");
+        try {
+            NameValidator passwordvalidator = new RegexpNameValidator(".{5,}",
+                "INVALID_PASSWORD", "Password must have at least 5 characters");
+            InputResource cacheConfig = new ClassPathResource(
+                "META-INF/ehcache.xml");
+            Cache<String, User> userCache = new EhCache(cacheConfig, "users");
+            MessageDigester passwordEncoder = new Md5HexMessageDigester();
+            UserSet userset = new JpaUserSet(userCache, passwordvalidator,
+                passwordEncoder, entityManager);
+            GroupSet groupset = new JpaGroupSet(entityManager);
+            NameValidator uservalidator = new RegexpNameValidator(
+                "[a-zA-Z]+[a-zA-Z0-9]*", "INVALID_USERNAME",
+                "User name must consist of alphanumeric characters only");
+            NameValidator groupvalidator = new RegexpNameValidator(
+                "[a-zA-Z]+[a-zA-Z0-9]*", "INVALID_GROUPNAME",
+                "Group name must consist of alphanumeric characters only");
+
+            UserAdministration admin = new UserAdministrationImpl(userset,
+                groupset, uservalidator, groupvalidator);
+            UserAdminInitializer initializer = new UserAdminInitializer(admin,
+                new String[] { "erik", "admin" }, new String[] { "users",
+                    "administrators" }, new String[] { "abc123", "abc123" });
+            return admin;
+        } catch (IOException e) {
+            throw new RuntimeException(
+                "Could not initialize user administration", e);
+        }
+    }
+
+    @Produces
+    @ApplicationScoped
+    @AllPhotos
+    public Album getAllPhotos() {
+        LOGGER.info("Initializing photo album");
+
+        try {
+            File dir = new File(getCOnfiguration().getPath());
+            InputResource cacheConfig = new ClassPathResource(
+                "META-INF/ehcache.xml");
+            Cache<String, ArrayList<PhotoEntry>> photoCache = new EhCache<String, ArrayList<PhotoEntry>>(
+                cacheConfig, "photos");
+            Album fileSystemAlbum = new FileSystemAlbum(dir, "/", photoCache);
+            Album concurrentAlbum = new ConcurrentAlbum(fileSystemAlbum);
+
+            return concurrentAlbum;
+        } catch (IOException e) {
+            throw new RuntimeException("Could not initialize photo album", e);
+        }
+    }
+
+    @Produces
+    @SessionScoped
+    @AuthorizedPhotos
+    public Album getAuthorizedPhotos() {
+        LOGGER.info("Initializing authorized photos for current session");
+
+        return null;
+    }
+
+    @Produces
+    @SessionScoped
+    public User getUser() {
+        LOGGER.info("Initializing user object for current session");
+
+        Principal userPrincipal = request.getUserPrincipal();
+        if (userPrincipal == null) {
+            // CDI: cannot return null from this method.
+            throw new RuntimeException("No authenticated user");
+        }
+        String username = userPrincipal.getName();
+        List<User> users = entityManager
+            .createNamedQuery(User.QUERY_FIND_BY_NAME)
+            .setParameter(User.NAME_PARAM, username).getResultList();
+        if (users.size() > 1) {
+            throw new RuntimeException("More than one user found for '" +
+                username + "'");
+        }
+        if (users.isEmpty()) {
+            throw new RuntimeException("No authenticated user");
+        }
+        return users.get(0);
+    }
+
+}
diff --git a/src/main/java/org/wamblee/photos/security/AuthenticationFilter.java b/src/main/java/org/wamblee/photos/security/AuthenticationFilter.java
new file mode 100644 (file)
index 0000000..04442f2
--- /dev/null
@@ -0,0 +1,84 @@
+/*
+ * 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.security;
+
+import java.io.IOException;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+
+public class AuthenticationFilter implements Filter {
+    private static final String LOGINPAGE = "loginpage";
+    private static final String REQUIRED_ROLE = "role";
+    private static final String RESOURCES = "resources";
+
+    private String loginPage;
+    private String role;
+    private String resources;
+
+    public AuthenticationFilter() {
+        // Empty.
+    }
+
+    @Override
+    public void init(FilterConfig aFilterConfig) throws ServletException {
+        loginPage = aFilterConfig.getInitParameter(LOGINPAGE);
+        if (loginPage == null) {
+            throw new ServletException("No login page defined! Must specify '" +
+                LOGINPAGE + "' filter init parameter.");
+        }
+        role = aFilterConfig.getInitParameter(REQUIRED_ROLE);
+        if (role == null) {
+            throw new ServletException("No role name defined! Must specify '" +
+                REQUIRED_ROLE + "' filter init parameter.");
+        }
+        resources = aFilterConfig.getInitParameter(RESOURCES);
+    }
+
+    @Override
+    public void doFilter(ServletRequest aRequest, ServletResponse aResponse,
+        FilterChain aChain) throws IOException, ServletException {
+
+        HttpServletRequest request = (HttpServletRequest) aRequest;
+        String fullPath = request.getRequestURI();
+        String contextPath = request.getContextPath();
+        String relpath = null;
+        if (fullPath.startsWith(contextPath)) {
+            relpath = fullPath.substring(contextPath.length());
+        }
+
+        if (request.isUserInRole(role) ||
+            (resources != null && relpath != null && relpath
+                .startsWith(resources))) {
+            aChain.doFilter(aRequest, aResponse);
+        } else {
+            request.getSession().invalidate();
+            request.getRequestDispatcher(loginPage)
+                .forward(aRequest, aResponse);
+        }
+    }
+
+    @Override
+    public void destroy() {
+        // Empty.
+    }
+
+}
diff --git a/src/main/java/org/wamblee/photos/security/PhotoAuthorizationRule.java b/src/main/java/org/wamblee/photos/security/PhotoAuthorizationRule.java
new file mode 100644 (file)
index 0000000..0fab5c5
--- /dev/null
@@ -0,0 +1,103 @@
+/*
+ * 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.security;
+
+import java.util.List;
+
+import javax.inject.Inject;
+import javax.persistence.DiscriminatorValue;
+import javax.persistence.Entity;
+import javax.persistence.PostLoad;
+import javax.persistence.Transient;
+
+import org.wamblee.inject.InjectorBuilder;
+import org.wamblee.photos.model.PhotoEntry;
+import org.wamblee.security.authentication.UserAdministration;
+import org.wamblee.security.authorization.AuthorizationResult;
+import org.wamblee.security.authorization.Operation;
+import org.wamblee.security.authorization.ReadOperation;
+import org.wamblee.security.authorization.UrlAuthorizationRule;
+
+/**
+ * Authorization rule for photos. A user has access to all albums owned by his
+ * own group.
+ */
+@Entity
+@DiscriminatorValue("PHOTOS")
+public class PhotoAuthorizationRule extends UrlAuthorizationRule {
+
+    @Inject
+    @Transient
+    private UserAdministration userAdmin;
+
+    /**
+     * Constructs the authorization rule.
+     * 
+     */
+    public PhotoAuthorizationRule() {
+        // Empty.
+    }
+
+    @PostLoad
+    public void init() {
+        InjectorBuilder.getInjector().inject(this);
+    }
+
+    /* (non-Javadoc)
+     * @see org.wamblee.security.authorization.AuthorizationRule#getSupportedTypes()
+     */
+    public Class[] getSupportedTypes() {
+        return new Class[] { PhotoEntry.class };
+    }
+
+    /* (non-Javadoc)
+     * @see org.wamblee.security.authorization.AuthorizationRule#isAllowed(java.lang.Object, org.wamblee.security.authorization.Operation, org.wamblee.usermgt.User)
+     */
+    public AuthorizationResult isAllowed(Object aResource,
+        Operation anOperation, String aUser) {
+        if (!(aResource instanceof PhotoEntry)) {
+            return AuthorizationResult.UNSUPPORTED_RESOURCE;
+        }
+        String path = getResourcePath(aResource);
+        if (path.equals("/") && anOperation instanceof ReadOperation) {
+            return AuthorizationResult.GRANTED;
+        }
+        List<String> groups = userAdmin.getGroups(aUser);
+        for (String group : groups) {
+            String allowedPath = "/" + group;
+            if (path.startsWith(allowedPath)) {
+                return AuthorizationResult.GRANTED;
+            }
+        }
+        return AuthorizationResult.DENIED;
+    }
+
+    /**
+     * Gets the resource path for a photo entry.
+     */
+    protected String getResourcePath(Object aResource) {
+        return ((PhotoEntry) aResource).getPath();
+    }
+
+    /* (non-Javadoc)
+     * @see java.lang.Object#toString()
+     */
+    @Override
+    public String toString() {
+        return "PhotoAuthorizationRule()";
+    }
+}
diff --git a/src/main/java/org/wamblee/photos/utils/JpegUtils.java b/src/main/java/org/wamblee/photos/utils/JpegUtils.java
new file mode 100644 (file)
index 0000000..2b0a0e7
--- /dev/null
@@ -0,0 +1,106 @@
+/*
+ * 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.utils;
+
+import java.awt.Container;
+import java.awt.Graphics2D;
+import java.awt.Image;
+import java.awt.MediaTracker;
+import java.awt.RenderingHints;
+import java.awt.image.BufferedImage;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import sun.awt.image.codec.JPEGImageDecoderImpl;
+
+import com.sun.image.codec.jpeg.JPEGCodec;
+import com.sun.image.codec.jpeg.JPEGEncodeParam;
+import com.sun.image.codec.jpeg.JPEGImageDecoder;
+import com.sun.image.codec.jpeg.JPEGImageEncoder;
+
+/**
+ * Utility functions for processing JPEG images. 
+ */
+public class JpegUtils {
+       /**
+        * Scales an image preserving the aspect ratio. 
+        * 
+        * @param aMaxWidth Maximum width. 
+        * @param aMaxHeight Maximum height. 
+        * @param aImage Image to scale. 
+        * @return Scaled image. 
+        */
+       public static BufferedImage scaleImage(int aMaxWidth, int aMaxHeight, Image aImage) {
+        double thumbRatio = (double) aMaxWidth / (double) aMaxHeight;
+        int imageWidth = aImage.getWidth(null);
+        int imageHeight = aImage.getHeight(null);
+        double imageRatio = (double) imageWidth / (double) imageHeight;
+        if (thumbRatio < imageRatio) {
+            aMaxHeight = (int) (aMaxWidth / imageRatio);
+        } else {
+            aMaxWidth = (int) (aMaxHeight * imageRatio);
+        }
+               BufferedImage thumbImage = new BufferedImage(aMaxWidth, aMaxHeight,
+                               BufferedImage.TYPE_INT_RGB);
+               Graphics2D graphics2D = thumbImage.createGraphics();
+               graphics2D.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
+                               RenderingHints.VALUE_INTERPOLATION_BILINEAR);
+               graphics2D.drawImage(aImage, 0, 0, aMaxWidth, aMaxHeight, null);
+               return thumbImage;
+       }
+
+       /**
+        * Loads a jpeg image from an input stream.
+        *  
+        * @param aInput Input stream. 
+        * @return JPEG image. 
+        * @throws IOException In case of IO problems.  
+        * @throws InterruptedException When execution is interrupted. 
+        */
+       public static BufferedImage loadJpegImage(InputStream aInput) throws IOException, InterruptedException {
+               JPEGImageDecoder decoder = new JPEGImageDecoderImpl(aInput);
+               BufferedImage image = decoder.decodeAsBufferedImage();
+               MediaTracker mediaTracker = new MediaTracker(new Container());
+               mediaTracker.addImage(image, 0);
+               mediaTracker.waitForID(0);
+               return image;
+       }
+
+       /**
+        * Writes a JPEG image. 
+        * 
+        * @param aOutput Output stream to write to. 
+        * @param aQuality Quality of the JPEG image in the range 0..100
+        * @param aThumbImage
+        * @throws IOException
+        */
+       public static void writeJpegImage(OutputStream aOutput, int aQuality, BufferedImage aThumbImage) throws IOException {
+               // save thumbnail image to OUTFILE
+               
+               if ( aQuality < 0 || aQuality > 100 ) {
+                       throw new IllegalArgumentException("Argument quality must be in range 0.100: " + aQuality); 
+               }
+                       
+               JPEGImageEncoder encoder = JPEGCodec.createJPEGEncoder(aOutput);
+               JPEGEncodeParam param = encoder.getDefaultJPEGEncodeParam(aThumbImage);
+               aQuality = Math.max(0, Math.min(aQuality, 100));
+               param.setQuality((float) aQuality / 100.0f, false);
+               encoder.setJPEGEncodeParam(param);
+               encoder.encode(aThumbImage);
+       }
+
+}
diff --git a/src/main/java/org/wamblee/photos/wicket/BasePage.html b/src/main/java/org/wamblee/photos/wicket/BasePage.html
new file mode 100644 (file)
index 0000000..18506e3
--- /dev/null
@@ -0,0 +1,37 @@
+<html
+       xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.4-strict.dtd">
+<head>
+<wicket:head>
+       <title wicket:id="title">Title goes here</title>
+</wicket:head>
+</head>
+<body>
+       <div id="banner">
+               <wicket:link>
+                       <img src="wamblee_logo.png" />
+               </wicket:link>
+               <span class="title">wamblee photos</span>
+       </div>
+       <div id="logout">
+               <a href="#" wicket:id="logout">Logout</a>
+       </div>
+
+
+       <div id="menu">
+               <wicket:link>
+                       <ul>
+                               <li><a href="HomePage.html">Home</a></li>
+                       </ul>
+               </wicket:link>
+       </div>
+
+       <div id="feedback">
+               <div wicket:id="feedback"></div>
+       </div>
+
+       <div id="content">
+               <wicket:child />
+       </div>
+
+</body>
+</html>
diff --git a/src/main/java/org/wamblee/photos/wicket/BasePage.java b/src/main/java/org/wamblee/photos/wicket/BasePage.java
new file mode 100644 (file)
index 0000000..d4bdc11
--- /dev/null
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2005-2010 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 org.apache.wicket.markup.html.CSSPackageResource;
+import org.apache.wicket.markup.html.WebPage;
+import org.apache.wicket.markup.html.basic.Label;
+import org.apache.wicket.markup.html.link.Link;
+import org.apache.wicket.markup.html.panel.FeedbackPanel;
+import org.apache.wicket.model.IModel;
+import org.wamblee.wicket.behavior.TitleAttributeTooltipBehavior;
+import org.wamblee.wicket.css.ResetCssBehavior;
+import org.wamblee.wicket.page.ExpireBehavior;
+import org.wamblee.wicket.page.WebApplicationBasePage;
+
+public class BasePage extends WebApplicationBasePage {
+
+    private boolean isExpired = false;
+
+    public BasePage() {
+        this(null);
+    }
+
+    public BasePage(IModel aModel) {
+        super(aModel);
+        add(new ResetCssBehavior());
+        add(new TitleAttributeTooltipBehavior());
+        add(CSSPackageResource.getHeaderContribution(BasePage.class,
+            "photos.css"));
+        disableCaching();
+
+        add(new FeedbackPanel("feedback"));
+        add(new Label("title", getTitle()));
+
+        addBehavior(new ExpireBehavior() {
+            @Override
+            protected boolean isExpired(WebPage aPage) {
+                return isExpired;
+            }
+        });
+
+        add(new Link("logout") {
+            @Override
+            public void onClick() {
+                getRequestCycle().getSession().invalidate();
+            }
+        });
+    }
+
+    public void setExpired(boolean aExpired) {
+        isExpired = aExpired;
+    }
+
+    private String getTitle() {
+        String name = getClass().getSimpleName();
+        name = name.replaceAll("([A-Z]+)([A-Z][a-z])", "$1 $2");
+        name = name.replaceAll("[A-Z]+", " $0");
+        name = name.replaceAll("^ ", "");
+        name = name.replaceAll("  ", " ");
+        return name;
+    }
+}
diff --git a/src/main/java/org/wamblee/photos/wicket/ErrorPage.html b/src/main/java/org/wamblee/photos/wicket/ErrorPage.html
new file mode 100644 (file)
index 0000000..b767bf1
--- /dev/null
@@ -0,0 +1,18 @@
+<html
+       xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.4-strict.dtd">
+<head>
+<title>Wicket Quickstart Archetype Homepage</title>
+</head>
+<body>
+<strong>Wicket Quickstart Archetype Homepage</strong>
+<br />
+<br />
+
+<wicket:extend>
+       
+       <h1>The page could not be displayed</h1>
+       
+</wicket:extend>
+
+</body>
+</html>
diff --git a/src/main/java/org/wamblee/photos/wicket/ErrorPage.java b/src/main/java/org/wamblee/photos/wicket/ErrorPage.java
new file mode 100644 (file)
index 0000000..c60d418
--- /dev/null
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2005-2010 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 org.apache.wicket.PageParameters;
+
+/**
+ * Homepage
+ */
+public class ErrorPage extends BasePage {
+
+    private static final long serialVersionUID = 1L;
+    
+    /**
+     * Constructor that is invoked when page is invoked without a session.
+     * 
+     * @param parameters
+     *            Page parameters
+     */
+    public ErrorPage(final PageParameters parameters) throws Exception {
+        super();
+    }
+}
diff --git a/src/main/java/org/wamblee/photos/wicket/HomePage.html b/src/main/java/org/wamblee/photos/wicket/HomePage.html
new file mode 100644 (file)
index 0000000..2e7ccfe
--- /dev/null
@@ -0,0 +1,18 @@
+<html
+       xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.4-strict.dtd">
+<head>
+<title>Wicket Quickstart Archetype Homepage</title>
+</head>
+<body>
+<strong>Wicket Quickstart Archetype Homepage</strong>
+<br />
+<br />
+
+<wicket:extend>
+       
+       <span wicket:id="message">Message here.</span>
+
+</wicket:extend>
+
+</body>
+</html>
diff --git a/src/main/java/org/wamblee/photos/wicket/HomePage.java b/src/main/java/org/wamblee/photos/wicket/HomePage.java
new file mode 100644 (file)
index 0000000..a6126b7
--- /dev/null
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2005-2010 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.util.List;
+
+import javax.inject.Inject;
+
+import org.apache.wicket.PageParameters;
+import org.apache.wicket.markup.html.basic.Label;
+import org.wamblee.photos.model.Album;
+import org.wamblee.photos.model.PhotoEntry;
+import org.wamblee.photos.model.plumbing.AllPhotos;
+import org.wamblee.security.authentication.User;
+import org.wamblee.security.authentication.UserAdministration;
+
+/**
+ * Homepage
+ */
+public class HomePage extends BasePage {
+
+    private static final long serialVersionUID = 1L;
+
+    @Inject
+    private User user;
+
+    @Inject
+    private UserAdministration userAdmin;
+
+    @Inject
+    @AllPhotos
+    private Album album;
+
+    /**
+     * Constructor that is invoked when page is invoked without a session.
+     * 
+     * @param parameters
+     *            Page parameters
+     */
+    public HomePage(final PageParameters parameters) throws Exception {
+        super();
+        add(new Label("message", "Hello world!"));
+
+        System.out.println("Currently logged in user: " + user);
+
+        List<String> usernames = userAdmin.getUsers();
+        System.out.println("All user names: " + usernames);
+
+        usernames = userAdmin.getUsers("public");
+        System.out.println("Users in group public: " + usernames);
+
+        System.out.println("Entries: " + album.size());
+        for (int i = 0; i < album.size(); i++) {
+            PhotoEntry entry = album.getEntry(i);
+            System.out.println("Entry " + i + " " + entry.getId() + " " +
+                entry.getPath());
+        }
+    }
+}
diff --git a/src/main/java/org/wamblee/photos/wicket/WicketApplication.java b/src/main/java/org/wamblee/photos/wicket/WicketApplication.java
new file mode 100644 (file)
index 0000000..8339000
--- /dev/null
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2005-2010 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 javax.servlet.http.HttpServletRequest;
+
+import org.apache.wicket.Request;
+import org.apache.wicket.RequestCycle;
+import org.apache.wicket.Response;
+import org.apache.wicket.protocol.http.WebApplication;
+import org.apache.wicket.protocol.http.WebRequest;
+import org.apache.wicket.request.target.basic.RedirectRequestTarget;
+import org.apache.wicket.settings.IApplicationSettings;
+import org.wamblee.wicket.inject.ComponentInstantiationInjector;
+import org.wamblee.wicket.transactions.OpenTransactionInViewRequestCycle;
+
+/**
+ * Application object for your web application. If you want to run this
+ * application without deploying, run the Start class.
+ * 
+ * @see org.wamblee.Start#main(String[])
+ */
+public class WicketApplication extends WebApplication {
+
+    /**
+     * Constructor
+     */
+    public WicketApplication() {
+        // Empty.
+    }
+
+    @Override
+    public RequestCycle newRequestCycle(Request aRequest, Response aResponse) {
+        return new OpenTransactionInViewRequestCycle(this,
+            (WebRequest) aRequest, aResponse);
+    }
+
+    @Override
+    protected void init() {
+        super.init();
+        addComponentInstantiationListener(new ComponentInstantiationInjector());
+
+        IApplicationSettings settings = getApplicationSettings();
+        settings.setInternalErrorPage(ErrorPage.class);
+
+        // Use the lines below to get the internal error page also when in
+        // development mode.
+        // IExceptionSettings exs = getExceptionSettings();
+        // exs.setUnexpectedExceptionDisplay(IExceptionSettings.SHOW_INTERNAL_ERROR_PAGE);
+    }
+
+    /**
+     * @see org.apache.wicket.Application#getHomePage()
+     */
+    public Class<HomePage> getHomePage() {
+        return HomePage.class;
+    }
+
+}
diff --git a/src/main/java/org/wamblee/photos/wicket/banner.png b/src/main/java/org/wamblee/photos/wicket/banner.png
new file mode 100644 (file)
index 0000000..1098af2
Binary files /dev/null and b/src/main/java/org/wamblee/photos/wicket/banner.png differ
diff --git a/src/main/java/org/wamblee/photos/wicket/photos-print.css b/src/main/java/org/wamblee/photos/wicket/photos-print.css
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/main/java/org/wamblee/photos/wicket/photos.css b/src/main/java/org/wamblee/photos/wicket/photos.css
new file mode 100644 (file)
index 0000000..eaa78ec
--- /dev/null
@@ -0,0 +1,110 @@
+/* general */
+body {
+    font-family: Arial, Helvetica, sans-serif;
+    list-style-type: square;
+    font-size: 20px; 
+}
+/* menu styling */
+
+#menu {
+    font-size: 20px;
+    clear: both;
+}
+
+#menu ul {
+    list-style: none;
+    margin: 0;
+    padding: 0;
+    padding-top: 1em;
+}
+
+#menu li {
+    display: inline;
+    background: black;
+    padding: 4px 10px 5px 10px;
+    color: white;
+}
+
+#menu li a {
+    color: #82cafa;
+    text-decoration: none;
+}
+/* banner */
+
+#banner {
+    background: url(banner.png);
+    height: 150px;
+    width: 100%;
+}
+
+#banner img {
+    float: left;
+    margin: 0;
+    border: medium none;
+}
+
+#banner .title {
+    display: block;
+    font-size: 16px;
+    position: absolute;
+    text-align: center;
+    top: 135px;
+    width: 178px;
+}
+
+#logout {
+    display: block;
+    position: absolute;
+    top: 20px;
+    left: 220px;
+}
+
+/* feedback panel */
+
+ul.feedbackPanel {
+    color: red;
+}
+/* the contents on the main page */
+
+#content {
+    clear: both;
+    margin-top: 1em;
+}
+
+#content em {
+    display: block;
+    margin-bottom: 0px;
+    font-style: normal;
+    font-weight: bold;
+    font-size: 24px;
+}
+
+#content table {
+    text-align: left;
+}
+
+#content th, #content td {
+    vertical-align: top;
+    padding: 10px;
+}
+
+.entries a {
+    display: block;
+    float: left;
+    font-size: 24px;
+    margin-left: 10%;
+    margin-right: 24px;
+    line-height: 34px;
+    width: 100%;
+}
+
+.entries .index {
+    display: block;
+    float: left;
+    font-size: 30px;
+    width: 100%;
+    margin-top: 10px;
+}
+
+
+
diff --git a/src/main/java/org/wamblee/photos/wicket/wamblee_logo.png b/src/main/java/org/wamblee/photos/wicket/wamblee_logo.png
new file mode 100644 (file)
index 0000000..6407ed9
Binary files /dev/null and b/src/main/java/org/wamblee/photos/wicket/wamblee_logo.png differ
diff --git a/src/main/webapp/WEB-INF/beans.xml b/src/main/webapp/WEB-INF/beans.xml
new file mode 100644 (file)
index 0000000..82ec09d
--- /dev/null
@@ -0,0 +1,5 @@
+<beans xmlns="http://java.sun.com/xml/ns/javaee"
+    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/beans_1_0.xsd">
+   
+</beans>
\ No newline at end of file
diff --git a/src/main/webapp/WEB-INF/web.xml b/src/main/webapp/WEB-INF/web.xml
new file mode 100644 (file)
index 0000000..6762478
--- /dev/null
@@ -0,0 +1,98 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+<web-app xmlns="http://java.sun.com/xml/ns/javaee"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
+       version="3.0">
+
+       <display-name>cdi</display-name>
+
+       <!--  
+             There are three means to configure Wickets configuration mode and they are
+             tested in the order given. 
+             1) A system property: -Dwicket.configuration
+             2) servlet specific <init-param>
+             3) context specific <context-param>
+             The value might be either "development" (reloading when templates change)
+             or "deployment". If no configuration is found, "development" is the default.
+       -->
+       
+       <filter>
+               <filter-name>authentication</filter-name>
+               <filter-class>org.wamblee.photos.security.AuthenticationFilter</filter-class>
+               <init-param>
+                       <param-name>loginpage</param-name>
+                       <param-value>/login.jsp</param-value>
+               </init-param>
+               <!-- each authenticated user is assigned to the gruop ALL in the security realm configuration --> 
+               <init-param>
+                       <param-name>role</param-name>
+                       <param-value>ALL</param-value>
+               </init-param>
+               <!-- defines the resource URLs for which no authentication is required --> 
+               <init-param>
+                       <param-name>resources</param-name>
+                       <param-value>/resources</param-value>
+               </init-param>
+       </filter>
+
+       <filter>
+               <filter-name>photos</filter-name>
+               <filter-class>org.apache.wicket.protocol.http.WicketFilter</filter-class>
+               <init-param>
+                       <param-name>applicationClassName</param-name>
+                       <param-value>org.wamblee.photos.wicket.WicketApplication</param-value>
+               </init-param>
+               <init-param>
+                       <param-name>configuration</param-name>
+                       <param-value>development</param-value>
+               </init-param>
+       </filter>
+
+    <filter-mapping>
+       <filter-name>authentication</filter-name>
+       <url-pattern>/*</url-pattern>
+    </filter-mapping>
+       <filter-mapping>
+               <filter-name>photos</filter-name>
+               <url-pattern>/*</url-pattern>
+       </filter-mapping>
+       
+       <security-constraint>
+               <web-resource-collection>
+                       <web-resource-name>resources</web-resource-name>
+                       <url-pattern>/resources/*</url-pattern>
+               </web-resource-collection>
+       </security-constraint>
+       <security-constraint>
+               <web-resource-collection>
+                       <web-resource-name>securedaccess</web-resource-name>
+                       <url-pattern>/*</url-pattern>
+               </web-resource-collection>
+               <auth-constraint>
+                       <role-name>ALL</role-name>
+                       <role-name>users</role-name>
+               </auth-constraint>
+       </security-constraint>
+       
+       <login-config>
+               <auth-method>FORM</auth-method>
+               <realm-name>PhotoXChangeRealm</realm-name>
+               <form-login-config>
+                       <form-login-page>/login.jsp</form-login-page>
+                       <form-error-page>/loginError.jsp</form-error-page>
+               </form-login-config>
+       </login-config>
+       
+       <session-config>
+               <session-timeout>10</session-timeout>
+       </session-config>
+       
+       <welcome-file-list>
+               <welcome-file>login.jsp</welcome-file>
+       </welcome-file-list>
+       
+       <security-role>
+               <role-name>ALL</role-name>
+       </security-role>
+       
+</web-app>
diff --git a/src/main/webapp/login.jsp b/src/main/webapp/login.jsp
new file mode 100644 (file)
index 0000000..e3fbb11
--- /dev/null
@@ -0,0 +1,56 @@
+<%@ page language="java" pageEncoding="UTF-8" session="true"%>
+
+
+
+<%
+    String path = request.getContextPath();
+    String basePath = request.getScheme() + "://" +
+        request.getServerName() + ":" + request.getServerPort() + path +
+        "/";
+%>
+
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+<html>
+
+<head>
+<title wicket:id="title">Home Page</title>
+
+
+<link rel="stylesheet" type="text/css"
+       href="resources/org.wamblee.photos.wicket.BasePage/photos.css" />
+</head>
+
+<body>
+
+       <div id="banner">
+               <wicket:link>
+                       <img
+                               src="resources/org.wamblee.photos.wicket.BasePage/wamblee_logo.png" />
+               </wicket:link>
+               <span class="title">wamblee photos</span>
+       </div>
+
+
+       Please login:
+       <form method="POST" action="j_security_check">
+               <table>
+                       <tr>
+                               <td>Username:</td>
+                               <td><input type="text" name="j_username" />
+                               </td>
+                       </tr>
+                       <tr>
+                               <td>Password:</td>
+                               <td><input type="password" name="j_password" autocomplete="off"/>
+                               </td>
+                       </tr>
+                       <tr>
+                               <td><input type="submit" value="Login" />
+                               </td>
+                               <td></td>
+                       </tr>
+               </table>
+       </form>
+
+</body>
+</html>
diff --git a/src/main/webapp/loginError.jsp b/src/main/webapp/loginError.jsp
new file mode 100644 (file)
index 0000000..0791b49
--- /dev/null
@@ -0,0 +1,17 @@
+
+<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>
+<%
+String path = request.getContextPath();
+String basePath = request.getScheme()+"://"+request.getServerName()+":"+request.getServerPort()+path+"/";
+%>
+
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+<html>
+  <head>
+  </head>
+  
+  <body>
+    Wrong user id or password specified. <br/>
+    <jsp:include page="login.jsp"></jsp:include>
+  </body>
+</html>
diff --git a/src/main/webapp/logout.jsp b/src/main/webapp/logout.jsp
new file mode 100644 (file)
index 0000000..2421ffe
--- /dev/null
@@ -0,0 +1,6 @@
+
+
+<%
+  session.invalidate();
+  response.sendRedirect( "" );
+%>
\ No newline at end of file
diff --git a/todo.txt b/todo.txt
new file mode 100644 (file)
index 0000000..35231be
--- /dev/null
+++ b/todo.txt
@@ -0,0 +1,97 @@
+
+- modify database table names. 
+
+rename USERS to SEC_USER;
+rename table GROUPS to SEC_GROUP;
+rename table USER_GROUPS to SEC_USER_GROUP;
+
+- modify config of flexible jdbc realm. 
+
+  * db query for users 
+  * db query for groups
+  
+- modify database schema.
+
+rename table USER_CONDITIONS to SEC_USER_CONDITION;
+update SEC_USER_CONDITION set TYPE = 'ANYUSER' where TYPE = 'ANY';
+update SEC_USER_CONDITION set TYPE = 'GROUP' where TYPE = 'URL';
+
+alter table SEC_USER_CONDITION change GROUPNAME GRP varchar(255);
+rename table OPERATION_CONDITIONS to SEC_OPERATION_CONDITION;
+alter table SEC_OPERATION_CONDITION change OPERATION CLASSNAME varchar(255);
+rename table PATH_CONDITIONS to SEC_PATH_CONDITION; 
+rename table AUTHORIZATION_RULES to SEC_AUTH_RULE; 
+alter table SEC_AUTH_RULE change RESULT AUTH_RESULT varchar(255);
+alter table SEC_AUTH_RULE change RESOURCE_CLASSNAME RES_CLASSNAME varchar(255);
+
+alter table SEC_AUTH_RULE drop foreign key FK422045712468C452;
+alter table SEC_AUTH_RULE drop foreign key FK42204571FD5B8562;
+alter table SEC_AUTH_RULE drop foreign key FK422045712A7093A2;
+
+alter table SEC_AUTH_RULE change USERCONDITION_ID USER_COND_PK bigint(20);
+alter table SEC_AUTH_RULE change OPERATIONCONDITION_ID OPER_COND_PK bigint(20);
+alter table SEC_AUTH_RULE change PATHCONDITION_ID PATH_COND_PK bigint(20);
+
+alter table SEC_AUTH_RULE drop key FK422045712468C452;
+alter table SEC_AUTH_RULE drop key FK42204571FD5B8562;
+alter table SEC_AUTH_RULE drop key FK422045712A7093A2;
+
+alter table SEC_AUTH_RULE add foreign key USER_COND_KEY(USER_COND_PK) references SEC_USER_CONDITION (ID);
+alter table SEC_AUTH_RULE add foreign key OPER_COND_KEY(OPER_COND_PK) references SEC_OPERATION_CONDITION (ID);
+alter table SEC_AUTH_RULE add foreign key PATH_COND_KEY(PATH_COND_PK) references SEC_PATH_CONDITION (ID);
+
+update SEC_AUTH_RULE set RES_CLASSNAME = 'org.wamblee.photos.wicket.BasePage' where RES_CLASSNAME like '%tapestry%';
+
+rename table AUTHORIZATION_SERVICE to SEC_AUTH_SVC;
+rename table AUTHORIZATION_SERVICE to SEC_AUTH_SVC;
+
+alter table SEC_AUTH_SVC_RULE drop foreign key FK7DFDBD476CC274C0;
+alter table SEC_AUTH_SVC_RULE drop key FK7DFDBD476CC274C0;
+alter table SEC_AUTH_SVC_RULE drop foreign key FK7DFDBD47C0669E1B;
+alter table SEC_AUTH_SVC_RULE drop key FK7DFDBD47C0669E1B;
+
+alter table SEC_AUTH_SVC_RULE change ID SVC_ID bigint(20);
+alter table SEC_AUTH_SVC_RULE change POSITION RULE_INDEX int(11);
+
+alter table SEC_AUTH_SVC_RULE add foreign key SVCKEY(SVC_ID) references SEC_AUTH_SVC(ID);
+alter table SEC_AUTH_SVC_RULE add foreign key RULEKEY(RULE_ID) references SEC_AUTH_RULE(ID);
+
+
+
+=======================
+
+| SEC_AUTH_RULE | CREATE TABLE `SEC_AUTH_RULE` (
+  `ID` bigint(20) NOT NULL AUTO_INCREMENT,
+  `TYPE` varchar(255) NOT NULL DEFAULT '',
+  `VERSION` int(11) NOT NULL DEFAULT '0',
+  `AUTH_RESULT` varchar(255) DEFAULT NULL,
+  `RES_CLASSNAME` varchar(255) DEFAULT NULL,
+  `USERCONDITION_ID` bigint(20) DEFAULT NULL,
+  `PATHCONDITION_ID` bigint(20) DEFAULT NULL,
+  `OPERATIONCONDITION_ID` bigint(20) DEFAULT NULL,
+  PRIMARY KEY (`ID`),
+  KEY `FK422045712468C452` (`OPERATIONCONDITION_ID`),
+  KEY `FK42204571FD5B8562` (`USERCONDITION_ID`),
+  KEY `FK422045712A7093A2` (`PATHCONDITION_ID`),
+  CONSTRAINT `FK422045712468C452` FOREIGN KEY (`OPERATIONCONDITION_ID`) REFERENCES `SEC_OPERATION_CONDITION` (`ID`),
+  CONSTRAINT `FK422045712A7093A2` FOREIGN KEY (`PATHCONDITION_ID`) REFERENCES `SEC_PATH_CONDITION` (`ID`),
+  CONSTRAINT `FK42204571FD5B8562` FOREIGN KEY (`USERCONDITION_ID`) REFERENCES `SEC_USER_CONDITION` (`ID`)
+
+
+show create table SEC_AUTH_SVC_RULE;
++-------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
+| Table             | Create Table                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
++-------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
+| SEC_AUTH_SVC_RULE | CREATE TABLE `SEC_AUTH_SVC_RULE` (
+  `ID` bigint(20) NOT NULL DEFAULT '0',
+  `RULE_ID` bigint(20) NOT NULL DEFAULT '0',
+  `POSITION` int(11) NOT NULL DEFAULT '0',
+  PRIMARY KEY (`ID`,`POSITION`),
+  KEY `FK7DFDBD476CC274C0` (`ID`),
+  KEY `FK7DFDBD47C0669E1B` (`RULE_ID`),
+  CONSTRAINT `FK7DFDBD476CC274C0` FOREIGN KEY (`ID`) REFERENCES `SEC_AUTH_SVC` (`ID`),
+  CONSTRAINT `FK7DFDBD47C0669E1B` FOREIGN KEY (`RULE_ID`) REFERENCES `SEC_AUTH_RULE` (`ID`)
+) ENGINE=InnoDB DEFAULT CHARSET=latin1 |
+
+
+