changeset 531:9cedd170b581 digilibPDF

* PDF generation works now even with subdirectories * genericsification and clean up
author robcast
date Fri, 05 Feb 2010 20:58:38 +0100
parents bd6569a95a3c
children 6d66b4ed7f07
files servlet/src/digilib/auth/AuthOps.java servlet/src/digilib/auth/AuthOpsImpl.java servlet/src/digilib/auth/HashTree.java servlet/src/digilib/auth/XMLAuthOps.java servlet/src/digilib/image/DocuImage.java servlet/src/digilib/image/DocuImageImpl.java servlet/src/digilib/image/ImageLoaderDocuImage.java servlet/src/digilib/image/JAIDocuImage.java servlet/src/digilib/image/JAIImageLoaderDocuImage.java servlet/src/digilib/io/AliasingDocuDirCache.java servlet/src/digilib/io/DocuDirCache.java servlet/src/digilib/io/DocuDirectory.java servlet/src/digilib/io/DocuDirent.java servlet/src/digilib/io/FileOps.java servlet/src/digilib/io/ImageFileset.java servlet/src/digilib/io/MetadataMap.java servlet/src/digilib/io/XMLListLoader.java servlet/src/digilib/io/XMLMetaLoader.java servlet/src/digilib/servlet/DigilibConfiguration.java servlet/src/digilib/servlet/DigilibImageWorker.java servlet/src/digilib/servlet/DigilibInfoReader.java servlet/src/digilib/servlet/DigilibRequest.java servlet/src/digilib/servlet/DocumentBean.java servlet/src/digilib/servlet/ImageJobInformation.java servlet/src/digilib/servlet/Initialiser.java servlet/src/digilib/servlet/PDFCache.java servlet/src/digilib/servlet/PDFJobInformation.java servlet/src/digilib/servlet/PDFMaker.java servlet/src/digilib/servlet/Parameter.java servlet/src/digilib/servlet/ParameterMap.java servlet/src/digilib/servlet/RequestHandler.java servlet/src/digilib/servlet/Scaler.java
diffstat 32 files changed, 5626 insertions(+), 1042 deletions(-) [+]
line wrap: on
line diff
--- a/servlet/src/digilib/auth/AuthOps.java	Thu Jan 14 14:30:30 2010 +0100
+++ b/servlet/src/digilib/auth/AuthOps.java	Fri Feb 05 20:58:38 2010 +0100
@@ -20,33 +20,96 @@
 
 package digilib.auth;
 
+import java.util.List;
 
-import java.io.*;
-import java.util.*;
-import javax.servlet.http.*;
+import javax.servlet.http.HttpServletRequest;
 
+import digilib.servlet.DigilibRequest;
+
+/** Class of operations requiring authentication. */
 public interface AuthOps {
 
-  /**
-   *  check if the request must be authorized to access filepath
-   */
-  public boolean isAuthRequired(String filepath, HttpServletRequest request) throws AuthOpException;
+	/** Test if the request must be authorized to access the filepath.
+	 *
+	 * Information about the user is taken from the ServletRequest.
+	 * @param filepath filepath to be accessed.
+	 * @param request ServletRequest with user information.
+	 * @throws AuthOpException Exception thrown on error.
+	 * @return true if the user request must be authorized.
+	 */
+	public boolean isAuthRequired(String filepath, HttpServletRequest request)
+		throws AuthOpException;
 
-  /**
-   *  check if the request is allowed to access filepath
-   */
-  public boolean isAuthorized(String filepath, HttpServletRequest request) throws AuthOpException;
+	/** Test if the request must be authorized to access the filepath.
+	 *
+	 * Information about the user is taken from the DigilibRequest.
+	 * @param request DigilibRequest with user information.
+	 * @throws AuthOpException Exception thrown on error.
+	 * @return true if the user request must be authorized.
+	 */
+	public boolean isAuthRequired(DigilibRequest request)
+		throws AuthOpException;
+
+	/** Test if the request is allowed to access filepath.
+	 * 
+	 * @param filepath filepath to be acessed.
+	 * @param request Request with user information.
+	 * @throws AuthOpException Exception thrown on error.
+	 * @return true if the request is allowed.
+	 */
+	public boolean isAuthorized(String filepath, HttpServletRequest request)
+		throws AuthOpException;
+
+	/** Test if the request is allowed to access filepath.
+	 * 
+	 * @param request Request with user information.
+	 * @throws AuthOpException Exception thrown on error.
+	 * @return true if the request is allowed.
+	 */
+	public boolean isAuthorized(DigilibRequest request)
+		throws AuthOpException;
 
-  /**
-   *  return a list of authorization roles needed for request
-   *  to access the specified path
-   *    (does not look at request address for now)
-   */
-  public List rolesForPath(String filepath, HttpServletRequest request) throws AuthOpException;
+	/** Authorization roles needed for request.
+	 *
+	 * Returns the list of authorization roles that are needed to access the
+	 * specified path. No list means the path is free.
+	 *
+	 * The location information of the request is also considered.
+	 *
+	 * @param filepath filepath to be accessed.
+	 * @param request ServletRequest with address information.
+	 * @throws AuthOpException Exception thrown on error.
+	 * @return List of Strings with role names.
+	 */
+	public List<String> rolesForPath(String filepath, HttpServletRequest request)
+		throws AuthOpException;
 
-  /**
-   * check request authorization against a list of roles
-   */
-  public boolean isRoleAuthorized(List roles, HttpServletRequest request);
+	/** Authorization roles needed for request.
+	 *
+	 * Returns the list of authorization roles that are needed to access the
+	 * specified path. No list means the path is free.
+	 *
+	 * The location information of the request is also considered.
+	 *
+	 * @param request DigilibRequest with address information.
+	 * @throws AuthOpException Exception thrown on error.
+	 * @return List of Strings with role names.
+	 */
+	public List<String> rolesForPath(DigilibRequest request)
+		throws AuthOpException;
+
+	/** Test request authorization against a list of roles.
+	 * @param roles List of Strings with role names.
+	 * @param request ServletRequest with address information.
+	 * @return true if the user information in the request authorizes one of the roles.
+	 */
+	public boolean isRoleAuthorized(List<String> roles, HttpServletRequest request);
+
+	/** Test request authorization against a list of roles.
+	 * @param roles List of Strings with role names.
+	 * @param request ServletRequest with address information.
+	 * @return true if the user information in the request authorizes one of the roles.
+	 */
+	public boolean isRoleAuthorized(List<String> roles, DigilibRequest request);
 
 }
--- a/servlet/src/digilib/auth/AuthOpsImpl.java	Thu Jan 14 14:30:30 2010 +0100
+++ b/servlet/src/digilib/auth/AuthOpsImpl.java	Fri Feb 05 20:58:38 2010 +0100
@@ -20,58 +20,112 @@
 
 package digilib.auth;
 
+import java.util.List;
+
 import javax.servlet.http.HttpServletRequest;
-import java.util.*;
+
+import org.apache.log4j.Logger;
 
-import digilib.*;
+import digilib.servlet.DigilibRequest;
 
+/** Basic implementation of AuthOps interface.
+ *
+ * Provides basic implementations. Only rolesForPath needs to be implemented
+ * by specific implementations.
+ */
 public abstract class AuthOpsImpl implements AuthOps {
 
-  protected Utils util;
-
+	/** general logger for this class */
+	protected Logger logger = Logger.getLogger(this.getClass());
+	
+  /** Default constructor. */  
   public AuthOpsImpl() {
-    util = new Utils();
-    try {
-      init();
-    } catch (AuthOpException e) {
-    }
-  }
-
-  public AuthOpsImpl(Utils u) {
-    util = u;
     try {
       init();
     } catch (AuthOpException e) {
     }
   }
 
+
+  /** Test if the request is allowed to access filepath.
+   * @param filepath filepath to be acessed.
+   * @param request Request with user information.
+   * @throws AuthOpException Exception thrown on error.
+   * @return true if the request is allowed.
+   */
   public boolean isAuthRequired(String filepath, HttpServletRequest request) throws AuthOpException {
     // check permissions
-    List rolesRequired = rolesForPath(filepath, request);
+    List<String> rolesRequired = rolesForPath(filepath, request);
     return (rolesRequired != null);
   }
 
+  /**
+   * @see digilib.auth.AuthOps#isAuthRequired(digilib.servlet.DigilibRequest)
+   */
+  public boolean isAuthRequired(DigilibRequest request)
+	  throws AuthOpException {
+		// check permissions
+		List<String> rolesRequired = rolesForPath(request);
+		return (rolesRequired != null);
+  }
+
+  /** Return authorization roles needed for request.
+   *
+   * Returns a list of authorization roles that would be allowed to access the
+   * specified path. The location information of the request is considered also.
+   * @param filepath filepath to be accessed.
+   * @param request ServletRequest with address information.
+   * @throws AuthOpException Exception thrown on error.
+   * @return List of Strings with role names.
+   */
   public boolean isAuthorized(String filepath, HttpServletRequest request) throws AuthOpException {
-    List rolesAllowed = rolesForPath(filepath, request);
+    List<String> rolesAllowed = rolesForPath(filepath, request);
     return isRoleAuthorized(rolesAllowed, request);
   }
 
-  public boolean isRoleAuthorized(List roles, HttpServletRequest request) {
-    ListIterator r = roles.listIterator();
-    String s = "";
-    while (r.hasNext()) {
-      s = (String)r.next();
-      util.dprintln(5, "Testing role: "+s);
+  /**
+   * @see digilib.auth.AuthOps#isAuthorized(digilib.servlet.DigilibRequest)
+   */
+  public boolean isAuthorized(DigilibRequest request)
+	  throws AuthOpException {
+		List<String> rolesAllowed = rolesForPath(request);
+		return isRoleAuthorized(rolesAllowed, request);
+  }
+
+  /** Test request authorization against a list of roles.
+   * @param roles List of Strings with role names.
+   * @param request ServletRequest with address information.
+   * @return true if the user information in the request authorizes one of the roles.
+   */
+  public boolean isRoleAuthorized(List<String> roles, HttpServletRequest request) {
+    for (String s: roles) {
+      logger.debug("Testing role: "+s);
       if (request.isUserInRole(s)) {
-        util.dprintln(5, "Role Authorized");
+      	logger.debug("Role Authorized");
         return true;
       }
     }
     return false;
   }
 
+  /**
+   * @see digilib.auth.AuthOps#isRoleAuthorized(java.util.List, digilib.servlet.DigilibRequest)
+   */
+  public boolean isRoleAuthorized(List<String> roles, DigilibRequest request) {
+	for (String s: roles) {
+	  logger.debug("Testing role: "+s);
+	  if (((HttpServletRequest)request.getServletRequest()).isUserInRole(s)) {
+	  	logger.debug("Role Authorized");
+		return true;
+	  }
+	}
+	return false;
+  }
+
   public abstract void init() throws AuthOpException;
 
-  public abstract List rolesForPath(String filepath, HttpServletRequest request) throws AuthOpException;
+  public abstract List<String> rolesForPath(String filepath, HttpServletRequest request) throws AuthOpException;
+
+  public abstract List<String> rolesForPath(DigilibRequest request) throws AuthOpException;
 
 }
--- a/servlet/src/digilib/auth/HashTree.java	Thu Jan 14 14:30:30 2010 +0100
+++ b/servlet/src/digilib/auth/HashTree.java	Fri Feb 05 20:58:38 2010 +0100
@@ -1,80 +1,113 @@
 /*  HashTree -- Tree in a Hashtable
 
-  Digital Image Library servlet components
+ Digital Image Library servlet components
 
-  Copyright (C) 2001, 2002 Robert Casties (robcast@mail.berlios.de)
+ Copyright (C) 2001, 2002 Robert Casties (robcast@mail.berlios.de)
 
-  This program is free software; you can redistribute  it and/or modify it
-  under  the terms of  the GNU General  Public License as published by the
-  Free Software Foundation;  either version 2 of the  License, or (at your
-  option) any later version.
-   
-  Please read license.txt for the full details. A copy of the GPL
-  may be found at http://www.gnu.org/copyleft/lgpl.html
+ This program is free software; you can redistribute  it and/or modify it
+ under  the terms of  the GNU General  Public License as published by the
+ Free Software Foundation;  either version 2 of the  License, or (at your
+ option) any later version.
+ 
+ Please read license.txt for the full details. A copy of the GPL
+ may be found at http://www.gnu.org/copyleft/lgpl.html
 
-  You should have received a copy of the GNU General Public License
-  along with this program; if not, write to the Free Software
-  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ You should have received a copy of the GNU General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 
-*/
+ */
 
 package digilib.auth;
 
-import java.util.*;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.StringTokenizer;
 
+/**
+ * Tree representation wrapper for a HashMap.
+ * 
+ * The HashTree is constructed from a HashMap filled with 'branches' with
+ * 'leaves'. The branches are stored as String keys in the HashMap. The String
+ * values are leaves.
+ * 
+ * Branches are matched in 'twigs' separated by 'twig separator' Strings. The
+ * return values for a match are leaf values separated by 'leaf separator'
+ * Strings.
+ * 
+ * @author casties
+ */
 public class HashTree {
 
-  private Hashtable table;
-  private String twigSep = "/";
-  private String leafSep = ",";
+    private Map<String, String> table;
 
-  public HashTree() {
-    table = new Hashtable();
-  }
+    private String twigSep = "/";
+
+    private String leafSep = ",";
 
-  public HashTree(Hashtable t, String twig_separator, String leaf_separator) {
-    table = t;
-    twigSep = twig_separator;
-    leafSep = leaf_separator;
-    optimizeTable();
-  }
+    /**
+     * Constructor of a HashTree.
+     * 
+     * Creates a HashTree wrapper around a given HashMap, using the given twig
+     * separator and leaf separator.
+     * 
+     * @param t
+     * @param twig_separator
+     * @param leaf_separator
+     */
+    public HashTree(Map<String, String> t, String twig_separator, String leaf_separator) {
+        table = t;
+        twigSep = twig_separator;
+        leafSep = leaf_separator;
+        optimizeTable();
+    }
 
-  void optimizeTable() {
-  }
-
-  List match(String branch) {
-    String b = "";
-    String m;
-    LinkedList matches = new LinkedList();
+    void optimizeTable() {
+    }
 
-    // split branch
-    StringTokenizer twig = new StringTokenizer(branch, twigSep);
-    // walk branch and check with tree
-    while (twig.hasMoreTokens()) {
-      if (b.length() == 0) {
-        b = twig.nextToken();
-      } else {
-        b += twigSep + twig.nextToken();
-      }
-      m = (String)table.get(b);
-      //System.out.println("CHECK: "+b+" = "+m);
-      if (m != null) {
-        if (m.indexOf(leafSep) < 0) {
-          // single leaf
-          matches.add(m);
+    /**
+     * Matches the given branch against the HashTree.
+     * 
+     * Returns a LinkedList of all leaves on all matching branches in the tree.
+     * Branches in the tree match if they are substrings starting at the same
+     * root.
+     * 
+     * @param branch
+     * @return
+     */
+    List<String> match(String branch) {
+        String b = "";
+        String m;
+        LinkedList<String> matches = new LinkedList<String>();
+
+        // split branch
+        StringTokenizer twig = new StringTokenizer(branch, twigSep);
+        // walk branch and check with tree
+        while (twig.hasMoreTokens()) {
+            if (b.length() == 0) {
+                b = twig.nextToken();
+            } else {
+                b += twigSep + twig.nextToken();
+            }
+            m = table.get(b);
+            if (m != null) {
+                if (m.indexOf(leafSep) < 0) {
+                    // single leaf
+                    matches.add(m);
+                } else {
+                    // split leaves
+                    StringTokenizer leaf = new StringTokenizer(m, leafSep);
+                    while (leaf.hasMoreTokens()) {
+                        matches.add(leaf.nextToken());
+                    }
+                }
+            }
+        }
+        if (matches.size() > 0) {
+            return matches;
         } else {
-          // split leaves
-          StringTokenizer leaf = new StringTokenizer(m, leafSep);
-          while (leaf.hasMoreTokens()) {
-            matches.add(leaf.nextToken());
-          }
+            return null;
         }
-      }
     }
-    if (matches.size() > 0) {
-      return matches;
-    } else {
-      return null;
-    }
-  }
 }
--- a/servlet/src/digilib/auth/XMLAuthOps.java	Thu Jan 14 14:30:30 2010 +0100
+++ b/servlet/src/digilib/auth/XMLAuthOps.java	Fri Feb 05 20:58:38 2010 +0100
@@ -20,78 +20,148 @@
 
 package digilib.auth;
 
+import java.io.File;
+import java.util.List;
+import java.util.Map;
+
 import javax.servlet.http.HttpServletRequest;
-import java.util.*;
-import java.io.*;
 
-import digilib.*;
-import digilib.io.*;
+import digilib.io.XMLListLoader;
+import digilib.servlet.DigilibRequest;
 
-
+/** Implementation of AuthOps using XML files.
+ *
+ * The configuration file is read by an XMLListLoader into HashTree objects for 
+ * authentication paths and IP numbers.
+ */
 public class XMLAuthOps extends AuthOpsImpl {
 
-  private String configFile = "/docuserver/www/digitallibrary/WEB-INF/digilib-auth.xml";
-  private HashTree authPaths;
-  private HashTree authIPs;
+	private File configFile;
+	private HashTree authPaths;
+	private HashTree authIPs;
+
+	/** Constructor taking an XML config file.
+	 *
+	 * @param u utils object
+	 * @param confFile Configuration file.
+	 * @throws AuthOpException Exception thrown on error.
+	 */
+	public XMLAuthOps(File confFile) throws AuthOpException {
+		configFile = confFile;
+		init();
+	}
+
+	/** Set configuration file.
+	 *
+	 * @param confFile XML config file.
+	 * @throws AuthOpException Exception thrown on error.
+	 */
+	public void setConfig(File confFile) throws AuthOpException {
+		configFile = confFile;
+		init();
+	}
 
-  public XMLAuthOps(Utils u, String confFile) throws AuthOpException {
-    util = u;
-    configFile = confFile;
-    init();
-  }
-
-  public void setConfig(String confFile) throws AuthOpException {
-    configFile = confFile;
-    init();
-  }
+	/** Initialize.
+	 *
+	 * Read configuration files and setup authentication arrays.
+	 *
+	 * @throws AuthOpException Exception thrown on error.
+	 */
+	public void init() throws AuthOpException {
+		logger.debug("xmlauthops.init (" + configFile + ")");
+		Map<String, String> pathList = null;
+		Map<String, String> ipList = null;
+		try {
+			// load authPaths
+			XMLListLoader pathLoader =
+				new XMLListLoader("digilib-paths", "path", "name", "role");
+			pathList = pathLoader.loadURL(configFile.toURL().toString());
+			// load authIPs
+			XMLListLoader ipLoader =
+				new XMLListLoader("digilib-addresses", "address", "ip", "role");
+			ipList = ipLoader.loadURL(configFile.toURL().toString());
+		} catch (Exception e) {
+			throw new AuthOpException(
+				"ERROR loading authorization config file: " + e);
+		}
+		if ((pathList == null) || (ipList == null)) {
+			throw new AuthOpException("ERROR unable to load authorization config file!");
+		}
+		// setup path tree
+		authPaths = new HashTree(pathList, "/", ",");
+		// setup ip tree
+		authIPs = new HashTree(ipList, ".", ",");
+	}
 
-  public void init() throws AuthOpException {
-    util.dprintln(10, "xmlauthops.init ("+configFile+")");
-    Hashtable pathList = null;
-    Hashtable ipList = null;
-    try {
-      // create data loader for auth-path file
-      File confFile = new File(configFile);
-      // load authPaths
-      XMLListLoader pathLoader = new XMLListLoader("digilib-paths", "path", "name", "role");
-      pathList = pathLoader.loadURL(confFile.toURL().toString());
-      // load authIPs
-      XMLListLoader ipLoader = new XMLListLoader("digilib-addresses", "address", "ip", "role");
-      ipList = ipLoader.loadURL(confFile.toURL().toString());
-    }
-    catch (Exception e) {
-      throw new AuthOpException("ERROR loading authorization config file: "+e);
-    }
-    if ((pathList == null)||(ipList == null)) {
-      throw new AuthOpException("ERROR unable to load authorization config file!");
-    }
-    // setup path tree
-    authPaths = new HashTree(pathList, "/", ",");
-    // setup ip tree
-    authIPs = new HashTree(ipList, ".", ",");
-  }
+	/** Return authorization roles needed for request.
+	 *
+	 * Returns the list of authorization roles that are needed to access the
+	 * specified path. No list means the path is free.
+	 *
+	 * The location information of the request is also considered.
+	 *
+	 * @param filepath filepath to be accessed.
+	 * @param request ServletRequest with address information.
+	 * @throws AuthOpException Exception thrown on error.
+	 * @return List of Strings with role names.
+	 */
+	public List<String> rolesForPath(String filepath, HttpServletRequest request)
+		throws digilib.auth.AuthOpException {
+		logger.debug("rolesForPath ("
+				+ filepath
+				+ ") by ["
+				+ request.getRemoteAddr()
+				+ "]");
 
-  public List rolesForPath(String filepath, HttpServletRequest request) throws digilib.auth.AuthOpException {
-    util.dprintln(4, "rolesForPath ("+filepath+") by ["+request.getRemoteAddr()+"]");
+		// check if the requests address provides a role
+		List<String> provided = authIPs.match(request.getRemoteAddr());
+		if ((provided != null) && (provided.contains("ALL"))) {
+			// ALL switches off checking;
+			return null;
+		}
+		// which roles are required?
+		List<String> required = authPaths.match(filepath);
+		// do any provided roles match?
+		if ((provided != null) && (required != null)) {
+			for (int i = 0; i < provided.size(); i++) {
+				if (required.contains(provided.get(i))) {
+					// satisfied
+					return null;
+				}
+			}
+		}
+		return required;
+	}
 
-    // check if the requests address provides a role
-    List provided = authIPs.match(request.getRemoteAddr());
-    if ((provided != null)&&(provided.contains("ALL"))) {
-      // ALL switches off checking;
-      return null;
-    }
-    // which roles are required?
-    List required = authPaths.match(filepath);
-    // do any provided roles match?
-    if ((provided != null)&&(required != null)) {
-      for (int i = 0; i < provided.size(); i++) {
-        if (required.contains(provided.get(i))) {
-          // satisfied
-          return null;
-        }
-      }
-    }
-    return required;
-  }
+	/**
+	 * @see digilib.auth.AuthOps#rolesForPath(digilib.servlet.DigilibRequest)
+	 */
+	public List<String> rolesForPath(DigilibRequest request) throws AuthOpException {
+		logger.debug("rolesForPath ("
+				+ request.getFilePath()
+				+ ") by ["
+				+ request.getServletRequest().getRemoteAddr()
+				+ "]");
+
+		// check if the requests address provides a role
+		List<String> provided =
+			authIPs.match(request.getServletRequest().getRemoteAddr());
+		if ((provided != null) && (provided.contains("ALL"))) {
+			// ALL switches off checking;
+			return null;
+		}
+		// which roles are required?
+		List<String> required = authPaths.match(request.getFilePath());
+		// do any provided roles match?
+		if ((provided != null) && (required != null)) {
+			for (int i = 0; i < provided.size(); i++) {
+				if (required.contains(provided.get(i))) {
+					// satisfied
+					return null;
+				}
+			}
+		}
+		return required;
+	}
 
 }
--- a/servlet/src/digilib/image/DocuImage.java	Thu Jan 14 14:30:30 2010 +0100
+++ b/servlet/src/digilib/image/DocuImage.java	Fri Feb 05 20:58:38 2010 +0100
@@ -2,7 +2,7 @@
 
   Digital Image Library servlet components
 
-  Copyright (C) 2001, 2002 Robert Casties (robcast@mail.berlios.de)
+  Copyright (C) 2001, 2002, 2003 Robert Casties (robcast@mail.berlios.de)
 
   This program is free software; you can redistribute  it and/or modify it
   under  the terms of  the GNU General  Public License as published by the
@@ -14,55 +14,211 @@
 
   You should have received a copy of the GNU General Public License
   along with this program; if not, write to the Free Software
-  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 
 */
 
 package digilib.image;
 
-import javax.servlet.*;
-import javax.servlet.http.*;
-import java.io.*;
-import java.util.*;
+import java.awt.Rectangle;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.Iterator;
+
+import digilib.io.ImageFile;
+import digilib.io.FileOpException;
 
-import java.awt.image.*;
-import java.awt.image.renderable.*;
-
-import digilib.*;
-import digilib.io.*;
-
+/** The basic class for the representation of a digilib image.
+ *
+ * The actual image object is hidden in the class, only methods for loading,
+ * manipulation, and saving are exported. This strategy enables implementations
+ * using different toolkits that rely on different image base classes (like
+ * JIMI, Java2D and JAI).
+ */
 public interface DocuImage {
 
-  public String[] getKnownFileTypes();
+	/** Loads an image file into the Object.
+	 * 
+	 * @param f Image File.
+	 * @throws FileOpException Exception thrown if any error occurs.
+	 */
+	public void loadImage(ImageFile f) throws FileOpException;
+
+	/** This DocuImage support the loadSubImage operation.
+	 * 
+	 * @return boolean
+	 */
+	public boolean isSubimageSupported();
 
-  /**
-   *  send an image file as-is
-   */
-  public void sendFile(File f, ServletResponse res) throws FileOpException;
+	/** Load only a subsampled region of the image file.
+	 * 
+	 * @param f
+	 * @param region
+	 * @param subsample
+	 * @throws FileOpException
+	 */
+	public void loadSubimage(ImageFile f, Rectangle region, int subsample)
+		throws FileOpException;
+
+	/** Writes the current image to a ServletResponse.
+	 *
+	 * The image is encoded to the mime-type <code>mt</code> and sent to the output
+	 * stream of the <code>ServletResponse</code> <code>res</code>.
+	 *
+	 * Currently only mime-types "image/jpeg" and "image/png" are supported.
+	 * 
+	 * @param mt mime-type of the image to be sent.
+	 * @param res ServletResponse where the image is sent.
+	 * @throws FileOpException Exception thrown on any error.
+	 */
+	public void writeImage(String mt, OutputStream ostream)
+		throws FileOpException;
 
-  /**
-   *  load image file
-   */
-  public void loadImage(File f) throws FileOpException;
+	/** The width of the current image in pixel.
+	 * 
+	 * @return Image width in pixels.
+	 */
+	public int getWidth();
+
+	/** The height of the current image in pixel.
+	 * 
+	 * @return Image height in pixels.
+	 */
+	public int getHeight();
+
+	/** The mime-type of the current image.
+	 * 
+	 * @return String the mime-type of this image.
+	 */
+	public String getMimetype();
 
-  /**
-   *  write image with mime type mt to Stream
-   */
-  public void writeImage(String mt, ServletResponse res) throws FileOpException;
+	/** Crops the current image.
+	 * 
+	 * Cuts out a region of the size <code>width</code> x <code>height</code> at
+	 * the offset <code>xoff</code>, <code>yoff</code> from the current image
+	 * and replaces the current image with the result.
+	 * 
+	 * @param xoff X offset of crop region
+	 * @param yoff Y offset of crop region
+	 * @param width width of crop region
+	 * @param height height of crop region
+	 * @throws ImageOpException
+	 */
+	public void crop(int xoff, int yoff, int width, int height)
+		throws ImageOpException;
+
+	/** Scales the current image.
+	 * 
+	 * Replaces the current image with an image scaled by the factor
+	 * <code>scale</code>.
+	 * 
+	 * @param scale scaling factor
+	 * @throws ImageOpException
+	 */
+	public void scale(double scaleX, double scaleY) throws ImageOpException;
 
-  /**
-   *  get the image height and width
-   */
-  public int getWidth();
-  public int getHeight();
+	/** Crops and scales the current image.
+	 *
+	 * The current image is cropped to a rectangle of <code>width</code>,
+	 * <code>height</code> at position <code>x_off</code>, <code>y_off</code>. The
+	 * resulting image is scaled by the factor <code>scale</code> using the
+	 * interpolation quality <code>qual</code> (0=worst).
+	 * 
+	 * @param x_off x offset of the crop rectangle in pixel.
+	 * @param y_off y offset of the crop rectangle in pixel.
+	 * @param width width of the crop rectangle in pixel.
+	 * @param height height of the crop rectangle in pixel.
+	 * @param scale scaling factor.
+	 * @param qual interpolation quality (0=worst).
+	 * @throws ImageOpException exception thrown on any error.
+	 */
+	public void cropAndScale(
+		int x_off,
+		int y_off,
+		int width,
+		int height,
+		double scale,
+		int qual)
+		throws ImageOpException;
+
+	/** Rotates the current image.
+	 * 
+	 * Replaces the current image with a rotated image. The image is rotated
+	 * around the center by the <code>angle</code> 
+	 * given in degrees [0, 360] clockwise.
+	 * Image size and aspect ratio are likely to change.
+	 * 
+	 * @param angle rotation angle in degree
+	 */
+	public void rotate(double angle) throws ImageOpException;
+
+	/** Mirrors the current image.
+	 * 
+	 * Replaces  the current image with a mirrored image. The mirror axis goes
+	 * through the center of the image and is rotated by <code>angle</code>
+	 * degrees. Currently only horizontal and vertical mirroring (0 and 90
+	 * degree) are supported. 
+	 * 
+	 * @param angle angle of mirror axis
+	 * @throws ImageOpException
+	 */
+	public void mirror(double angle) throws ImageOpException;
 
-  /**
-   *  crop and scale image
-   *    take rectangle width,height at position x_off,y_off
-   *    and scale by scale with interpolation quality qual (0=worst)
-   */
-  public void cropAndScale(
-                int x_off, int y_off,
-                int width, int height,
-                float scale, int qual) throws ImageOpException;
+	/** Enhances brightness and contrast of the current image.
+	 * 
+	 * Replaces the current image with a brightness and contrast enhanced image.
+	 * Contrast is enhanced by multiplying the pixel value with the constant
+	 * <code>mult</code>. Brightness is enhanced by adding the constant
+	 * <code>add</code> to the pixel value. Operation: p1 = (p0*mult)+add.
+	 * 
+	 * @param mult multiplicative constant for contrast enhancement
+	 * @param add additive constant for brightness enhancement
+	 * @throws ImageOpException
+	 */
+	public void enhance(float mult, float add) throws ImageOpException;
+
+	/** Manipulates the colors of the current image.
+	 * 
+	 * Replaces the current image with a color modified image.
+	 * For the red, green and blue color channels all pixel values are multiplied
+	 * by the constant
+	 * <code>m</code> and added to the constant
+	 * <code>a</code>. Operation: p1 = (p0*m)+a.
+	 * 
+	 * @param rgbm multiplicative constants for red, green, blue
+	 * @param rgba additive constant for red, green, blue
+	 * @throws ImageOpException
+	 */
+	public void enhanceRGB(float[] rgbm, float[] rgba)
+		throws ImageOpException;
+
+	/**
+	 * Returns the interpolation quality.
+	 * @return int
+	 */
+	public int getQuality();
+
+	/**
+	 * Sets the interpolation quality.
+	 * @param quality The quality to set
+	 */
+	public void setQuality(int quality);
+	
+	/** Frees all resources bound to the DocuImage.
+	 * 
+	 * Things that should be freed are image objects and open files.
+	 * 
+	 */
+	public void dispose();
+
+    /**
+     * Check image size and type and store in ImageFile f
+     */
+    public boolean identify(ImageFile imgf) throws IOException;
+
+    /**
+     * Returns a list of supported image formats
+     */
+	public Iterator<String> getSupportedFormats();
+
 }
--- a/servlet/src/digilib/image/DocuImageImpl.java	Thu Jan 14 14:30:30 2010 +0100
+++ b/servlet/src/digilib/image/DocuImageImpl.java	Fri Feb 05 20:58:38 2010 +0100
@@ -2,7 +2,7 @@
 
   Digital Image Library servlet components
 
-  Copyright (C) 2001, 2002 Robert Casties (robcast@mail.berlios.de)
+  Copyright (C) 2001, 2002, 2003 Robert Casties (robcast@mail.berlios.de)
 
   This program is free software; you can redistribute  it and/or modify it
   under  the terms of  the GNU General  Public License as published by the
@@ -14,71 +14,153 @@
 
   You should have received a copy of the GNU General Public License
   along with this program; if not, write to the Free Software
-  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 
 */
 
 package digilib.image;
 
-import java.io.*;
-import javax.servlet.ServletResponse;
+import java.awt.Rectangle;
+import java.io.File;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+
+import org.apache.log4j.Logger;
+import org.marcoschmidt.image.ImageInfo;
 
-import digilib.*;
-import digilib.io.*;
+import digilib.io.FileOpException;
+import digilib.io.ImageFile;
 
+/** Simple abstract implementation of the <code>DocuImage</code> interface.
+ *
+ * This implementation provides basic functionality for the utility methods like
+ * <code>SetUtils</code>, and <code>getKnownFileTypes</code>. Image methods like
+ * <code>loadImage</code>, <code>writeImage</code>, <code>getWidth</code>,
+ * <code>getHeight</code>, <code>crop</code> and <code>scale</code> must be
+ * implemented by derived classes.
+ */
 public abstract class DocuImageImpl implements DocuImage {
 
-  protected Utils util = null;
-
-  public DocuImageImpl() {
-    util = new Utils();
-  }
-
-  public DocuImageImpl(Utils u) {
-    util = u;
-  }
+	/** logger */
+	protected static final Logger logger = Logger.getLogger(DocuImage.class);
+	
+	/** Interpolation quality. */
+	protected int quality = 0;
+	
+	/** epsilon for float comparisons. */
+	public final double epsilon = 1e-5;
+	
+	/** image mime-type */
+	protected String mimeType = null;
 
-  public void setUtils(Utils u) {
-    util = u;
-  }
+	/**
+	 * Returns the quality.
+	 * @return int
+	 */
+	public int getQuality() {
+		return quality;
+	}
 
-  protected String[] knownFileTypes = {"jpg", "png", "gif", "tiff"};
-
-  public String[] getKnownFileTypes() {
-    return knownFileTypes;
-  }
+	/**
+	 * Sets the quality.
+	 * @param quality The quality to set
+	 */
+	public void setQuality(int quality) {
+		this.quality = quality;
+	}
 
-  /**
-   *  send an image file as-is
-   */
-  public void sendFile(File f, ServletResponse response) throws FileOpException {
-    util.dprintln(4, "sendFile("+f+")");
-    String mimeType = FileOps.mimeForFile(f);
-    if (mimeType == null) {
-      util.dprintln(2, "ERROR(sendFile): unknown file Type");
-      throw new FileOpException("Unknown file type.");
+    /** Check image size and type and store in ImageFile f */
+    public boolean identify(ImageFile imgf) throws IOException {
+        // fileset to store the information
+        File f = imgf.getFile();
+        if (f == null) {
+            throw new IOException("File not found!");
+        }
+        RandomAccessFile raf = new RandomAccessFile(f, "r");
+        // set up ImageInfo object
+        ImageInfo iif = new ImageInfo();
+        iif.setInput(raf);
+        iif.setCollectComments(false);
+        iif.setDetermineImageNumber(false);
+        logger.debug("identifying (ImageInfo) " + f);
+        // try with ImageInfo first
+        if (iif.check()) {
+            ImageSize d = new ImageSize(iif.getWidth(), iif.getHeight());
+            imgf.setSize(d);
+            imgf.setMimetype(iif.getMimeType());
+            //logger.debug("  format:"+iif.getFormatName());
+            raf.close();
+            logger.debug("image size: " + imgf.getSize());
+            return true;
+        }
+        return false;
     }
-    response.setContentType(mimeType);
-    // open file
-    try {
-      FileInputStream inFile = new FileInputStream(f);
-      OutputStream outStream = response.getOutputStream();
-      byte dataBuffer[] = new byte[1024];
-      int len;
-      while ((len = inFile.read(dataBuffer)) != -1) {
-        // copy out file
-        outStream.write(dataBuffer, 0, len);
-      }
-      inFile.close();
-    } catch (IOException e) {
-      util.dprintln(2, "ERROR(sendFile): unable to send file");
-      throw new FileOpException("Unable to send file.");
-    }
-  }
+        
+    /** Crop and scale the current image.
+	 *
+	 * The current image is cropped to a rectangle of width, height at position
+	 * x_off, y_off. The resulting image is scaled by the factor scale using the
+	 * interpolation quality qual (0=worst).
+	 * 
+	 * @param x_off X offset of the crop rectangle in pixel.
+	 * @param y_off Y offset of the crop rectangle in pixel.
+	 * @param width Width of the crop rectangle in pixel.
+	 * @param height Height of the crop rectangle in pixel.
+	 * @param scale Scaling factor.
+	 * @param qual Interpolation quality (0=worst).
+	 * @throws ImageOpException Exception thrown on any error.
+	 */
+	public void cropAndScale(
+		int x_off, int y_off, int width, int height, double scale, int qual) 
+		throws ImageOpException {
+
+		setQuality(qual);
+		crop(x_off, y_off, width, height);
+		scale(scale, scale);
+	}
+	
+	public String getMimetype() {
+		return mimeType;
+	}
+
+	public void rotate(double angle) throws ImageOpException {
+		// just a do-nothing implementation
+	}
 
-  public abstract void loadImage(File f) throws FileOpException;
-  public abstract void writeImage(String mt, ServletResponse res) throws FileOpException;
-  public abstract int getWidth();
-  public abstract int getHeight();
-  public abstract void cropAndScale(int x_off, int y_off, int width, int height, float scale, int qual)  throws ImageOpException;
+	public void mirror(double angle) throws ImageOpException {
+		// just a do-nothing implementation
+	}
+
+	public void enhance(float mult, float add) throws ImageOpException {
+		// just a do-nothing implementation
+	}
+
+	public boolean isSubimageSupported() {
+		// partial loading not supported per default
+		return false;
+	}
+
+	public void loadSubimage(ImageFile f, Rectangle region, int subsample)
+		throws FileOpException {
+		// empty implementation
+	}
+
+	public void enhanceRGB(float[] rgbm, float[] rgba)
+		throws ImageOpException {
+		// emtpy implementation
+	}
+
+	public void dispose() {
+		// emtpy implementation
+	}
+
+	public Iterator<String> getSupportedFormats() {
+		List<String> empty = new LinkedList<String>();
+		return empty.iterator();
+	}
+	
+
 }
--- a/servlet/src/digilib/image/ImageLoaderDocuImage.java	Thu Jan 14 14:30:30 2010 +0100
+++ b/servlet/src/digilib/image/ImageLoaderDocuImage.java	Fri Feb 05 20:58:38 2010 +0100
@@ -1,164 +1,570 @@
 /* ImageLoaderDocuImage -- Image class implementation using JDK 1.4 ImageLoader
 
-  Digital Image Library servlet components
+ Digital Image Library servlet components
 
-  Copyright (C) 2001, 2002 Robert Casties (robcast@mail.berlios.de)
+ Copyright (C) 2002, 2003 Robert Casties (robcast@mail.berlios.de)
 
-  This program is free software; you can redistribute  it and/or modify it
-  under  the terms of  the GNU General  Public License as published by the
-  Free Software Foundation;  either version 2 of the  License, or (at your
-  option) any later version.
-   
-  Please read license.txt for the full details. A copy of the GPL
-  may be found at http://www.gnu.org/copyleft/lgpl.html
+ This program is free software; you can redistribute  it and/or modify it
+ under  the terms of  the GNU General  Public License as published by the
+ Free Software Foundation;  either version 2 of the  License, or (at your
+ option) any later version.
+ 
+ Please read license.txt for the full details. A copy of the GPL
+ may be found at http://www.gnu.org/copyleft/lgpl.html
 
-  You should have received a copy of the GNU General Public License
-  along with this program; if not, write to the Free Software
-  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
-
-*/
+ You should have received a copy of the GNU General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
 
 package digilib.image;
 
-import javax.servlet.*;
-import javax.servlet.http.*;
-import java.io.*;
-import java.util.*;
+import java.awt.Image;
+import java.awt.Rectangle;
+import java.awt.RenderingHints;
+import java.awt.geom.AffineTransform;
+import java.awt.geom.Rectangle2D;
+import java.awt.image.AffineTransformOp;
+import java.awt.image.BufferedImage;
+import java.awt.image.ConvolveOp;
+import java.awt.image.Kernel;
+import java.awt.image.RescaleOp;
+import java.io.File;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.RandomAccessFile;
+import java.util.Arrays;
+import java.util.Iterator;
 
-import java.awt.*;
-import java.awt.image.*;
-import java.awt.geom.*;
-import java.awt.image.renderable.*;
+import javax.imageio.IIOImage;
+import javax.imageio.ImageIO;
+import javax.imageio.ImageReadParam;
+import javax.imageio.ImageReader;
+import javax.imageio.ImageWriteParam;
+import javax.imageio.ImageWriter;
+import javax.imageio.stream.FileImageInputStream;
+import javax.imageio.stream.ImageInputStream;
+import javax.imageio.stream.ImageOutputStream;
 
-import javax.imageio.*;
+import digilib.io.FileOpException;
+import digilib.io.FileOps;
+import digilib.io.ImageFile;
+import digilib.io.ImageFileset;
 
-import digilib.*;
-import digilib.io.*;
-
+/** Implementation of DocuImage using the ImageLoader API of Java 1.4 and Java2D. */
 public class ImageLoaderDocuImage extends DocuImageImpl {
 
-  private BufferedImage img;
+	/** image object */
+	protected BufferedImage img;
+
+	/** interpolation type */
+	protected RenderingHints renderHint;
 
-  public ImageLoaderDocuImage() {
-  }
+	/** ImageIO image reader */
+	protected ImageReader reader;
 
-  public ImageLoaderDocuImage(Utils u) {
-    util = u;
-  }
+	/** File that was read */
+	protected File imgFile;
+
+	/* loadSubimage is supported. */
+	public boolean isSubimageSupported() {
+		return true;
+	}
 
-  /**
-   *  load image file
-   */
-  public void loadImage(File f) throws FileOpException {
-    util.dprintln(10, "loadImage!");
-    System.gc();
-    try {
-      for (int i = 0; i < ImageIO.getReaderFormatNames().length; i++) {
-    System.out.println("ImageLoader reader:"+ImageIO.getReaderFormatNames()[i]);
-      }
-      for (int i = 0; i < ImageIO.getWriterFormatNames().length; i++) {
-    System.out.println("ImageLoader writer:"+ImageIO.getWriterFormatNames()[i]);
-      }
-      img = ImageIO.read(f);
-      if (img == null) {
-        util.dprintln(3, "ERROR(loadImage): unable to load file");
-        throw new FileOpException("Unable to load File!");
-      }
-    }
-    catch (IOException e) {
-      throw new FileOpException("Error reading image.");
-    }
-  }
+	public void setQuality(int qual) {
+		quality = qual;
+		renderHint = new RenderingHints(null);
+		// hint.put(RenderingHints.KEY_ANTIALIASING,
+		// RenderingHints.VALUE_ANTIALIAS_OFF);
+		// setup interpolation quality
+		if (qual > 0) {
+			logger.debug("quality q1");
+			renderHint.put(RenderingHints.KEY_INTERPOLATION,
+					RenderingHints.VALUE_INTERPOLATION_BICUBIC);
+		} else {
+			logger.debug("quality q0");
+			renderHint.put(RenderingHints.KEY_INTERPOLATION,
+					RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR);
+		}
+	}
+
+	public int getHeight() {
+		int h = 0;
+		try {
+			if (img == null) {
+				h = reader.getHeight(0);
+			} else {
+				h = img.getHeight();
+			}
+		} catch (IOException e) {
+			logger.debug("error in getHeight", e);
+		}
+		return h;
+	}
 
-  /**
-   *  write image of type mt to Stream
-   */
-  public void writeImage(String mt, ServletResponse res)
-         throws FileOpException {
-    util.dprintln(10, "writeImage!");
-    try {
-      // setup output
-      String type = "png";
-      if (mt == "image/jpeg") {
-        type = "jpeg";
-      } else if (mt == "image/png") {
-        type = "png";
-      } else {
-        // unknown mime type
-        util.dprintln(2, "ERROR(writeImage): Unknown mime type "+mt);
-        throw new FileOpException("Unknown mime type: "+mt);
-      }
-      res.setContentType(mt);
-      // render output
-      if (ImageIO.write(img, type, res.getOutputStream())) {
-        // writing was OK
-        return;
-      } else {
-        throw new FileOpException("Error writing image: Unknown image format!");
-      }
-    } catch (IOException e) {
-      // e.printStackTrace();
-      throw new FileOpException("Error writing image.");
-    }
-  }
+	public int getWidth() {
+		int w = 0;
+		try {
+			if (img == null) {
+				w = reader.getWidth(0);
+			} else {
+				w = img.getWidth();
+			}
+		} catch (IOException e) {
+			logger.debug("error in getHeight", e);
+		}
+		return w;
+	}
+
+	/* returns a list of supported image formats */
+	public Iterator<String> getSupportedFormats() {
+		String[] formats = ImageIO.getReaderFormatNames();
+		return Arrays.asList(formats).iterator();
+	}
 
-  public int getWidth() {
-    if (img != null) {
-      return img.getWidth();
-    }
-    return 0;
-  }
-
-  public int getHeight() {
-    if (img != null) {
-      return img.getHeight();
-    }
-    return 0;
-  }
-
-  /**
-   *  crop and scale image
-   *    take rectangle width,height at position x_off,y_off
-   *    and scale by scale
-   */
-   public void cropAndScale(int x_off, int y_off, int width, int height,
-         float scale, int qual) throws ImageOpException {
-    util.dprintln(10, "cropAndScale!");
-
-    int scaleInt = 0;
-    // setup interpolation quality
-    if (qual > 0) {
-      util.dprintln(4, "quality q1");
-      scaleInt = AffineTransformOp.TYPE_BILINEAR;
-    } else {
-      util.dprintln(4, "quality q0");
-      scaleInt = AffineTransformOp.TYPE_NEAREST_NEIGHBOR;
+    /** Check image size and type and store in ImageFile f */
+    public boolean identify(ImageFile imgf) throws IOException {
+        // try parent method first
+        if (super.identify(imgf)) {
+            return true;
+        }
+        // fileset to store the information
+        ImageFileset imgfs = imgf.getParent();
+        File f = imgf.getFile();
+        if (f == null) {
+            throw new IOException("File not found!");
+        }
+        logger.debug("identifying (ImageIO) " + f);
+        /*
+         * try ImageReader
+         */
+        RandomAccessFile raf = new RandomAccessFile(f, "r");
+        ImageInputStream istream = ImageIO.createImageInputStream(raf);
+        Iterator<ImageReader> readers = ImageIO.getImageReaders(istream);
+        if (readers.hasNext()) {
+            ImageReader reader = readers.next();
+            /* are there more readers? */
+            logger.debug("ImageIO: this reader: " + reader.getClass());
+            while (readers.hasNext()) {
+                logger.debug("ImageIO: next reader: "
+                        + readers.next().getClass());
+            }
+            try {
+                reader.setInput(istream);
+                ImageSize d = new ImageSize(reader.getWidth(0), reader.getHeight(0));
+                imgf.setSize(d);
+                //String t = reader.getFormatName();
+                String t = FileOps.mimeForFile(f);
+                imgf.setMimetype(t);
+                //logger.debug("  format:"+t);
+                if (imgfs != null) {
+                    imgfs.setAspect(d);
+                }
+                return true;
+            } finally {
+                // dispose the reader to free resources
+                reader.dispose();
+                raf.close();
+            }
+        }
+        throw new FileOpException("ERROR: unknown image file format!");
     }
 
-    // setup Crop
-    BufferedImage croppedImg = img.getSubimage(x_off, y_off, width, height);
+    
+    /* load image file */
+	public void loadImage(ImageFile f) throws FileOpException {
+		logger.debug("loadImage " + f.getFile());
+		try {
+			img = ImageIO.read(f.getFile());
+			if (img == null) {
+				throw new FileOpException("Unable to load File!");
+			}
+		} catch (IOException e) {
+			throw new FileOpException("Error reading image.");
+		}
+	}
+
+	/**
+	 * Get an ImageReader for the image file.
+	 * 
+	 * @return
+	 */
+	public ImageReader getReader(ImageFile f) throws IOException {
+		logger.debug("preloadImage " + f.getFile());
+		if (reader != null) {
+			logger.debug("Reader was not null!");
+			// clean up old reader
+			dispose();
+		}
+		// System.gc();
+		RandomAccessFile rf = new RandomAccessFile(f.getFile(), "r");
+		ImageInputStream istream = new FileImageInputStream(rf);
+		// Iterator readers = ImageIO.getImageReaders(istream);
+		String mt = f.getMimetype();
+		logger.debug("File type:" + mt);
+		Iterator<ImageReader> readers = ImageIO.getImageReadersByMIMEType(mt);
+		if (!readers.hasNext()) {
+			throw new FileOpException("Unable to load File!");
+		}
+		reader = readers.next();
+		/* are there more readers? */
+		logger.debug("ImageIO: this reader: " + reader.getClass());
+		while (readers.hasNext()) {
+			logger.debug("ImageIO: next reader: " + readers.next().getClass());
+		}
+		// */
+		reader.setInput(istream);
+		imgFile = f.getFile();
+		return reader;
+	}
+
+	/* Load an image file into the Object. */
+	public void loadSubimage(ImageFile f, Rectangle region, int prescale)
+			throws FileOpException {
+		logger.debug("loadSubimage");
+		// System.gc();
+		try {
+			if ((reader == null) || (imgFile != f.getFile())) {
+				getReader(f);
+			}
+			// set up reader parameters
+			ImageReadParam readParam = reader.getDefaultReadParam();
+			readParam.setSourceRegion(region);
+			if (prescale > 1) {
+				readParam.setSourceSubsampling(prescale, prescale, 0, 0);
+			}
+			// read image
+			logger.debug("loading..");
+			img = reader.read(0, readParam);
+			logger.debug("loaded");
+		} catch (IOException e) {
+			throw new FileOpException("Unable to load File!");
+		}
+		if (img == null) {
+			throw new FileOpException("Unable to load File!");
+		}
+	}
 
-    img = null; // free img
-    util.dprintln(3, "CROP:"+croppedImg.getWidth()+"x"+croppedImg.getHeight()); //DEBUG
-//    util.dprintln(2, "  time "+(System.currentTimeMillis()-startTime)+"ms");
+	/* write image of type mt to Stream */
+	public void writeImage(String mt, OutputStream ostream)
+			throws FileOpException {
+		logger.debug("writeImage");
+		// setup output
+		ImageWriter writer = null;
+		ImageOutputStream imgout = null;
+		try {
+			imgout = ImageIO.createImageOutputStream(ostream);
+			if (mt == "image/jpeg") {
+				/*
+				 * JPEG doesn't do transparency so we have to convert any RGBA
+				 * image to RGB :-( *Java2D BUG*
+				 */
+				if (img.getColorModel().hasAlpha()) {
+					logger.debug("BARF: JPEG with transparency!!");
+					int w = img.getWidth();
+					int h = img.getHeight();
+					// BufferedImage.TYPE_INT_RGB seems to be fastest (JDK1.4.1,
+					// OSX)
+					int destType = BufferedImage.TYPE_INT_RGB;
+					BufferedImage img2 = new BufferedImage(w, h, destType);
+					img2.createGraphics().drawImage(img, null, 0, 0);
+					img = img2;
+				}
+				writer = (ImageWriter) ImageIO.getImageWritersByFormatName(
+						"jpeg").next();
+				if (writer == null) {
+					throw new FileOpException("Unable to get JPEG writer");
+				}
+				ImageWriteParam param = writer.getDefaultWriteParam();
+				if (quality > 1) {
+					// change JPEG compression quality
+					param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
+					//logger.debug("JPEG qual before: "
+					//		+ Float.toString(param.getCompressionQuality()));
+					param.setCompressionQuality(0.9f);
+					//logger.debug("JPEG qual now: "
+					//		+ Float.toString(param.getCompressionQuality()));
+				}
+				writer.setOutput(imgout);
+				// render output
+				logger.debug("writing");
+				writer.write(null, new IIOImage(img, null, null), param);
+			} else if (mt == "image/png") {
+				// render output
+				writer = (ImageWriter) ImageIO.getImageWritersByFormatName(
+						"png").next();
+				if (writer == null) {
+					throw new FileOpException("Unable to get PNG writer");
+				}
+				writer.setOutput(imgout);
+				logger.debug("writing");
+				writer.write(img);
+			} else {
+				// unknown mime type
+				throw new FileOpException("Unknown mime type: " + mt);
+			}
 
-    if (croppedImg == null) {
-      util.dprintln(2, "ERROR(cropAndScale): error in crop");
-      throw new ImageOpException("Unable to crop");
-    }
+		} catch (IOException e) {
+			throw new FileOpException("Error writing image.");
+		} finally {
+			// clean up
+			if (writer != null) {
+				writer.dispose();
+			}
+		}
+	}
+
+	public void scale(double scale, double scaleY) throws ImageOpException {
+		logger.debug("scale");
+		/* for downscaling in high quality the image is blurred first */
+		if ((scale <= 0.5) && (quality > 1)) {
+			int bl = (int) Math.floor(1 / scale);
+			blur(bl);
+		}
+		/* then scaled */
+		AffineTransformOp scaleOp = new AffineTransformOp(AffineTransform
+				.getScaleInstance(scale, scale), renderHint);
+		BufferedImage scaledImg = null;
+		// enforce destination image type (*Java2D BUG*)
+		int type = img.getType();
+		// FIXME: which type would be best?
+		if ((quality > 0) && (type != 0)) {
+			logger.debug("creating destination image");
+			Rectangle2D dstBounds = scaleOp.getBounds2D(img);
+			scaledImg = new BufferedImage((int) dstBounds.getWidth(),
+					(int) dstBounds.getHeight(), type);
+		}
+		logger.debug("scaling...");
+		scaledImg = scaleOp.filter(img, scaledImg);
+		if (scaledImg == null) {
+			throw new ImageOpException("Unable to scale");
+		}
+		// DEBUG
+		logger.debug("destination image type " + scaledImg.getType());
+		logger.debug("SCALE: " + scale + " ->" + scaledImg.getWidth() + "x"
+				+ scaledImg.getHeight());
+		img = scaledImg;
+		scaledImg = null;
+	}
 
-    // setup scale
-    AffineTransformOp scaleOp = new AffineTransformOp(
-                      AffineTransform.getScaleInstance(scale, scale),
-                      scaleInt);
-    BufferedImage scaledImg = scaleOp.filter(croppedImg, null);
-    croppedImg = null; // free opCrop
+	public void blur(int radius) throws ImageOpException {
+		// DEBUG
+		logger.debug("blur: " + radius);
+		// minimum radius is 2
+		int klen = Math.max(radius, 2);
+		// FIXME: use constant kernels for most common sizes
+		int ksize = klen * klen;
+		// kernel is constant 1/k
+		float f = 1f / ksize;
+		float[] kern = new float[ksize];
+		for (int i = 0; i < ksize; i++) {
+			kern[i] = f;
+		}
+		Kernel blur = new Kernel(klen, klen, kern);
+		// blur with convolve operation
+		ConvolveOp blurOp = new ConvolveOp(blur, ConvolveOp.EDGE_NO_OP,
+				renderHint);
+		// blur needs explicit destination image type for color *Java2D BUG*
+		BufferedImage blurredImg = null;
+		if (img.getType() == BufferedImage.TYPE_3BYTE_BGR) {
+			blurredImg = new BufferedImage(img.getWidth(), img.getHeight(), img
+					.getType());
+		}
+		blurredImg = blurOp.filter(img, blurredImg);
+		if (blurredImg == null) {
+			throw new ImageOpException("Unable to scale");
+		}
+		img = blurredImg;
+	}
+
+	public void crop(int x_off, int y_off, int width, int height)
+			throws ImageOpException {
+		// setup Crop
+		BufferedImage croppedImg = img.getSubimage(x_off, y_off, width, height);
+		// DEBUG
+		// util.dprintln(2, " time
+		// "+(System.currentTimeMillis()-startTime)+"ms");
+		if (croppedImg == null) {
+			throw new ImageOpException("Unable to crop");
+		}
+		logger.debug("CROP:" + croppedImg.getWidth() + "x"
+				+ croppedImg.getHeight());
+		img = croppedImg;
+	}
+
+	public void enhance(float mult, float add) throws ImageOpException {
+		/*
+		 * Only one constant should work regardless of the number of bands
+		 * according to the JDK spec. Doesn't work on JDK 1.4 for OSX and Linux
+		 * (at least). RescaleOp scaleOp = new RescaleOp( (float)mult,
+		 * (float)add, null); scaleOp.filter(img, img);
+		 */
+
+		/* The number of constants must match the number of bands in the image. */
+		int ncol = img.getColorModel().getNumComponents();
+		float[] dm = new float[ncol];
+		float[] da = new float[ncol];
+		for (int i = 0; i < ncol; i++) {
+			dm[i] = (float) mult;
+			da[i] = (float) add;
+		}
+		RescaleOp scaleOp = new RescaleOp(dm, da, null);
+		scaleOp.filter(img, img);
+	}
+
+	public void enhanceRGB(float[] rgbm, float[] rgba) throws ImageOpException {
+
+		/*
+		 * The number of constants must match the number of bands in the image.
+		 * We do only 3 (RGB) bands.
+		 */
+
+		int ncol = img.getColorModel().getNumColorComponents();
+		if ((ncol != 3) || (rgbm.length != 3) || (rgba.length != 3)) {
+			logger
+					.debug("ERROR(enhance): unknown number of color bands or coefficients ("
+							+ ncol + ")");
+			return;
+		}
+		RescaleOp scaleOp = new RescaleOp(rgbOrdered(rgbm), rgbOrdered(rgba),
+				null);
+		scaleOp.filter(img, img);
+	}
 
-    if (scaledImg == null) {
-      util.dprintln(2, "ERROR(cropAndScale): error in scale");
-      throw new ImageOpException("Unable to scale");
-    }
-    img = scaledImg;
-  }
+	/**
+	 * Ensures that the array f is in the right order to map the images RGB
+	 * components. (not shure what happens
+	 */
+	public float[] rgbOrdered(float[] fa) {
+		/*
+		 * TODO: this is UGLY, UGLY!!
+		 */
+		float[] fb;
+		int t = img.getType();
+		if (img.getColorModel().hasAlpha()) {
+			fb = new float[4];
+			if ((t == BufferedImage.TYPE_INT_ARGB)
+					|| (t == BufferedImage.TYPE_INT_ARGB_PRE)) {
+				// RGB Type
+				fb[0] = fa[0];
+				fb[1] = fa[1];
+				fb[2] = fa[2];
+				fb[3] = 1f;
+			} else {
+				// this isn't tested :-(
+				fb[0] = 1f;
+				fb[1] = fa[0];
+				fb[2] = fa[1];
+				fb[3] = fa[2];
+			}
+		} else {
+			fb = new float[3];
+			if (t == BufferedImage.TYPE_3BYTE_BGR) {
+				// BGR Type (actually it looks like RBG...)
+				fb[0] = fa[0];
+				fb[1] = fa[2];
+				fb[2] = fa[1];
+			} else {
+				fb[0] = fa[0];
+				fb[1] = fa[1];
+				fb[2] = fa[2];
+			}
+		}
+		return fb;
+	}
 
+	public void rotate(double angle) throws ImageOpException {
+		// setup rotation
+		double rangle = Math.toRadians(angle);
+		// create offset to make shure the rotated image has no negative
+		// coordinates
+		double w = img.getWidth();
+		double h = img.getHeight();
+		AffineTransform trafo = new AffineTransform();
+		// center of rotation
+		double x = (w / 2);
+		double y = (h / 2);
+		trafo.rotate(rangle, x, y);
+		// try rotation to see how far we're out of bounds
+		AffineTransformOp rotOp = new AffineTransformOp(trafo, renderHint);
+		Rectangle2D rotbounds = rotOp.getBounds2D(img);
+		double xoff = rotbounds.getX();
+		double yoff = rotbounds.getY();
+		// move image back in line
+		trafo
+				.preConcatenate(AffineTransform.getTranslateInstance(-xoff,
+						-yoff));
+		// transform image
+		rotOp = new AffineTransformOp(trafo, renderHint);
+		BufferedImage rotImg = rotOp.filter(img, null);
+		// calculate new bounding box
+		// Rectangle2D bounds = rotOp.getBounds2D(img);
+		if (rotImg == null) {
+			throw new ImageOpException("Unable to rotate");
+		}
+		img = rotImg;
+		// crop new image (with self-made rounding)
+		/*
+		 * img = rotImg.getSubimage( (int) (bounds.getX()+0.5), (int)
+		 * (bounds.getY()+0.5), (int) (bounds.getWidth()+0.5), (int)
+		 * (bounds.getHeight()+0.5));
+		 */
+	}
+
+	public void mirror(double angle) throws ImageOpException {
+		// setup mirror
+		double mx = 1;
+		double my = 1;
+		double tx = 0;
+		double ty = 0;
+		if (Math.abs(angle - 0) < epsilon) { // 0 degree
+			mx = -1;
+			tx = getWidth();
+		} else if (Math.abs(angle - 90) < epsilon) { // 90 degree
+			my = -1;
+			ty = getHeight();
+		} else if (Math.abs(angle - 180) < epsilon) { // 180 degree
+			mx = -1;
+			tx = getWidth();
+		} else if (Math.abs(angle - 270) < epsilon) { // 270 degree
+			my = -1;
+			ty = getHeight();
+		} else if (Math.abs(angle - 360) < epsilon) { // 360 degree
+			mx = -1;
+			tx = getWidth();
+		}
+		AffineTransformOp mirOp = new AffineTransformOp(new AffineTransform(mx,
+				0, 0, my, tx, ty), renderHint);
+		BufferedImage mirImg = mirOp.filter(img, null);
+		if (mirImg == null) {
+			throw new ImageOpException("Unable to mirror");
+		}
+		img = mirImg;
+	}
+
+	/*
+	 * (non-Javadoc)
+	 * 
+	 * @see java.lang.Object#finalize()
+	 */
+	protected void finalize() throws Throwable {
+		dispose();
+		super.finalize();
+	}
+
+	public void dispose() {
+		// we must dispose the ImageReader because it keeps the filehandle
+		// open!
+		if (reader != null) {
+			reader.dispose();
+			reader = null;
+		}
+		img = null;
+	}
+
+	public Image getImage(){
+		return (Image) img;
+	}
+	
+	
 }
--- a/servlet/src/digilib/image/JAIDocuImage.java	Thu Jan 14 14:30:30 2010 +0100
+++ b/servlet/src/digilib/image/JAIDocuImage.java	Fri Feb 05 20:58:38 2010 +0100
@@ -1,168 +1,491 @@
 /* JAIDocuImage -- Image class implementation using JAI (Java Advanced Imaging)
 
-  Digital Image Library servlet components
+ Digital Image Library servlet components
 
-  Copyright (C) 2001, 2002 Robert Casties (robcast@mail.berlios.de)
+ Copyright (C) 2001, 2002, 2003 Robert Casties (robcast@mail.berlios.de)
 
-  This program is free software; you can redistribute  it and/or modify it
-  under  the terms of  the GNU General  Public License as published by the
-  Free Software Foundation;  either version 2 of the  License, or (at your
-  option) any later version.
-   
-  Please read license.txt for the full details. A copy of the GPL
-  may be found at http://www.gnu.org/copyleft/lgpl.html
+ This program is free software; you can redistribute  it and/or modify it
+ under  the terms of  the GNU General  Public License as published by the
+ Free Software Foundation;  either version 2 of the  License, or (at your
+ option) any later version.
+ 
+ Please read license.txt for the full details. A copy of the GPL
+ may be found at http://www.gnu.org/copyleft/lgpl.html
 
-  You should have received a copy of the GNU General Public License
-  along with this program; if not, write to the Free Software
-  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ You should have received a copy of the GNU General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
-*/
+ */
 
 package digilib.image;
 
-import javax.servlet.*;
-import javax.servlet.http.*;
-import java.io.*;
-import java.util.*;
+import java.awt.Rectangle;
+import java.awt.RenderingHints;
+import java.awt.image.RenderedImage;
+import java.awt.image.renderable.ParameterBlock;
+import java.io.File;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.Enumeration;
+import java.util.Iterator;
+import java.util.List;
 
-import java.awt.*;
-import java.awt.image.*;
-import java.awt.image.renderable.*;
-import javax.media.jai.*;
+import javax.media.jai.BorderExtender;
+import javax.media.jai.Interpolation;
+import javax.media.jai.JAI;
+import javax.media.jai.KernelJAI;
+import javax.media.jai.ParameterBlockJAI;
+import javax.media.jai.RenderedOp;
+import javax.media.jai.operator.TransposeDescriptor;
+import javax.media.jai.operator.TransposeType;
 
-import digilib.*;
-import digilib.io.*;
+import com.sun.media.jai.codec.ImageCodec;
 
+import digilib.io.FileOpException;
+import digilib.io.FileOps;
+import digilib.io.ImageFile;
+import digilib.io.ImageFileset;
 
+/** A DocuImage implementation using Java Advanced Imaging Library. */
 public class JAIDocuImage extends DocuImageImpl {
 
-  private RenderedImage img;
+	protected RenderedImage img;
 
-  public JAIDocuImage() {
-  }
-
-  public JAIDocuImage(Utils u) {
-    util = u;
-  }
+	protected Interpolation interpol = null;
 
-  /**
-   *  load image file
-   */
-  public void loadImage(File f) throws FileOpException {
-    System.gc();
-    img = JAI.create("fileload", f.getAbsolutePath());
-    if (img == null) {
-      util.dprintln(3, "ERROR(loadImage): unable to load file");
-      throw new FileOpException("Unable to load File!");
-    }
-  }
+	/*
+	 * static { // we could set our own tile cache size here TileCache tc =
+	 * JAI.createTileCache(100*1024*1024);
+	 * JAI.getDefaultInstance().setTileCache(tc); }
+	 */
+
+	public boolean isSubimageSupported() {
+		return true;
+	}
 
-  /**
-   *  write image of type mt to Stream
-   */
-  public void writeImage(String mt, ServletResponse res)
-         throws FileOpException {
-    try {
-    // setup output
-    ParameterBlock pb3 = new ParameterBlock();
-    pb3.addSource(img);
-    pb3.add(res.getOutputStream());
-    if (mt == "image/jpeg") {
-      pb3.add("JPEG");
-    } else if (mt == "image/png") {
-      pb3.add("PNG");
-    } else {
-      // unknown mime type
-      util.dprintln(2, "ERROR(writeImage): Unknown mime type "+mt);
-      throw new FileOpException("Unknown mime type: "+mt);
-    }
-    res.setContentType(mt);
-    // render output
-    JAI.create("encode", pb3);
-
-    } catch (IOException e) {
-      throw new FileOpException("Error writing image.");
-    }
-  }
+	/*
+	 * Real setQuality implementation. Creates the correct Interpolation.
+	 */
+	public void setQuality(int qual) {
+		quality = qual;
+		// setup interpolation quality
+		if (qual > 1) {
+			logger.debug("quality q2");
+			interpol = Interpolation.getInstance(Interpolation.INTERP_BICUBIC);
+		} else if (qual == 1) {
+			logger.debug("quality q1");
+			interpol = Interpolation.getInstance(Interpolation.INTERP_BILINEAR);
+		} else {
+			logger.debug("quality q0");
+			interpol = Interpolation.getInstance(Interpolation.INTERP_NEAREST);
+		}
+	}
 
-  public int getWidth() {
-    if (img != null) {
-      return img.getWidth();
-    }
-    return 0;
-  }
-
-  public int getHeight() {
-    if (img != null) {
-      return img.getHeight();
-    }
-    return 0;
-  }
-
-
-  /**
-   *  crop and scale image
-   *    take rectangle width,height at position x_off,y_off
-   *    and scale by scale
-   */
-   public void cropAndScale(int x_off, int y_off, int width, int height,
-         float scale, int qual) throws ImageOpException {
-
-    Interpolation scaleInt = null;
-    // setup interpolation quality
-    if (qual > 1) {
-      util.dprintln(4, "quality q2");
-      scaleInt = Interpolation.getInstance(Interpolation.INTERP_BICUBIC);
-    } else if (qual == 1) {
-      util.dprintln(4, "quality q1");
-      scaleInt = Interpolation.getInstance(Interpolation.INTERP_BILINEAR);
-    } else {
-      util.dprintln(4, "quality q0");
-      scaleInt = Interpolation.getInstance(Interpolation.INTERP_NEAREST);
+	/* returns a list of supported image formats */
+    @SuppressWarnings("unchecked") // ImageCodec.getCodecs() returns a naked Enumeration
+    public Iterator<String> getSupportedFormats() {
+        Enumeration<ImageCodec> codecs = ImageCodec.getCodecs();
+        List<String> formats = new ArrayList<String>();
+        for (ImageCodec codec = codecs.nextElement(); codecs.hasMoreElements(); codec = codecs
+                .nextElement()) {
+            logger.debug("known format:"+codec.getFormatName());
+            formats.add(codec.getFormatName());
+        }
+        logger.debug("tilecachesize:"
+                + JAI.getDefaultInstance().getTileCache().getMemoryCapacity());
+        return formats.iterator();
     }
 
-    // setup Crop
-    ParameterBlock pb1 = new ParameterBlock();
-    pb1.addSource(img);
-    pb1.add((float)x_off);
-    pb1.add((float)y_off);
-    pb1.add((float)width);
-    pb1.add((float)height);
-    RenderedImage croppedImg = JAI.create("crop", pb1);
-    img = null; // free img
+	/* Check image size and type and store in ImageFile f */
+	public boolean identify(ImageFile imgf) throws IOException {
+		// try parent method first
+		if (super.identify(imgf)) {
+			return true;
+		}
+		// fileset to store the information
+		ImageFileset imgfs = imgf.getParent();
+		File f = imgf.getFile();
+		if (f == null) {
+			throw new IOException("File not found!");
+		}
+		/*
+		 * try JAI
+		 */
+		logger.debug("identifying (JAI) " + f);
+		try {
+			RenderedOp img = JAI.create("fileload", f.getAbsolutePath());
+			ImageSize d = new ImageSize(img.getWidth(), img.getHeight());
+			imgf.setSize(d);
+			String t = FileOps.mimeForFile(f);
+			imgf.setMimetype(t);
+			// logger.debug(" format:"+t);
+			if (imgfs != null) {
+				imgfs.setAspect(d);
+			}
+			logger.debug("image size: " + imgf.getSize());
+			return true;
+		} catch (Exception e) {
+			throw new FileOpException("ERROR: unknown image file format!");
+		}
+	}
+
+	/* Load an image file into the Object. */
+	public void loadImage(ImageFile f) throws FileOpException {
+		img = JAI.create("fileload", f.getFile().getAbsolutePath());
+		if (img == null) {
+			throw new FileOpException("Unable to load File!");
+		}
+	}
+
+	/* Load an image file into the Object. */
+	public void loadSubimage(ImageFile f, Rectangle region, int subsample)
+			throws FileOpException {
+		logger.debug("loadSubimage");
+		img = JAI.create("fileload", f.getFile().getAbsolutePath());
+		if ((region.width < img.getWidth())
+				|| (region.height < img.getHeight())) {
+			// setup Crop
+			ParameterBlock cp = new ParameterBlock();
+			cp.addSource(img);
+			cp.add((float) region.x);
+			cp.add((float) region.y);
+			cp.add((float) region.width);
+			cp.add((float) region.height);
+			logger.debug("loadSubimage: crop");
+			img = JAI.create("crop", cp);
+		}
+		if (subsample > 1) {
+			float sc = 1f / subsample;
+			ParameterBlockJAI sp = new ParameterBlockJAI("scale");
+			sp.addSource(img);
+			sp.setParameter("xScale", sc);
+			sp.setParameter("yScale", sc);
+			sp.setParameter("interpolation", Interpolation
+					.getInstance(Interpolation.INTERP_NEAREST));
+			// scale
+			logger.debug("loadSubimage: scale");
+			img = JAI.create("scale", sp);
+		}
+	}
 
-    util.dprintln(3, "CROP:"+croppedImg.getWidth()+"x"+croppedImg.getHeight()); //DEBUG
+	/* Write the current image to an OutputStream. */
+	public void writeImage(String mt, OutputStream ostream)
+			throws FileOpException {
+		try {
+			// setup output
+			ParameterBlock pb3 = new ParameterBlock();
+			pb3.addSource(img);
+			pb3.add(ostream);
+			if (mt == "image/jpeg") {
+				pb3.add("JPEG");
+			} else if (mt == "image/png") {
+				pb3.add("PNG");
+			} else {
+				// unknown mime type
+				throw new FileOpException("Unknown mime type: " + mt);
+			}
+			// render output
+			JAI.create("encode", pb3);
+
+		} catch (IOException e) {
+			throw new FileOpException("Error writing image.");
+		}
+	}
+
+	/**
+	 * The width of the curent image in pixel.
+	 * 
+	 * @return Image width in pixels.
+	 */
+	public int getWidth() {
+		if (img != null) {
+			return img.getWidth();
+		}
+		return 0;
+	}
+
+	/**
+	 * The height of the curent image in pixel.
+	 * 
+	 * @return Image height in pixels.
+	 */
+	public int getHeight() {
+		if (img != null) {
+			return img.getHeight();
+		}
+		return 0;
+	}
 
-    if (croppedImg == null) {
-      util.dprintln(2, "ERROR(cropAndScale): error in crop");
-      throw new ImageOpException("Unable to crop");
-    }
+	/* scales the current image */
+	public void scale(double scale, double scaleY) throws ImageOpException {
+		logger.debug("scale");
+		if ((scale < 1) && (img.getColorModel().getPixelSize() == 1)
+				&& (quality > 0)) {
+			/*
+			 * "SubsampleBinaryToGray" for downscaling BW
+			 */
+			scaleBinary((float) scale);
+		} else if ((scale <= 0.5) && (quality > 1)) {
+			/*
+			 * blur and "Scale" for downscaling color images
+			 */
+			if ((scale <= 0.5) && (quality > 1)) {
+				int bl = (int) Math.floor(1 / scale);
+				// don't blur more than 3
+				blur(Math.min(bl, 3));
+			}
+			scaleAll((float) scale);
+		} else {
+			/*
+			 * "Scale" for the rest
+			 */
+			scaleAll((float) scale);
+		}
+
+		// DEBUG
+		logger.debug("SCALE: " + scale + " ->" + img.getWidth() + "x"
+				+ img.getHeight());
+
+	}
+
+	public void scaleAll(float scale) throws ImageOpException {
+		RenderedImage scaledImg;
+		// DEBUG
+		logger.debug("scaleAll: " + scale);
+		ParameterBlockJAI param = new ParameterBlockJAI("Scale");
+		param.addSource(img);
+		param.setParameter("xScale", scale);
+		param.setParameter("yScale", scale);
+		param.setParameter("interpolation", interpol);
+		// hint with border extender
+		RenderingHints hint = new RenderingHints(JAI.KEY_BORDER_EXTENDER,
+				BorderExtender.createInstance(BorderExtender.BORDER_COPY));
+		// scale
+		scaledImg = JAI.create("Scale", param, hint);
+
+		if (scaledImg == null) {
+			throw new ImageOpException("Unable to scale");
+		}
+		img = scaledImg;
+	}
 
-    // setup scale
-    ParameterBlock pb2 = new ParameterBlock();
-    pb2.addSource(croppedImg);
-    pb2.add(scale);
-    pb2.add(scale);
-    pb2.add(0f);
-    pb2.add(0f);
-    pb2.add(scaleInt);
-    // the following is nice but way too slow...
-    //if (opCrop.getColorModel().getPixelSize() < 8) {
-    // change color model if necessary
-    //  util.dprintln("converting color model...");
-    //  BufferedImage bi = new BufferedImage(1, 1, BufferedImage.TYPE_BYTE_GRAY);
-    //  ImageLayout lay = new ImageLayout(bi);
-    //  rh = new RenderingHints(JAI.KEY_IMAGE_LAYOUT, lay);
-    //}
-    RenderedImage scaledImg = JAI.create("scale", pb2);
-    croppedImg = null; // free opCrop
+	public void blur(int radius) throws ImageOpException {
+		RenderedImage blurredImg;
+		int klen = Math.max(radius, 2);
+		logger.debug("blur: " + klen);
+		int ksize = klen * klen;
+		float f = 1f / ksize;
+		float[] kern = new float[ksize];
+		for (int i = 0; i < ksize; i++) {
+			kern[i] = f;
+		}
+		KernelJAI blur = new KernelJAI(klen, klen, kern);
+		ParameterBlockJAI param = new ParameterBlockJAI("Convolve");
+		param.addSource(img);
+		param.setParameter("kernel", blur);
+		// hint with border extender
+		RenderingHints hint = new RenderingHints(JAI.KEY_BORDER_EXTENDER,
+				BorderExtender.createInstance(BorderExtender.BORDER_COPY));
+		blurredImg = JAI.create("Convolve", param, hint);
+		if (blurredImg == null) {
+			throw new ImageOpException("Unable to scale");
+		}
+		img = blurredImg;
+	}
+
+	public void scaleBinary(float scale) throws ImageOpException {
+		RenderedImage scaledImg;
+		// DEBUG
+		logger.debug("scaleBinary: " + scale);
+		ParameterBlockJAI param = new ParameterBlockJAI("SubsampleBinaryToGray");
+		param.addSource(img);
+		param.setParameter("xScale", scale);
+		param.setParameter("yScale", scale);
+		// hint with border extender
+		RenderingHints hint = new RenderingHints(JAI.KEY_BORDER_EXTENDER,
+				BorderExtender.createInstance(BorderExtender.BORDER_COPY));
+		// scale
+		scaledImg = JAI.create("SubsampleBinaryToGray", param, hint);
+		if (scaledImg == null) {
+			throw new ImageOpException("Unable to scale");
+		}
+		img = scaledImg;
+	}
+
+	/* crops the current image */
+	public void crop(int x_off, int y_off, int width, int height)
+			throws ImageOpException {
+		// setup Crop
+		ParameterBlock param = new ParameterBlock();
+		param.addSource(img);
+		param.add((float) x_off);
+		param.add((float) y_off);
+		param.add((float) width);
+		param.add((float) height);
+		RenderedImage croppedImg = JAI.create("crop", param);
+
+		logger.debug("CROP: " + x_off + "," + y_off + ", " + width + ","
+				+ height + " ->" + croppedImg.getWidth() + "x"
+				+ croppedImg.getHeight());
+		// DEBUG
+
+		if (croppedImg == null) {
+			throw new ImageOpException("Unable to crop");
+		}
+		img = croppedImg;
+	}
+
+	/* rotates the current image */
+	public void rotate(double angle) throws ImageOpException {
+		RenderedImage rotImg;
+		// convert degrees to radians
+		double rangle = Math.toRadians(angle);
+		double x = img.getWidth() / 2;
+		double y = img.getHeight() / 2;
+
+		// optimize rotation by right angles
+		TransposeType rotOp = null;
+		if (Math.abs(angle - 0) < epsilon) {
+			// 0 degree
+			return;
+		} else if (Math.abs(angle - 90) < epsilon) {
+			// 90 degree
+			rotOp = TransposeDescriptor.ROTATE_90;
+		} else if (Math.abs(angle - 180) < epsilon) {
+			// 180 degree
+			rotOp = TransposeDescriptor.ROTATE_180;
+		} else if (Math.abs(angle - 270) < epsilon) {
+			// 270 degree
+			rotOp = TransposeDescriptor.ROTATE_270;
+		} else if (Math.abs(angle - 360) < epsilon) {
+			// 360 degree
+			return;
+		}
+		if (rotOp != null) {
+			// use Transpose operation
+			ParameterBlock pb = new ParameterBlock();
+			pb.addSource(img);
+			pb.add(rotOp);
+			rotImg = JAI.create("transpose", pb);
+		} else {
+			// setup "normal" rotation
+			ParameterBlock param = new ParameterBlock();
+			param.addSource(img);
+			param.add((float) x);
+			param.add((float) y);
+			param.add((float) rangle);
+			param.add(interpol);
 
-    if (scaledImg == null) {
-      util.dprintln(2, "ERROR(cropAndScale): error in scale");
-      throw new ImageOpException("Unable to scale");
-    }
+			rotImg = JAI.create("rotate", param);
+		}
+
+		logger.debug("ROTATE: " + x + "," + y + ", " + angle + " (" + rangle
+				+ ")" + " ->" + rotImg.getWidth() + "x" + rotImg.getHeight());
+		// DEBUG
+
+		if (rotImg == null) {
+			throw new ImageOpException("Unable to rotate");
+		}
+		img = rotImg;
+	}
+
+	/*
+	 * mirrors the current image works only horizontal and vertical
+	 */
+	public void mirror(double angle) throws ImageOpException {
+		RenderedImage mirImg;
+		// only mirroring by right angles
+		TransposeType rotOp = null;
+		if (Math.abs(angle) < epsilon) {
+			// 0 degree
+			rotOp = TransposeDescriptor.FLIP_HORIZONTAL;
+		} else if (Math.abs(angle - 90) < epsilon) {
+			// 90 degree
+			rotOp = TransposeDescriptor.FLIP_VERTICAL;
+		} else if (Math.abs(angle - 180) < epsilon) {
+			// 180 degree
+			rotOp = TransposeDescriptor.FLIP_HORIZONTAL;
+		} else if (Math.abs(angle - 270) < epsilon) {
+			// 270 degree
+			rotOp = TransposeDescriptor.FLIP_VERTICAL;
+		} else if (Math.abs(angle - 360) < epsilon) {
+			// 360 degree
+			rotOp = TransposeDescriptor.FLIP_HORIZONTAL;
+		}
+		// use Transpose operation
+		ParameterBlock param = new ParameterBlock();
+		param.addSource(img);
+		param.add(rotOp);
+		mirImg = JAI.create("transpose", param);
+
+		if (mirImg == null) {
+			throw new ImageOpException("Unable to flip");
+		}
+		img = mirImg;
+	}
 
-    img = scaledImg;
-  }
+	/* contrast and brightness enhancement */
+	public void enhance(float mult, float add) throws ImageOpException {
+		RenderedImage enhImg;
+		double[] ma = { mult };
+		double[] aa = { add };
+		// use Rescale operation
+		ParameterBlock param = new ParameterBlock();
+		param.addSource(img);
+		param.add(ma);
+		param.add(aa);
+		enhImg = JAI.create("rescale", param);
+
+		logger.debug("ENHANCE: *" + mult + ", +" + add + " ->"
+				+ enhImg.getWidth() + "x" + enhImg.getHeight());
+		// DEBUG
+
+		if (enhImg == null) {
+			throw new ImageOpException("Unable to enhance");
+		}
+		img = enhImg;
+	}
+
+	/*
+	 * (non-Javadoc)
+	 * 
+	 * @see digilib.image.DocuImage#enhanceRGB(float[], float[])
+	 */
+	public void enhanceRGB(float[] rgbm, float[] rgba) throws ImageOpException {
+		RenderedImage enhImg;
+		int nb = rgbm.length;
+		double[] ma = new double[nb];
+		double[] aa = new double[nb];
+		for (int i = 0; i < nb; i++) {
+			ma[i] = rgbm[i];
+			aa[i] = rgba[i];
+		}
+		// use Rescale operation
+		ParameterBlock param = new ParameterBlock();
+		param.addSource(img);
+		param.add(ma);
+		param.add(aa);
+		enhImg = JAI.create("rescale", param);
+
+		logger.debug("ENHANCE_RGB: *" + rgbm + ", +" + rgba + " ->"
+				+ enhImg.getWidth() + "x" + enhImg.getHeight());
+		// DEBUG
+
+		if (enhImg == null) {
+			throw new ImageOpException("Unable to enhanceRGB");
+		}
+		img = enhImg;
+	}
+
+	/*
+	 * (non-Javadoc)
+	 * 
+	 * @see digilib.image.DocuImage#dispose()
+	 */
+	public void dispose() {
+		img = null;
+	}
 
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/servlet/src/digilib/image/JAIImageLoaderDocuImage.java	Fri Feb 05 20:58:38 2010 +0100
@@ -0,0 +1,184 @@
+/* JAIImageLoaderDocuImage -- Image class implementation using JAI's ImageLoader Plugin
+
+  Digital Image Library servlet components
+
+  Copyright (C) 2002, 2003 Robert Casties (robcast@mail.berlios.de)
+
+  This program is free software; you can redistribute  it and/or modify it
+  under  the terms of  the GNU General  Public License as published by the
+  Free Software Foundation;  either version 2 of the  License, or (at your
+  option) any later version.
+   
+  Please read license.txt for the full details. A copy of the GPL
+  may be found at http://www.gnu.org/copyleft/lgpl.html
+
+  You should have received a copy of the GNU General Public License
+  along with this program; if not, write to the Free Software
+  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+*/
+
+package digilib.image;
+
+import java.awt.Rectangle;
+import java.awt.image.renderable.ParameterBlock;
+import java.io.File;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.RandomAccessFile;
+import java.util.Iterator;
+
+import javax.imageio.ImageIO;
+import javax.imageio.ImageReadParam;
+import javax.imageio.ImageReader;
+import javax.imageio.stream.FileImageInputStream;
+import javax.imageio.stream.ImageInputStream;
+import javax.media.jai.JAI;
+
+import digilib.io.FileOpException;
+import digilib.io.ImageFile;
+
+/** DocuImage implementation using the Java Advanced Imaging API and the ImageLoader
+ * API of Java 1.4.
+ */
+public class JAIImageLoaderDocuImage extends JAIDocuImage {
+
+	/** ImageIO image reader */
+	protected ImageReader reader;
+	/** current image file */
+	protected File imgFile;
+
+	/* loadSubimage is supported. */
+	public boolean isSubimageSupported() {
+		return true;
+	}
+
+	public int getHeight() {
+		int h = 0;
+		try {
+			if (img == null) {
+				h = reader.getHeight(0);
+			} else {
+				h = img.getHeight();
+			}
+		} catch (IOException e) {
+			logger.debug("error in getHeight", e);
+		}
+		return h;
+	}
+
+	public int getWidth() {
+		int w = 0;
+		try {
+			if (img == null) {
+				w = reader.getWidth(0);
+			} else {
+				w = img.getWidth();
+			}
+		} catch (IOException e) {
+			logger.debug("error in getHeight", e);
+		}
+		return w;
+	}
+
+	/* Load an image file into the Object. */
+	public void loadImage(ImageFile f) throws FileOpException {
+		logger.debug("loadImage: "+f.getFile());
+		//System.gc();
+		img = JAI.create("ImageRead", f.getFile().getAbsolutePath());
+		if (img == null) {
+			throw new FileOpException("Unable to load File!");
+		}
+	}
+
+	/* Get an ImageReader for the image file. */
+	public ImageReader getReader(ImageFile f) throws IOException {
+		logger.debug("preloadImage: "+f.getFile());
+		//System.gc();
+		RandomAccessFile rf = new RandomAccessFile(f.getFile(), "r");
+		ImageInputStream istream = new FileImageInputStream(rf);
+		//Iterator readers = ImageIO.getImageReaders(istream);
+		Iterator<ImageReader> readers = ImageIO.getImageReadersByMIMEType(f.getMimetype());
+		if (! readers.hasNext()) {
+			throw new FileOpException("Unable to load File!");
+		}
+		reader = readers.next();
+		logger.debug("JAIImageIO: this reader: " + reader.getClass());
+		while (readers.hasNext()) {
+			logger.debug("  next reader: " + readers.next().getClass());
+		}
+		reader.setInput(istream);
+		return reader;
+	}
+
+	/* Load an image file into the Object. */
+	public void loadSubimage(ImageFile f, Rectangle region, int prescale)
+		throws FileOpException {
+		logger.debug("loadSubimage: "+f.getFile());
+		//System.gc();
+		try {
+			if ((reader == null) || (imgFile != f.getFile())) {
+				getReader(f);
+			}
+			ImageReadParam readParam = reader.getDefaultReadParam();
+			readParam.setSourceRegion(region);
+			readParam.setSourceSubsampling(prescale, prescale, 0, 0);
+			img = reader.read(0, readParam);
+			/* JAI imageread seems to ignore the readParam :-(
+			ImageInputStream istream = (ImageInputStream) reader.getInput();
+			ParameterBlockJAI pb = new ParameterBlockJAI("imageread");
+			pb.setParameter("Input", istream);
+			pb.setParameter("ReadParam", readParam);
+			pb.setParameter("Reader", reader);
+			img = JAI.create("imageread", pb);
+			*/
+		} catch (IOException e) {
+			throw new FileOpException("Unable to load File!");
+		}
+		if (img == null) {
+			throw new FileOpException("Unable to load File!");
+		}
+		imgFile = f.getFile();
+	}
+
+
+	/* Write the current image to an OutputStream. */
+	public void writeImage(String mt, OutputStream ostream)
+		throws FileOpException {
+		logger.debug("writeImage");
+		try {
+			// setup output
+			ParameterBlock pb3 = new ParameterBlock();
+			pb3.addSource(img);
+			pb3.add(ostream);
+			if (mt == "image/jpeg") {
+				pb3.add("JPEG");
+			} else if (mt == "image/png") {
+				pb3.add("PNG");
+			} else {
+				// unknown mime type
+				throw new FileOpException("Unknown mime type: " + mt);
+			}
+			// render output
+			JAI.create("ImageWrite", pb3);
+		} catch (IOException e) {
+			throw new FileOpException("Error writing image.");
+		}
+	}
+
+	/* (non-Javadoc)
+	 * @see java.lang.Object#finalize()
+	 */
+	protected void finalize() throws Throwable {
+		dispose();
+		super.finalize();
+	}
+
+	public void dispose() {
+		// we must dispose the ImageReader because it keeps the filehandle open!
+		reader.dispose();
+		reader = null;
+		img = null;
+	}
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/servlet/src/digilib/io/AliasingDocuDirCache.java	Fri Feb 05 20:58:38 2010 +0100
@@ -0,0 +1,96 @@
+/*
+ * AliasingDocuDirCache -- DocuDirCache using alias entries from config file
+ * 
+ * Digital Image Library servlet components
+ * 
+ * Copyright (C) 2003 Robert Casties (robcast@mail.berlios.de)
+ * 
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the Free
+ * Software Foundation; either version 2 of the License, or (at your option)
+ * any later version.
+ * 
+ * Please read license.txt for the full details. A copy of the GPL may be found
+ * at http://www.gnu.org/copyleft/lgpl.html
+ * 
+ * You should have received a copy of the GNU General Public License along with
+ * this program; if not, write to the Free Software Foundation, Inc., 59 Temple
+ * Place, Suite 330, Boston, MA 02111-1307 USA
+ * 
+ * Created on 04.11.2003
+ */
+
+package digilib.io;
+
+import java.io.File;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import digilib.servlet.DigilibConfiguration;
+
+/**
+ * @author casties
+ *  
+ */
+public class AliasingDocuDirCache extends DocuDirCache {
+
+	/**
+	 * @param baseDirs
+	 * @param fileClasses
+	 * @param confFileName
+	 * @throws FileOpException
+	 */
+	public AliasingDocuDirCache(String[] baseDirs, int[] fileClasses,
+			File confFile, DigilibConfiguration dlConfig)
+			throws FileOpException {
+		// create standard DocuDirCache
+		super(baseDirs, fileClasses, dlConfig);
+		Map<String,String> pathMap = null;
+		// read alias config file
+		try {
+			// load into pathMap
+			XMLListLoader mapLoader = new XMLListLoader("digilib-aliases",
+					"mapping", "link", "dir");
+			pathMap = mapLoader.loadURL(confFile.toURL().toString());
+		} catch (Exception e) {
+			throw new FileOpException("ERROR loading mapping file: " + e);
+		}
+		if (pathMap == null) {
+			throw new FileOpException("ERROR: unable to load mapping file!");
+		}
+
+		/*
+		 * load map entries into cache
+		 */
+
+		for (Entry<String, String> linkdir: pathMap.entrySet()) {
+			if (linkdir.getValue() == null) {
+				logger.error("Key mismatch in mapping file!");
+				break;	
+			}
+			DocuDirectory destDir = new DocuDirectory(linkdir.getValue(), this);
+			if (destDir.isValid()) {
+				logger.debug("Aliasing dir: " + linkdir.getKey());
+				// add the alias name
+				putName(FileOps.normalName(linkdir.getKey()), destDir);
+				// add the real dir
+				putDir(destDir);
+			}
+		}
+	}
+
+	/**
+	 * Adds a DocuDirectory under another name to the cache.
+	 * 
+	 * @param name
+	 * @param newdir
+	 */
+    public void putName(String name, DocuDirectory newdir) {
+        if (map.containsKey(name)) {
+            logger.warn("Duplicate key in AliasingDocuDirCache.put -- ignored!");
+        } else {
+            map.put(name, newdir);
+        }
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/servlet/src/digilib/io/DocuDirCache.java	Fri Feb 05 20:58:38 2010 +0100
@@ -0,0 +1,361 @@
+/*
+ * DocuDirCache.java
+ * 
+ * Digital Image Library servlet components
+ * 
+ * Copyright (C) 2003 Robert Casties (robcast@mail.berlios.de)
+ * 
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the Free
+ * Software Foundation; either version 2 of the License, or (at your option)
+ * any later version.
+ * 
+ * Please read license.txt for the full details. A copy of the GPL may be found
+ * at http://www.gnu.org/copyleft/lgpl.html
+ * 
+ * You should have received a copy of the GNU General Public License along with
+ * this program; if not, write to the Free Software Foundation, Inc., 59 Temple
+ * Place, Suite 330, Boston, MA 02111-1307 USA
+ * 
+ * Created on 03.03.2003
+ */
+
+package digilib.io;
+
+import java.io.File;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.log4j.Logger;
+
+import digilib.servlet.DigilibConfiguration;
+
+/**
+ * @author casties
+ */
+public class DocuDirCache {
+
+	/** general logger for this class */
+	Logger logger = Logger.getLogger(this.getClass());
+
+	/** HashMap of directories */
+	Map<String, DocuDirectory> map = null;
+
+	/** names of base directories */
+	String[] baseDirNames = null;
+
+	/** array of allowed file classes (image/text) */
+	private int[] fileClasses = null;
+
+	/** number of files in the whole cache (approximate) */
+	long numFiles = 0;
+
+	/** number of cache hits */
+	long hits = 0;
+
+	/** number of cache misses */
+	long misses = 0;
+
+	/** use safe (but slow) indexing */
+	boolean safeDirIndex = false;
+
+	/** the root directory element */
+	public static Directory ROOT = null;
+
+	/**
+	 * Constructor with array of base directory names and file classes.
+	 * 
+	 * @param bd
+	 *            base directory names
+	 */
+	public DocuDirCache(String[] bd, int[] fileClasses,
+			DigilibConfiguration dlConfig) {
+		baseDirNames = bd;
+		map = new HashMap<String, DocuDirectory>();
+		this.fileClasses = fileClasses;
+		safeDirIndex = dlConfig.getAsBoolean("safe-dir-index");
+	}
+
+	/**
+	 * Constructor with array of base directory names.
+	 * 
+	 * @param bd
+	 *            base directory names
+	 */
+	public DocuDirCache(String[] bd) {
+		baseDirNames = bd;
+		map = new HashMap<String, DocuDirectory>();
+		// default file class is CLASS_IMAGE
+		fileClasses = new int[1];
+		fileClasses[0] = FileOps.CLASS_IMAGE;
+	}
+
+	/**
+	 * The number of directories in the cache.
+	 * 
+	 * @return
+	 */
+	public int size() {
+		return (map != null) ? map.size() : 0;
+	}
+
+	/**
+	 * Add a DocuDirectory to the cache.
+	 * 
+	 * @param newdir
+	 */
+	public void put(DocuDirectory newdir) {
+		String s = newdir.getDirName();
+		if (map.containsKey(s)) {
+			logger.warn("Duplicate key in DocuDirCache.put -- ignoring!");
+		} else {
+			map.put(s, newdir);
+			numFiles += newdir.size();
+		}
+	}
+
+	/**
+	 * Add a directory to the cache and check its parents.
+	 * 
+	 * @param newDir
+	 */
+	public synchronized void putDir(DocuDirectory newDir) {
+		put(newDir);
+		String parent = FileOps.parent(newDir.getDirName());
+		if (parent != "") {
+			// check the parent in the cache
+			DocuDirectory pd = (DocuDirectory) map.get(parent);
+			if (pd == null) {
+				// the parent is unknown
+				pd = new DocuDirectory(parent, this);
+				putDir(pd);
+			}
+			newDir.setParent(pd);
+		}
+	}
+
+	/**
+	 * Get a list with all children of a directory.
+	 * 
+	 * Returns a List of DocuDirectory's. Returns an empty List if the directory
+	 * has no children. If recurse is false then only direct children are
+	 * returned.
+	 * 
+	 * @param dirname
+	 * @param recurse
+	 *            find all children and their children.
+	 * @return
+	 */
+	public List<DocuDirectory> getChildren(String dirname, boolean recurse) {
+		List<DocuDirectory> l = new LinkedList<DocuDirectory>();
+		for (DocuDirectory dd: map.values()) {
+			if (recurse) {
+				if (dd.getDirName().startsWith(dirname)) {
+					l.add(dd);
+				}
+			} else {
+				if (FileOps.parent(dd.getDirName()).equals(dirname)) {
+					l.add(dd);
+				}
+			}
+		}
+		return l;
+	}
+
+	/**
+	 * Returns the DocuDirent with the pathname <code>fn</code> and the index
+	 * <code>in</code> and the class <code>fc</code>.
+	 * 
+	 * If <code>fn</code> is a file then the corresponding DocuDirent is
+	 * returned and the index is ignored.
+	 * 
+	 * @param fn
+	 *            digilib pathname
+	 * @param in
+	 *            file index
+	 * @param fc
+	 *            file class
+	 * @return
+	 */
+	public DocuDirent getFile(String fn, int in, int fc) {
+		DocuDirectory dd;
+		// file number is 1-based, vector index is 0-based
+		int n = in - 1;
+		// first, assume fn is a directory and look in the cache
+		dd = (DocuDirectory) map.get(fn);
+        // logger.debug("fn: " + fn);
+        // logger.debug("dd: " + dd);
+		if (dd == null) {
+			// cache miss
+			misses++;
+			/*
+			 * see if fn is a directory
+			 */
+			File f = new File(baseDirNames[0], fn);
+			if (f.isDirectory()) {
+                // logger.debug(fn + " is a dir");
+				dd = new DocuDirectory(fn, this);
+				if (dd.isValid()) {
+					// add to the cache
+					putDir(dd);
+				}
+			} else {
+				/*
+				 * maybe it's a file
+				 */
+				// get the parent directory string (like we store it in the
+				// cache)
+				String d = FileOps.parent(fn);
+				// try it in the cache
+                // logger.debug(fn + " is a file in dir " + d);
+				dd = (DocuDirectory) map.get(d);
+				if (dd == null) {
+					// try to read from disk
+					dd = new DocuDirectory(d, this);
+					if (dd.isValid()) {
+						// add to the cache
+                        // logger.debug(dd + " is valid");
+						putDir(dd);
+					} else {
+						// invalid path
+						return null;
+					}
+				} else {
+					// it was not a real cache miss
+					misses--;
+				}
+				// get the file's index
+				n = dd.indexOf(f.getName(), fc);
+                // logger.debug(f.getName() + ", index is " + n + ", fc = " + fc);
+			}
+		} else {
+			// cache hit
+			hits++;
+		}
+		dd.refresh();
+        // logger.debug(dd + " refreshed");
+		if (dd.isValid()) {
+			try {
+                // logger.debug(dd + " is valid");
+				return dd.get(n, fc);
+			} catch (IndexOutOfBoundsException e) {
+                // logger.debug(fn + " not found in directory");
+			}
+		}
+		return null;
+	}
+
+	/**
+	 * Returns the DocuDirectory indicated by the pathname <code>fn</code>.
+	 * 
+	 * If <code>fn</code> is a file then its parent directory is returned.
+	 * 
+	 * @param fn
+	 *            digilib pathname
+	 * @return
+	 */
+	public DocuDirectory getDirectory(String fn) {
+		DocuDirectory dd;
+		// first, assume fn is a directory and look in the cache
+		dd = (DocuDirectory) map.get(fn);
+		if (dd == null) {
+			// cache miss
+			misses++;
+			// see if it's a directory
+			File f = new File(baseDirNames[0], fn);
+			if (f.isDirectory()) {
+				dd = new DocuDirectory(fn, this);
+				if (dd.isValid()) {
+					// add to the cache
+					putDir(dd);
+				}
+			} else {
+				// maybe it's a file
+				if (f.canRead()) {
+					// try the parent directory in the cache
+					dd = (DocuDirectory) map.get(f.getParent());
+					if (dd == null) {
+						// try to read from disk
+						dd = new DocuDirectory(f.getParent(), this);
+						if (dd.isValid()) {
+							// add to the cache
+							putDir(dd);
+						} else {
+							// invalid path
+							return null;
+						}
+					} else {
+						// not a real cache miss then
+						misses--;
+					}
+				} else {
+					// it's not even a file :-(
+					return null;
+				}
+			}
+		} else {
+			// cache hit
+			hits++;
+		}
+		dd.refresh();
+		if (dd.isValid()) {
+			return dd;
+		}
+		return null;
+	}
+
+	/**
+	 * @return String[]
+	 */
+	public String[] getBaseDirNames() {
+		return baseDirNames;
+	}
+
+	/**
+	 * @return long
+	 */
+	public long getNumFiles() {
+		return numFiles;
+	}
+
+	/**
+	 * Sets the baseDirNames.
+	 * 
+	 * @param baseDirNames
+	 *            The baseDirNames to set
+	 */
+	public void setBaseDirNames(String[] baseDirNames) {
+		this.baseDirNames = baseDirNames;
+	}
+
+	/**
+	 * @return long
+	 */
+	public long getHits() {
+		return hits;
+	}
+
+	/**
+	 * @return long
+	 */
+	public long getMisses() {
+		return misses;
+	}
+
+	/**
+	 * @return
+	 */
+	public int[] getFileClasses() {
+		return fileClasses;
+	}
+
+	/**
+	 * @param fileClasses
+	 */
+	public void setFileClasses(int[] fileClasses) {
+		this.fileClasses = fileClasses;
+	}
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/servlet/src/digilib/io/DocuDirectory.java	Fri Feb 05 20:58:38 2010 +0100
@@ -0,0 +1,576 @@
+/* DocuDirectory -- Directory of DocuFilesets.
+
+ Digital Image Library servlet components
+
+ Copyright (C) 2003 Robert Casties (robcast@mail.berlios.de)
+
+ This program is free software; you can redistribute  it and/or modify it
+ under  the terms of  the GNU General  Public License as published by the
+ Free Software Foundation;  either version 2 of the  License, or (at your
+ option) any later version.
+ 
+ Please read license.txt for the full details. A copy of the GPL
+ may be found at http://www.gnu.org/copyleft/lgpl.html
+
+ You should have received a copy of the GNU General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+ * Created on 25.02.2003
+ */
+
+package digilib.io;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import org.xml.sax.SAXException;
+
+/**
+ * @author casties
+ */
+public class DocuDirectory extends Directory {
+
+	/** list of files (DocuDirent) */
+	private List<List<DocuDirent>> list = null;
+
+	/** directory object is valid (exists on disk) */
+	private boolean isValid = false;
+
+	/** reference of the parent DocuDirCache */
+	private DocuDirCache cache = null;
+
+	/** directory name (digilib canonical form) */
+	private String dirName = null;
+
+	/** directory metadata */
+	private MetadataMap dirMeta = null;
+
+	/** state of metadata is valid */
+	private boolean metaChecked = false;
+
+	/** unresolved file metadata */
+	private Map<String, MetadataMap> unresolvedFileMeta = null;
+
+	/** time of last access of this object (not the filesystem) */
+	private long objectATime = 0;
+
+	/** time directory was last modified on the file system */
+	private long dirMTime = 0;
+
+	/**
+	 * Constructor with digilib directory path and a parent DocuDirCache.
+	 * 
+	 * Directory names at the given path are appended to the base directories
+	 * from the cache. The directory is checked on disk and isValid is set.
+	 * 
+	 * @see readDir
+	 * 
+	 * @param path
+	 *            digilib directory path name
+	 * @param cache
+	 *            parent DocuDirCache
+	 */
+	public DocuDirectory(String path, DocuDirCache cache) {
+		this.dirName = path;
+		this.cache = cache;
+		initDir();
+		checkDir();
+	}
+
+	/**
+	 * Sets and checks the dir object.
+	 *  
+	 */
+	protected void initDir() {
+		String baseDirName = cache.getBaseDirNames()[0];
+		// clear directory first
+		//list = new ArrayList[FileOps.NUM_CLASSES];
+
+		list = new ArrayList<List<DocuDirent>>(FileOps.NUM_CLASSES);
+		// create empty list
+		for (int i=0; i < FileOps.NUM_CLASSES; ++i) {
+		    list.add(null);
+		}
+		isValid = false;
+		dirMTime = 0;
+		// the first directory has to exist
+		dir = new File(baseDirName, dirName);
+	}
+
+	/**
+	 * number of DocuFiles in this directory.
+	 *  
+	 */
+	public int size() {
+		return ((list != null) && (list.get(0) != null)) ? list.get(0).size() : 0;
+	}
+
+	/**
+	 * number of files of this class in this directory.
+	 * 
+	 * @param fc
+	 *            fileClass
+	 */
+	public int size(int fc) {
+		return ((list != null) && (list.get(fc) != null)) ? list.get(fc).size() : 0;
+	}
+
+	/**
+	 * Returns the ImageFile at the index.
+	 * 
+	 * @param index
+	 * @return
+	 */
+	public ImageFileset get(int index) {
+		if ((list == null) || (list.get(0) == null) || (index >= list.get(0).size())) {
+			return null;
+		}
+		return (ImageFileset) list.get(0).get(index);
+	}
+
+	/**
+	 * Returns the file of the class at the index.
+	 * 
+	 * @param index
+	 * @param fc
+	 *            fileClass
+	 * @return
+	 */
+	public DocuDirent get(int index, int fc) {
+		if ((list == null) || (list.get(fc) == null) || (index >= list.get(fc).size())) {
+			return null;
+		}
+		return (DocuDirent) list.get(fc).get(index);
+	}
+
+	/**
+	 * Checks if the directory exists on the filesystem.
+	 * 
+	 * Sets isValid.
+	 * 
+	 * @return
+	 */
+	public boolean checkDir() {
+		if (dir == null) {
+			initDir();
+		}
+		isValid = dir.isDirectory();
+		return isValid;
+	}
+
+	/**
+	 * Read the filesystem directory and fill this object.
+	 * 
+	 * Clears the List and (re)reads all files.
+	 * 
+	 * @return boolean the directory exists
+	 */
+	public synchronized boolean readDir() {
+		// check directory first
+		checkDir();
+		if (!isValid) {
+			return false;
+		}
+		// first file extension to try for scaled directories
+		String scalext = null;
+		// read all filenames
+		logger.debug("reading directory " + dir.getPath());
+		/*
+		 * using ReadableFileFilter is safer (we won't get directories with file
+		 * extensions) but slower.
+		 */
+		File[] allFiles = null;
+		if (cache.safeDirIndex) {
+			allFiles = dir.listFiles(new FileOps.ReadableFileFilter());
+		} else {
+			allFiles = dir.listFiles();
+		}
+		//logger.debug("  done");
+		if (allFiles == null) {
+			// not a directory
+			return false;
+		}
+		// list of base dirs from the parent cache
+		String[] baseDirNames = cache.getBaseDirNames();
+		// number of base dirs
+		int nb = baseDirNames.length;
+		// array of base dirs
+		Directory[] dirs = new Directory[nb];
+		// first entry is this directory
+		dirs[0] = this;
+		// fill array with the remaining directories
+		for (int j = 1; j < nb; j++) {
+			File d = new File(baseDirNames[j], dirName);
+			if (d.isDirectory()) {
+				dirs[j] = new Directory(d);
+				logger.debug("  reading scaled directory " + d.getPath());
+				dirs[j].readDir();
+				//logger.debug("    done");
+			}
+		}
+
+		// go through all file classes
+		for (int classIdx = 0; classIdx < FileOps.NUM_CLASSES; classIdx++) {
+			int fileClass = cache.getFileClasses()[classIdx];
+			//logger.debug("filtering directory "+dir.getPath()+" for class
+			// "+fc);
+			File[] fileList = FileOps.listFiles(allFiles, FileOps
+					.filterForClass(fileClass));
+			//logger.debug(" done");
+			// number of files in the directory
+			int numFiles = fileList.length;
+			if (numFiles > 0) {
+				// create new list
+				list.set(fileClass, new ArrayList<DocuDirent>(numFiles));
+				// sort the file names alphabetically and iterate the list
+				// Arrays.sort(fileList); // not needed <hertzhaft>
+				Map<Integer, Object> hints = FileOps.newHints(FileOps.HINT_BASEDIRS, dirs);
+				hints.put(FileOps.HINT_FILEEXT, scalext);
+				for (int i = 0; i < numFiles; i++) {
+					DocuDirent f = FileOps.fileForClass(fileClass, fileList[i],
+							hints);
+					// add the file to our list
+                    // logger.debug(f.getName());
+
+					list.get(fileClass).add(f);
+					f.setParent(this);
+				}
+                // we sort the inner ArrayList (the list of files not the list of file types)
+				// for binarySearch to work (DocuDirent's natural sort order is by filename)
+                Collections.sort(list.get(fileClass));
+			}
+		}
+		// clear the scaled directories
+		for (int j = 1; j < nb; j++) {
+			if (dirs[j] != null) {
+				dirs[j].clearFilenames();
+			}
+		}
+		// update number of cached files if this was the first time
+		if (dirMTime == 0) {
+			cache.numFiles += size();
+		}
+		dirMTime = dir.lastModified();
+		// read metadata as well
+		readMeta();
+		return isValid;
+	}
+
+	/**
+	 * Check to see if the directory has been modified and reread if necessary.
+	 * 
+	 * @return boolean the directory is valid
+	 */
+	public boolean refresh() {
+		if (isValid) {
+			if (dir.lastModified() > dirMTime) {
+				// on-disk modification time is more recent
+				readDir();
+			}
+			touch();
+		}
+		return isValid;
+	}
+
+	/**
+	 * Read directory metadata.
+	 *  
+	 */
+	public void readMeta() {
+		// check for directory metadata...
+		File mf = new File(dir, "index.meta");
+		if (mf.canRead()) {
+			XMLMetaLoader ml = new XMLMetaLoader();
+			try {
+				// read directory meta file
+				Map<String, MetadataMap> fileMeta = ml.loadURL(mf.getAbsolutePath());
+				if (fileMeta == null) {
+					return;
+				}
+				// meta for the directory itself is in the "" bin
+				dirMeta = fileMeta.remove("");
+				// read meta for files in this directory
+				readFileMeta(fileMeta, null);
+				// is there meta for other files left?
+				if (fileMeta.size() > 0) {
+					unresolvedFileMeta = fileMeta;
+				}
+			} catch (SAXException e) {
+				logger.warn("error parsing index.meta", e);
+			} catch (IOException e) {
+				logger.warn("error reading index.meta", e);
+			}
+		}
+		readParentMeta();
+		metaChecked = true;
+	}
+
+	/**
+	 * Read metadata from all known parent directories.
+	 *  
+	 */
+	public void readParentMeta() {
+		// check the parent directories for additional file meta
+		Directory dd = parent;
+		String path = dir.getName();
+		while (dd != null) {
+			if (((DocuDirectory) dd).hasUnresolvedFileMeta()) {
+				readFileMeta(((DocuDirectory) dd).unresolvedFileMeta, path);
+			}
+			// prepend parent dir path
+			path = dd.dir.getName() + "/" + path;
+			// become next parent
+			dd = dd.parent;
+		}
+	}
+
+	/**
+	 * Read metadata for the files in this directory.
+	 * 
+	 * Takes a Map with meta-information, adding the relative path before the
+	 * lookup.
+	 * 
+	 * @param fileMeta
+	 * @param relPath
+	 * @param fc
+	 *            fileClass
+	 */
+	protected void readFileMeta(Map<String,MetadataMap> fileMeta, String relPath) {
+		if (list == null) {
+			// there are no files
+			return;
+		}
+		String path = (relPath != null) ? (relPath + "/") : "";
+		// go through all file classes
+		for (int nc = 0; nc < list.size(); nc++) {
+			int fc = cache.getFileClasses()[nc];
+			if (list.get(fc) == null) {
+				continue;
+			}
+			// iterate through the list of files in this directory
+			for (DocuDirent f: list.get(fc)) {
+				// prepend path to the filename
+				String fn = path + f.getName();
+				// look up meta for this file and remove from dir
+				MetadataMap meta = fileMeta.remove(fn);
+				if (meta != null) {
+					// store meta in file
+					f.setFileMeta(meta);
+				}
+			}
+		}
+	}
+
+	protected void notifyChildMeta(MetadataMap childmeta) {
+		List<DocuDirectory> children = cache.getChildren(this.dirName, true);
+		if (children.size() > 0) {
+			/*for (DocuDirectory d: children) {
+				// TODO: finish this!
+				//((DocuDirectory) i.next()).readFileMeta()
+			}*/
+		}
+	}
+
+	/**
+	 * Update access time.
+	 * 
+	 * @return long time of last access.
+	 */
+	public long touch() {
+		long t = objectATime;
+		objectATime = System.currentTimeMillis();
+		return t;
+	}
+
+	/**
+	 * Searches for the file with the name <code>fn</code>.
+	 * 
+	 * Searches the directory for the file with the name <code>fn</code> and
+	 * returns its index. Returns -1 if the file cannot be found.
+	 * 
+	 * @param fn
+	 *            filename
+	 * @param fc
+	 *            file class
+	 * @return int index of file <code>fn</code>
+	 */
+	public int indexOf(String fn) {
+		int fc = FileOps.classForFilename(fn);
+		return indexOf(fn, fc);
+	}
+
+	/**
+	 * Searches for the file with the name <code>fn</code> and class fc.
+	 * 
+	 * Searches the directory for the file with the name <code>fn</code> and
+	 * returns its index. Returns -1 if the file cannot be found.
+	 * 
+	 * @param fn
+	 *            filename
+	 * @return int index of file <code>fn</code>
+	 */
+	public int indexOf(String fn, int fc) {
+		if (!isRead()) {
+			// read directory now
+			if (!readDir()) {
+				return -1;
+			}
+		}
+		List<DocuDirent> fileList = list.get(fc);
+		// empty directory?
+		if (fileList == null) {
+			return -1;
+		}
+        
+		// search for exact match (DocuDirent does compareTo<String>)
+        // OBS: fileList needs to be sorted first (see )! <hertzhaft>
+		int idx = Collections.binarySearch(fileList, fn);
+		if (idx >= 0) {
+			return idx;
+		} else {
+            logger.debug(fn + " not found by binarysearch");
+			// try closest matches without extension
+			idx = -idx - 1;
+			if ((idx < fileList.size())
+					&& isBaseInList(fileList, idx, fn)) {
+				// idx matches
+				return idx;
+			} else if ((idx > 0)
+					&& isBaseInList(fileList, idx-1, fn)) {
+				// idx-1 matches
+				return idx - 1;
+			} else if ((idx + 1 < fileList.size())
+					&& isBaseInList(fileList, idx+1, fn)) {
+				// idx+1 matches
+				return idx + 1;
+			}
+
+		}
+		return -1;
+	}
+
+	private boolean isBaseInList(List<DocuDirent> fl, int idx, String fn) {
+		String dfn = FileOps.basename((fl.get(idx))
+				.getName());
+		return (dfn.equals(fn)||dfn.equals(FileOps.basename(fn))); 
+	}
+	
+	
+	/**
+	 * Finds the DocuDirent with the name <code>fn</code>.
+	 * 
+	 * Searches the directory for the DocuDirent with the name <code>fn</code>
+	 * and returns it. Returns null if the file cannot be found.
+	 * 
+	 * @param fn
+	 *            filename
+	 * @return DocuDirent
+	 */
+	public DocuDirent find(String fn) {
+		int fc = FileOps.classForFilename(fn);
+		int i = indexOf(fn, fc);
+		if (i >= 0) {
+			return (DocuDirent) list.get(0).get(i);
+		}
+		return null;
+	}
+
+	/**
+	 * Finds the DocuDirent with the name <code>fn</code> and class
+	 * <code>fc</code>.
+	 * 
+	 * Searches the directory for the DocuDirent with the name <code>fn</code>
+	 * and returns it. Returns null if the file cannot be found.
+	 * 
+	 * @param fn
+	 *            filename
+	 * @return DocuDirent
+	 */
+	public DocuDirent find(String fn, int fc) {
+		int i = indexOf(fn, fc);
+		if (i >= 0) {
+			return (DocuDirent) list.get(fc).get(i);
+		}
+		return null;
+	}
+
+	/**
+	 * Returns the digilib canonical name.
+	 * 
+	 * @return
+	 */
+	public String getDirName() {
+		return dirName;
+	}
+
+	/**
+	 * The directory is valid (exists on disk).
+	 * 
+	 * @return boolean
+	 */
+	public boolean isValid() {
+		return isValid;
+	}
+
+	/**
+	 * The directory has been read from disk.
+	 * 
+	 * @return
+	 */
+	public boolean isRead() {
+		return (dirMTime != 0);
+	}
+
+	/**
+	 * @return long
+	 */
+	public long getAccessTime() {
+		return objectATime;
+	}
+
+	/**
+	 * @return Hashtable
+	 */
+	public MetadataMap getDirMeta() {
+		return dirMeta;
+	}
+
+	/**
+	 * Checks metadata
+	 *  
+	 */
+	public void checkMeta() {
+		if (metaChecked) {
+			return;
+		} else {
+			readMeta();
+		}
+	}
+
+	/**
+	 * @return long
+	 */
+	public long getDirMTime() {
+		return dirMTime;
+	}
+
+	/**
+	 * Sets the dirMeta.
+	 * 
+	 * @param dirMeta
+	 *            The dirMeta to set
+	 */
+	public void setDirMeta(MetadataMap dirMeta) {
+		this.dirMeta = dirMeta;
+	}
+
+	public boolean hasUnresolvedFileMeta() {
+		return (this.unresolvedFileMeta != null);
+	}
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/servlet/src/digilib/io/DocuDirent.java	Fri Feb 05 20:58:38 2010 +0100
@@ -0,0 +1,167 @@
+/*
+ * DocuDirent.java -- Abstract directory entry in a DocuDirectory
+ * 
+ * Digital Image Library servlet components
+ * 
+ * Copyright (C) 2003 Robert Casties (robcast@mail.berlios.de)
+ * 
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the Free
+ * Software Foundation; either version 2 of the License, or (at your option)
+ * any later version.
+ * 
+ * Please read license.txt for the full details. A copy of the GPL may be found
+ * at http://www.gnu.org/copyleft/lgpl.html
+ * 
+ * You should have received a copy of the GNU General Public License along with
+ * this program; if not, write to the Free Software Foundation, Inc., 59 Temple
+ * Place, Suite 330, Boston, MA 02111-1307 USA
+ * 
+ * Created on 15.09.2003 by casties
+ *  
+ */
+package digilib.io;
+
+import java.io.File;
+import java.util.Map;
+
+import org.apache.log4j.Logger;
+
+/**
+ * Abstract directory entry in a DocuDirectory.
+ * 
+ * @author casties
+ *  
+ */
+public abstract class DocuDirent implements Comparable<Object> {
+
+	/** the file class of this file */
+	protected static int fileClass = FileOps.CLASS_NONE;
+	/** HashMap with metadata */
+	protected MetadataMap fileMeta = null;
+	/** Is the Metadata valid */
+	protected boolean metaChecked = false;
+	/** the parent directory */
+	protected Directory parent = null;
+
+	/**
+	 * Checks metadata and does something with it.
+	 *  
+	 */
+	public abstract void checkMeta();
+
+	/**
+	 * gets the (default) File
+	 * 
+	 * @return
+	 */
+	public abstract File getFile();
+
+	/**
+	 * Reads meta-data for this Fileset if there is any.
+	 *  
+	 */
+	public void readMeta() {
+		if ((fileMeta != null) || (getFile() == null)) {
+			// there is already metadata or there is no file
+			return;
+		}
+		// metadata is in the file {filename}.meta
+		String fn = getFile().getAbsolutePath();
+		File mf = new File(fn + ".meta");
+		if (mf.canRead()) {
+			XMLMetaLoader ml = new XMLMetaLoader();
+			try {
+				// read meta file
+				Map<String, MetadataMap> meta = ml.loadURL(mf.getAbsolutePath());
+				if (meta == null) {
+					return;
+				}
+				fileMeta = meta.get(getName());
+			} catch (Exception e) {
+				Logger.getLogger(this.getClass()).warn("error reading file .meta", e);
+			}
+		}
+	}
+
+	/**
+	 * The name of the file.
+	 * 
+	 * If this is a Fileset, the method returns the name of the default file
+	 * (for image filesets the highest resolution file).
+	 * 
+	 * @return
+	 */
+	public String getName() {
+		File f = getFile();
+		return (f != null) ? f.getName() : null;
+	} 
+	
+	/**
+	 * Returns the parent Directory.
+	 * 
+	 * @return DocuDirectory
+	 */
+	public Directory getParent() {
+		return parent;
+	}
+	
+	/**
+	 * Sets the parent Directory.
+	 * 
+	 * @param parent
+	 *            The parent to set
+	 */
+	public void setParent(Directory parent) {
+		this.parent = parent;
+	} 
+	
+	/**
+	 * Returns the meta-data for this file(set).
+	 * 
+	 * @return HashMap
+	 */
+	public MetadataMap getFileMeta() {
+		return fileMeta;
+	} 
+	
+	/**
+	 * Sets the meta-data for this file(set) .
+	 * 
+	 * @param fileMeta
+	 *            The fileMeta to set
+	 */
+	public void setFileMeta(MetadataMap fileMeta) {
+		this.fileMeta = fileMeta;
+	} 
+	
+	/**
+	 * @return
+	 */
+	public boolean isMetaChecked() {
+		return metaChecked;
+	} 
+	
+	/**
+	 * @return
+	 */
+	public static int getFileClass() {
+		return fileClass;
+	}
+
+	/** Comparator using the file name.
+	 * Compares to a String (for binarySearch)
+     * or to another DocuDirent (for sort)
+	 * 
+	 * @see java.lang.Comparable#compareTo(java.lang.Object)
+	 */
+	public int compareTo(Object arg0) {
+		if (arg0 instanceof DocuDirent) {
+		    return getName().compareTo(((DocuDirent) arg0).getName());
+		} else {
+		    return getName().compareTo((String) arg0);
+		}
+	}
+
+	
+}
--- a/servlet/src/digilib/io/FileOps.java	Thu Jan 14 14:30:30 2010 +0100
+++ b/servlet/src/digilib/io/FileOps.java	Fri Feb 05 20:58:38 2010 +0100
@@ -1,188 +1,403 @@
-/*  FileOps -- Utility class for file operations
-
-  Digital Image Library servlet components
-
-  Copyright (C) 2001, 2002 Robert Casties (robcast@mail.berlios.de)
-
-  This program is free software; you can redistribute  it and/or modify it
-  under  the terms of  the GNU General  Public License as published by the
-  Free Software Foundation;  either version 2 of the  License, or (at your
-  option) any later version.
-   
-  Please read license.txt for the full details. A copy of the GPL
-  may be found at http://www.gnu.org/copyleft/lgpl.html
-
-  You should have received a copy of the GNU General Public License
-  along with this program; if not, write to the Free Software
-  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
-
-*/
+/*
+ * FileOps -- Utility class for file operations
+ * 
+ * Digital Image Library servlet components
+ * 
+ * Copyright (C) 2001, 2002 Robert Casties (robcast@mail.berlios.de)
+ * 
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the Free
+ * Software Foundation; either version 2 of the License, or (at your option)
+ * any later version.
+ * 
+ * Please read license.txt for the full details. A copy of the GPL may be found
+ * at http://www.gnu.org/copyleft/lgpl.html
+ * 
+ * You should have received a copy of the GNU General Public License along with
+ * this program; if not, write to the Free Software Foundation, Inc., 59 Temple
+ * Place, Suite 330, Boston, MA 02111-1307 USA
+ *  
+ */
 
 package digilib.io;
 
-import java.io.*;
-import java.util.*;
-
-import digilib.*;
-
+import java.io.File;
+import java.io.FileFilter;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.StringTokenizer;
 
 public class FileOps {
 
-  private Utils util = null;
-  public static String[] fileTypes = {
-            "jpg", "image/jpeg",
-            "jpeg", "image/jpeg",
-            "png", "image/png",
-            "gif", "image/gif",
-            "tif", "image/tiff",
-            "tiff", "image/tiff"};
+	/**
+	 * Array of file extensions and corresponding mime-types.
+	 */
+	private static final String[][] ft = { { "jpg", "image/jpeg" },
+			{ "jpeg", "image/jpeg" }, { "jp2", "image/jp2" },
+			{ "png", "image/png" }, { "gif", "image/gif" },
+			{ "tif", "image/tiff" }, { "tiff", "image/tiff" },
+                        { "fpx", "image/fpx" },
+			{ "txt", "text/plain" }, { "html", "text/html" },
+			{ "htm", "text/html" }, { "xml", "text/xml" },
+			{ "svg", "image/svg+xml" }, { "meta", "text/xml" } };
+
+	public static Map<String, String> fileTypes;
+
+	public static List<String> imageExtensions;
+
+	public static List<String> textExtensions;
+
+	public static List<String> svgExtensions;
+
+	public static final int CLASS_NONE = -1;
+
+	public static final int CLASS_IMAGE = 0;
+
+	public static final int CLASS_TEXT = 1;
+
+	public static final int CLASS_SVG = 2;
+
+	public static final int NUM_CLASSES = 3;
+
+	public static final Integer HINT_BASEDIRS = new Integer(1);
+
+	public static final Integer HINT_FILEEXT = new Integer(2);
+
+	public static final Integer HINT_DIRS = new Integer(3);
+
+	/**
+	 * static initializer for FileOps
+	 */
+	static {
+		fileTypes = new HashMap<String, String>();
+		imageExtensions = new ArrayList<String>();
+		textExtensions = new ArrayList<String>();
+		svgExtensions = new ArrayList<String>();
+		// iterate through file types in ft and fill the Map and Lists
+		for (int i = 0; i < ft.length; i++) {
+			String ext = ft[i][0];
+			String mt = ft[i][1];
+			fileTypes.put(ext, mt);
+			if (classForMimetype(mt) == CLASS_IMAGE) {
+				imageExtensions.add(ext);
+			} else if (classForMimetype(mt) == CLASS_TEXT) {
+				textExtensions.add(ext);
+			} else if (classForMimetype(mt) == CLASS_SVG) {
+				svgExtensions.add(ext);
+			}
+		}
+	}
 
-  public FileOps() {
-    util = new Utils();
-  }
+	/**
+	 * returns the file class for a mime-type
+	 * 
+	 * @param mt
+	 * @return
+	 */
+	public static int classForMimetype(String mt) {
+		if (mt == null) {
+			return CLASS_NONE;
+		}
+		if (mt.startsWith("image/svg")) {
+			return CLASS_SVG;
+		} else if (mt.startsWith("image")) {
+			return CLASS_IMAGE;
+		} else if (mt.startsWith("text")) {
+			return CLASS_TEXT;
+		}
+		return CLASS_NONE;
+	}
+
+	/**
+	 * get the mime type for a file format (by extension)
+	 */
+	public static String mimeForFile(File f) {
+		return (String) fileTypes.get(extname(f.getName().toLowerCase()));
+	}
 
-  public FileOps(Utils u) {
-    util = u;
-  }
+	/**
+	 * get the file class for the filename (by extension)
+	 * 
+	 * @param fn
+	 * @return
+	 */
+	public static int classForFilename(String fn) {
+		String mt = (String) fileTypes.get(extname(fn).toLowerCase());
+		return classForMimetype(mt);
+	}
+
+	public static Iterator<String> getImageExtensionIterator() {
+		return imageExtensions.iterator();
+	}
 
-  public void setUtils(Utils u) {
-    util = u;
-  }
+    public static List<String> getImageExtensions() {
+        return imageExtensions;
+    }
+
+    public static Iterator<String> getTextExtensionIterator() {
+		return textExtensions.iterator();
+	}
+
+	public static List<String> getTextExtensions() {
+        return textExtensions;
+    }
+
+    public static Iterator<String> getSVGExtensionIterator() {
+		return svgExtensions.iterator();
+	}
+
+    public static List<String> getSvgExtensions() {
+        return svgExtensions;
+    }
 
 
-  /**
-   *  get the mime type for a file format (by extension)
-   */
-  public static String mimeForFile(File f) {
-    String fn = f.getName();
-    for (int i = 0; i < fileTypes.length; i += 2) {
-      if (fn.toLowerCase().endsWith(fileTypes[i])) {
-        return fileTypes[i+1];
-      }
-    }
-    return null;
-  }
+	/**
+	 * convert a string with a list of pathnames into an array of strings using
+	 * the system's path separator string
+	 */
+	public static String[] pathToArray(String paths) {
+		// split list into directories
+		StringTokenizer dirs = new StringTokenizer(paths, File.pathSeparator);
+		int n = dirs.countTokens();
+		if (n < 1) {
+			return null;
+		}
+		// add directories into array
+		String[] pathArray = new String[n];
+		for (int i = 0; i < n; i++) {
+			String s = dirs.nextToken();
+			// make shure the dir name ends with a directory separator
+			if (s.endsWith(File.separator)) {
+				pathArray[i] = s;
+			} else {
+				pathArray[i] = s + File.separator;
+			}
+		}
+		return pathArray;
+	}
 
-  /**
-   *  get a filehandle for a file or directory name
-   *    returns File number n if fn is directory (starts with 1)
-   */
-  public File getFile(String fn, int n) throws FileOpException {
-    util.dprintln(4, "getFile ("+fn+", "+n+")");
+	/**
+	 * Extract the base of a file name (sans extension).
+	 * 
+	 * Returns the filename without the extension. The extension is the part
+	 * behind the last dot in the filename. If the filename has no dot the full
+	 * file name is returned.
+	 * 
+	 * @param fn
+	 * @return
+	 */
+	public static String basename(String fn) {
+		if (fn == null) {
+			return null;
+		}
+		int i = fn.lastIndexOf('.');
+		if (i > 0) {
+			return fn.substring(0, i);
+		}
+		return fn;
+	}
 
-    File f = new File(fn);
-    // if fn is a file name then return file
-    if (f.isFile()) {
-      return f;
-    }
-    // if fn is a directory name then open directory
-    if (f.isDirectory()) {
-      File[] fl = f.listFiles(new ImgFileFilter());
-      Arrays.sort(fl);
-      if ((n > 0) && (n <= fl.length)) {
-         return fl[n - 1];
-      }
-    }
-    throw new FileOpException("Unable to find file: "+fn);
-  }
+	/**
+	 * Extract the extension of a file name.
+	 * 
+	 * Returns the extension of a file name. The extension is the part behind
+	 * the last dot in the filename. If the filename has no dot the empty string
+	 * is returned.
+	 * 
+	 * @param fn
+	 * @return
+	 */
+	public static String extname(String fn) {
+		if (fn == null) {
+			return null;
+		}
+		int i = fn.lastIndexOf('.');
+		if (i > 0) {
+			return fn.substring(i + 1);
+		}
+		return "";
+	}
 
-  /**
-   *  get the number of files in a directory
-   *    (almost the same as getFile)
-   *  returns 0 in case of problems
-   */
-  public int getNumFiles(String fn) throws FileOpException {
-    util.dprintln(4, "getNumFiles ("+fn+")");
+	/**
+	 * Extract the parent directory of a (digilib) path name.
+	 * 
+	 * Returns the parent directory of a path name. The parent is the part
+	 * before the last slash in the path name. If the path name has no slash the
+	 * empty string is returned.
+	 * 
+	 * @param fn
+	 * @return
+	 */
+	public static String parent(String fn) {
+		if (fn == null) {
+			return null;
+		}
+		int i = fn.lastIndexOf('/');
+		if (i > 0) {
+			return fn.substring(0, i);
+		}
+		return "";
+	}
 
-    File f = new File(fn);
-    // if fn is a file name then return 1
-    if (f.isFile()) {
-      return 1;
-    }
-    // if fn is a directory name then return the number of files
-    if (f.isDirectory()) {
-      return f.listFiles(new ImgFileFilter()).length;
-    }
-    // then fn must be something strange...
-    return 0;
-  }
-
-
-  /**
-   *  get a filehandle for a file or directory name out of a list
-   *    dirs is a list of base directories, fn is the appended file/dirname
-   *    searches dirs until fn exists (backwards if fwd is false)
-   *    returns File number n if fn is directory (starts with 1)
-   */
-  public File getFileVariant(String[] dirs, String fn, int n, boolean fwd) throws FileOpException {
-    util.dprintln(4, "getVariantFile ("+dirs+", "+fn+", "+n+")");
+	/**
+	 * Normalize a path name.
+	 * 
+	 * Removes leading and trailing slashes. Returns null if there is other
+	 * unwanted stuff in the path name.
+	 * 
+	 * @param pathname
+	 * @return
+	 */
+	public static String normalName(String pathname) {
+		if (pathname == null) {
+			return null;
+		}
+		// upper-dir references are unwanted
+		if (pathname.indexOf("../") >= 0) {
+			return null;
+		}
+		int a = 0;
+		int e = pathname.length() - 1;
+		if (e < 0) {
+			return pathname;
+		}
+		// leading and trailing "/" are removed
+		while ((a <= e) && (pathname.charAt(a) == '/')) {
+			a++;
+		}
+		while ((a < e) && (pathname.charAt(e) == '/')) {
+			e--;
+		}
+		return pathname.substring(a, e + 1);
+	}
 
-    File f = null;
-    int start = 0;
-    int inc = 1;
-    int end = dirs.length;
-    if (fwd == false) {
-      start = dirs.length - 1;
-      inc = -1;
-      end = 0;
-    }
+	/**
+	 * FileFilter for general files
+	 */
+	static class ReadableFileFilter implements FileFilter {
+
+		public boolean accept(File f) {
+			return f.canRead();
+		}
+	}
+
+	/**
+	 * FileFilter for image types (helper class for getFile)
+	 */
+	static class ImageFileFilter implements FileFilter {
+
+		public boolean accept(File f) {
+			return (classForFilename(f.getName()) == CLASS_IMAGE);
+		}
+	}
+
+	/**
+	 * FileFilter for text types (helper class for getFile)
+	 */
+	static class TextFileFilter implements FileFilter {
+
+		public boolean accept(File f) {
+			return (classForFilename(f.getName()) == CLASS_TEXT);
+		}
+	}
 
-    for (int i = start; i != end; i += inc) {
-      try {
-        f = getFile(dirs[i]+fn, n);
-      } catch (FileOpException e) {
-        f = null;
-      }
-      if (f != null) {
-        return f;
-      }
-    }
-    throw new FileOpException("Unable to find file: "+fn);
-  }
+	/**
+	 * FileFilter for svg types (helper class for getFile).
+	 *  
+	 */
+	static class SVGFileFilter implements FileFilter {
+
+		public boolean accept(File f) {
+			return (classForFilename(f.getName()) == CLASS_SVG);
+		}
+	}
 
-  /**
-   *  get the number of files in a directory
-   *    (almost the same as getFileVariant)
-   *  returns 0 in case of problems
-   */
-  public int getNumFilesVariant(String[] dirs, String fn, boolean fwd) throws FileOpException {
-    util.dprintln(4, "getNumFilesVariant ("+dirs+", "+fn+")");
+	/**
+	 * Factory for FileFilters (image or text).
+	 * 
+	 * @param fileClass
+	 * @return
+	 */
+	public static FileFilter filterForClass(int fileClass) {
+		if (fileClass == CLASS_IMAGE) {
+			return new ImageFileFilter();
+		}
+		if (fileClass == CLASS_TEXT) {
+			return new TextFileFilter();
+		}
+		if (fileClass == CLASS_SVG) {
+			return new SVGFileFilter();
+		}
+		return null;
+	}
 
-    int nf = 0;
-    int start = 0;
-    int inc = 1;
-    int end = dirs.length;
-    if (fwd == false) {
-      start = dirs.length - 1;
-      inc = -1;
-      end = 0;
-    }
+	/**
+	 * Factory for DocuDirents based on file class.
+	 * 
+	 * Returns an ImageFileset, TextFile or SVGFile. baseDirs and scalext are
+	 * only for ImageFilesets.
+	 * 
+	 * @param fileClass
+	 * @param file
+	 * @param hints
+	 *            optional additional parameters
+	 * @return
+	 */
+	public static DocuDirent fileForClass(int fileClass, File file, Map<Integer,Object> hints) {
+		// what class of file do we have?
+		if (fileClass == CLASS_IMAGE) {
+			// image file
+			return new ImageFileset(file, hints);
+		} else if (fileClass == CLASS_TEXT) {
+			// text file
+			return new TextFile(file);
+		} else if (fileClass == CLASS_SVG) {
+			// text file
+			return new SVGFile(file);
+		}
+		return null;
+	}
 
-    for (int i = start; i != end; i += inc) {
-      try {
-        nf = getNumFiles(dirs[i]+fn);
-      } catch (FileOpException e) {
-        nf = 0;
-      }
-      if (nf > 0) {
-        return nf;
-      }
-    }
-    return 0;
-  }
+	/**
+	 * Filters a list of Files through a FileFilter.
+	 * 
+	 * @param files
+	 * @param filter
+	 * @return
+	 */
+	public static File[] listFiles(File[] files, FileFilter filter) {
+		if (files == null) {
+			return null;
+		}
+		File[] ff = new File[files.length];
+		int ffi = 0;
+		for (int i = 0; i < files.length; i++) {
+			if (filter.accept(files[i])) {
+				ff[ffi] = files[i];
+				ffi++;
+			}
+		}
+		File[] fff = new File[ffi];
+		System.arraycopy(ff, 0, fff, 0, ffi);
+		return fff;
+	}
 
-  /**
-   *  FileFilter for image types (helper class for getFile)
-   */
-  private class ImgFileFilter implements FileFilter {
-
-    public boolean accept(File f) {
-      if (f.isFile()) {
-        return (mimeForFile(f) != null);
-      } else {
-        return false;
-      }
-    }
-  }
+	/**
+	 * Creates a new hints Map with the given first element.
+	 * 
+	 * @param type
+	 * @param value
+	 * @return
+	 */
+	public static Map<Integer, Object> newHints(Integer type, Object value) {
+		Map<Integer, Object> m = new HashMap<Integer, Object>();
+		if (type != null) {
+			m.put(type, value);
+		}
+		return m;
+	}
 
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/servlet/src/digilib/io/ImageFileset.java	Fri Feb 05 20:58:38 2010 +0100
@@ -0,0 +1,413 @@
+/* ImageFileset -- digilib image file info class.  
+ * Digital Image Library servlet components  
+ * Copyright (C) 2003 Robert Casties (robcast@mail.berlios.de)  
+ * 
+ * This program is free software; you can
+ * redistribute it and/or modify it under the terms of the GNU General Public
+ * License as published by the Free Software Foundation; either version 2 of
+ * the License, or (at your option) any later version.  
+ * 
+ * Please read license.txt for the full details. A copy of the GPL may be 
+ * found at http://www.gnu.org/copyleft/lgpl.html  
+ * 
+ * You should have received a copy of the GNU General Public License along 
+ * with this program; if not, write to the Free Software Foundation, Inc., 
+ * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA  
+ */
+
+package digilib.io;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.Map;
+
+import digilib.image.ImageOps;
+import digilib.image.ImageSize;
+
+/**
+ * @author casties
+ */
+public class ImageFileset extends DocuDirent {
+
+	/** this is an image file */
+	protected static int fileClass = FileOps.CLASS_IMAGE;
+	
+	/** list of files (ImageFile) */
+	private List<ImageFile> list = null;
+
+	/** aspect ratio (width/height) */
+	private float aspect = 0;
+
+	/** resolution of the biggest image (DPI) */
+	private float resX = 0;
+
+	/** resolution of the biggest image (DPI) */
+	private float resY = 0;
+
+	/**
+	 * Creator for empty fileset.
+	 * 
+	 * 
+	 * @param initialCapacity
+	 */
+	public ImageFileset() {
+		list = new ArrayList<ImageFile>();
+	}
+
+	/**
+	 * Constructor with a file and hints.
+	 * 
+	 * The hints are expected to contain 'basedirs' and 'scaledfilext' keys.
+	 * 
+	 * @param file
+	 * @param hints
+	 */
+	public ImageFileset(File file, Map<Integer,Object> hints) {
+		Directory[] dirs = (Directory[]) hints.get(FileOps.HINT_BASEDIRS);
+		int nb = dirs.length;
+		list = new ArrayList<ImageFile>(nb);
+		parent = dirs[0];
+		fill(dirs, file, hints);
+	}
+
+	/**
+	 * Adds an ImageFile to this Fileset.
+	 * 
+	 * The files should be added in the order of higher to lower resolutions.
+	 * The first file is considered the hires "original".
+	 * 
+	 * 
+	 * @param f
+	 *            file to add
+	 * @return true (always)
+	 */
+	public boolean add(ImageFile f) {
+		f.setParent(this);
+		return list.add(f);
+	}
+
+	/**
+	 * The number of image files in this Fileset.
+	 * 
+	 * 
+	 * @return number of image files
+	 */
+	public int size() {
+		return (list != null) ? list.size() : 0;
+	}
+
+	/**
+	 * Gets the default File.
+	 *  
+	 */
+	public File getFile() {
+		return (list != null) ? list.get(0).getFile() : null;
+	}
+
+	/**
+	 * Get the ImageFile at the index.
+	 * 
+	 * 
+	 * @param index
+	 * @return
+	 */
+	public ImageFile get(int index) {
+		return list.get(index);
+	}
+
+	/**
+	 * Get the next smaller ImageFile than the given size.
+	 * 
+	 * Returns the ImageFile from the set that has a width and height smaller or
+	 * equal the given size. Returns null if there isn't any smaller image.
+	 * Needs DocuInfo instance to checkFile().
+	 * 
+	 * 
+	 * @param size
+	 * @param info
+	 * @return
+	 */
+	public ImageFile getNextSmaller(ImageSize size) {
+		for (Iterator<ImageFile> i = getHiresIterator(); i.hasNext();) {
+			ImageFile f = i.next();
+			try {
+				if (!f.isChecked()) {
+					ImageOps.checkFile(f);
+				}
+				if (f.getSize().isTotallySmallerThan(size)) {
+					return f;
+				}
+			} catch (IOException e) {
+			}
+		}
+		return null;
+	}
+
+	/**
+	 * Get the next bigger ImageFile than the given size.
+	 * 
+	 * Returns the ImageFile from the set that has a width or height bigger or
+	 * equal the given size. Returns null if there isn't any bigger image. Needs
+	 * DocuInfo instance to checkFile().
+	 * 
+	 * 
+	 * @param size
+	 * @param info
+	 * @return
+	 */
+	public ImageFile getNextBigger(ImageSize size) {
+		for (ListIterator<ImageFile> i = getLoresIterator(); i.hasPrevious();) {
+			ImageFile f = i.previous();
+			try {
+				if (!f.isChecked()) {
+					ImageOps.checkFile(f);
+				}
+				if (f.getSize().isBiggerThan(size)) {
+					return f;
+				}
+			} catch (IOException e) {
+			}
+		}
+		return null;
+	}
+
+	/**
+	 * Returns the biggest ImageFile in the set.
+	 * 
+	 * 
+	 * @return
+	 */
+	public ImageFile getBiggest() {
+		return this.get(0);
+	}
+
+	/**
+	 * Returns the biggest ImageFile in the set.
+	 * 
+	 * 
+	 * @return
+	 */
+	public ImageFile getSmallest() {
+		return this.get(this.size() - 1);
+	}
+
+	/**
+	 * Get an Iterator for this Fileset starting at the highest resolution
+	 * images.
+	 * 
+	 * 
+	 * @return
+	 */
+	public ListIterator<ImageFile> getHiresIterator() {
+		return list.listIterator();
+	}
+
+	/**
+	 * Get an Iterator for this Fileset starting at the lowest resolution
+	 * images.
+	 * 
+	 * The Iterator starts at the last element, so you have to use it backwards
+	 * with hasPrevious() and previous().
+	 * 
+	 * 
+	 * @return
+	 */
+	public ListIterator<ImageFile> getLoresIterator() {
+		return list.listIterator(list.size());
+	}
+
+	/**
+	 * Fill the ImageFileset with files from different base directories.
+	 * 
+	 * 
+	 * @param dirs
+	 *            list of base directories
+	 * @param fl
+	 *            file (from first base dir)
+	 * @param hints
+	 *  
+	 */
+	void fill(Directory[] dirs, File fl, Map<Integer,Object> hints) {
+		int nb = dirs.length;
+		String fn = fl.getName();
+		String baseFn = FileOps.basename(fn);
+		// add the first ImageFile to the ImageFileset
+		add(new ImageFile(fn, this, parent));
+		// iterate the remaining base directories
+		for (int dirIdx = 1; dirIdx < nb; dirIdx++) {
+			if (dirs[dirIdx] == null) {
+				continue;
+			}
+			// read the directory
+			if (dirs[dirIdx].getFilenames() == null) {
+				dirs[dirIdx].readDir();
+			}
+			String[] dirFiles = dirs[dirIdx].getFilenames();
+			// try the same filename as the original
+			int fileIdx = Arrays.binarySearch(dirFiles, fn);
+			if (fileIdx < 0) {
+				// try closest matches without extension
+				fileIdx = -fileIdx - 1;
+				// try idx
+				if ((fileIdx < dirFiles.length)
+						&& (FileOps.basename(dirFiles[fileIdx]).equals(baseFn))) {
+					// idx ok
+				} else if ((fileIdx > 0)
+						&& (FileOps.basename(dirFiles[fileIdx - 1])
+								.equals(baseFn))) {
+					// idx-1 ok
+					fileIdx = fileIdx - 1;
+				} else if ((fileIdx+1 < dirFiles.length)
+						&& (FileOps.basename(dirFiles[fileIdx + 1])
+								.equals(baseFn))) {
+					// idx+1 ok
+					fileIdx = fileIdx + 1;
+				} else {
+					// basename doesn't match
+					continue;
+				}
+			}
+			if (FileOps.classForFilename(dirFiles[fileIdx]) == FileOps.CLASS_IMAGE) {
+				/* logger.debug("adding file " + dirFiles[fileIdx]
+						+ " to Fileset " + this.getName()); */
+				add(new ImageFile(dirFiles[fileIdx], this, dirs[dirIdx]));
+			}
+		}
+	}
+
+	/**
+	 * Checks metadata and sets resolution in resX and resY.
+	 *  
+	 */
+	public void checkMeta() {
+		if (metaChecked) {
+			return;
+		}
+		if (fileMeta == null) {
+			// try to read metadata file
+			readMeta();
+			if (fileMeta == null) {
+				// try directory metadata
+				((DocuDirectory) parent).checkMeta();
+				if (((DocuDirectory) parent).getDirMeta() != null) {
+					fileMeta = ((DocuDirectory) parent).getDirMeta();
+				} else {
+					// try parent directory metadata
+					DocuDirectory gp = (DocuDirectory) parent.getParent();
+					if (gp != null) {
+						gp.checkMeta();
+						if (gp.getDirMeta() != null) {
+							fileMeta = gp.getDirMeta();
+						}
+					}
+				}
+			}
+		}
+		if (fileMeta == null) {
+			// no metadata available
+			metaChecked = true;
+			return;
+		}
+		metaChecked = true;
+		float dpi = 0;
+		float dpix = 0;
+		float dpiy = 0;
+		float sizex = 0;
+		float sizey = 0;
+		float pixx = 0;
+		float pixy = 0;
+		// DPI is valid for X and Y
+		if (fileMeta.containsKey("original-dpi")) {
+			try {
+				dpi = Float.parseFloat((String) fileMeta.get("original-dpi"));
+			} catch (NumberFormatException e) {
+			}
+			if (dpi != 0) {
+				resX = dpi;
+				resY = dpi;
+				return;
+			}
+		}
+		// DPI-X and DPI-Y
+		if (fileMeta.containsKey("original-dpi-x")
+				&& fileMeta.containsKey("original-dpi-y")) {
+			try {
+				dpix = Float.parseFloat((String) fileMeta
+						.get("original-dpi-x"));
+				dpiy = Float.parseFloat((String) fileMeta
+						.get("original-dpi-y"));
+			} catch (NumberFormatException e) {
+			}
+			if ((dpix != 0) && (dpiy != 0)) {
+				resX = dpix;
+				resY = dpiy;
+				return;
+			}
+		}
+		// SIZE-X and SIZE-Y and PIXEL-X and PIXEL-Y
+		if (fileMeta.containsKey("original-size-x")
+				&& fileMeta.containsKey("original-size-y")
+				&& fileMeta.containsKey("original-pixel-x")
+				&& fileMeta.containsKey("original-pixel-y")) {
+			try {
+				sizex = Float.parseFloat((String) fileMeta
+						.get("original-size-x"));
+				sizey = Float.parseFloat((String) fileMeta
+						.get("original-size-y"));
+				pixx = Float.parseFloat((String) fileMeta
+						.get("original-pixel-x"));
+				pixy = Float.parseFloat((String) fileMeta
+						.get("original-pixel-y"));
+			} catch (NumberFormatException e) {
+			}
+			if ((sizex != 0) && (sizey != 0) && (pixx != 0) && (pixy != 0)) {
+				resX = pixx / (sizex * 100 / 2.54f);
+				resY = pixy / (sizey * 100 / 2.54f);
+				return;
+			}
+		}
+	}
+
+	/**
+	 * @return
+	 */
+	public float getResX() {
+		return resX;
+	}
+
+	/**
+	 * @return
+	 */
+	public float getResY() {
+		return resY;
+	}
+
+	/**
+	 * Sets the aspect ratio from an ImageSize.
+	 * 
+	 * 
+	 * @param f
+	 */
+	public void setAspect(ImageSize s) {
+		aspect = s.getAspect();
+	}
+
+	/**
+	 * Returns the aspect ratio.
+	 * 
+	 * Aspect ratio is (width/height). So it's <1 for portrait and >1 for
+	 * landscape.
+	 * 
+	 * 
+	 * @return
+	 */
+	public float getAspect() {
+		return aspect;
+	}
+
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/servlet/src/digilib/io/MetadataMap.java	Fri Feb 05 20:58:38 2010 +0100
@@ -0,0 +1,14 @@
+/**
+ * 
+ */
+package digilib.io;
+
+import java.util.HashMap;
+
+/** Map for metadata related to files.
+ * @author casties
+ *
+ */
+public class MetadataMap extends HashMap<String, String> {
+
+}
--- a/servlet/src/digilib/io/XMLListLoader.java	Thu Jan 14 14:30:30 2010 +0100
+++ b/servlet/src/digilib/io/XMLListLoader.java	Fri Feb 05 20:58:38 2010 +0100
@@ -21,121 +21,158 @@
 package digilib.io;
 
 // JAXP packages
-import javax.xml.parsers.*;
-import org.xml.sax.*;
-import org.xml.sax.helpers.*;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.Map;
+
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.parsers.SAXParser;
+import javax.xml.parsers.SAXParserFactory;
 
-import java.util.*;
-import java.io.*;
+import org.apache.log4j.Logger;
+import org.xml.sax.Attributes;
+import org.xml.sax.SAXException;
+import org.xml.sax.SAXParseException;
+import org.xml.sax.helpers.DefaultHandler;
 
+/** Loads a simple XML list into a HashMap.
+ * 
+ * The XML file has an outer <code>list_tag</code>. Every entry is an 
+ * <code>entry_tag</code> with two attributes: the <code>key_att</code>
+ * key and the <code>value_att</code> value.
+ * 
+ * The file is read by the <code>loadURL</code> method, that returns a
+ * HashMap with the key-value pairs.
+ * 
+ * @author casties
+ */
 public class XMLListLoader {
 
-    private String listTag = "list";
-    private String entryTag = "entry";
-    private String keyAtt = "key";
-    private String valueAtt = "value";
+	private Logger logger = Logger.getLogger(this.getClass());
+	private String listTag = "list";
+	private String entryTag = "entry";
+	private String keyAtt = "key";
+	private String valueAtt = "value";
 
-    public XMLListLoader() {
-    }
+	public XMLListLoader() {
+	}
 
-    public XMLListLoader(String list_tag, String entry_tag, String key_att, String value_att) {
-      //System.out.println("xmlListLoader("+list_tag+","+entry_tag+","+key_att+","+value_att+")");
-      listTag = list_tag;
-      entryTag = entry_tag;
-      keyAtt = key_att;
-      valueAtt = value_att;
-    }
-
-    /**
-     *  inner class XMLListParser to be called by the parser
-     */
-    private class XMLListParser extends DefaultHandler {
+	public XMLListLoader(
+		String list_tag,
+		String entry_tag,
+		String key_att,
+		String value_att) {
+		logger.debug("xmlListLoader("+list_tag+","+entry_tag+","+key_att+","+value_att+")");
+		listTag = list_tag;
+		entryTag = entry_tag;
+		keyAtt = key_att;
+		valueAtt = value_att;
+	}
 
-      private Hashtable listData;
-      private Stack nameSpace;
+	/**
+	 *  inner class XMLListParser to be called by the parser
+	 */
+	private class XMLListParser extends DefaultHandler {
 
-      public Hashtable getData() {
-        return listData;
-      }
+		private Map<String, String> listData;
+		private LinkedList<String> tagSpace;
+
+		public Map<String, String> getData() {
+			return listData;
+		}
 
-      // Parser calls this once at the beginning of a document
-      public void startDocument() throws SAXException {
-          listData = new Hashtable();
-          nameSpace = new Stack();
-      }
+		// Parser calls this once at the beginning of a document
+		public void startDocument() throws SAXException {
+			listData = new HashMap<String, String>();
+			tagSpace = new LinkedList<String>();
+		}
 
-      // Parser calls this for each element in a document
-      public void startElement(String namespaceURI, String localName,
-                               String qName, Attributes atts)
-  	throws SAXException
-      {
-        //System.out.println("<"+qName);
-        // open a new namespace
-        nameSpace.push(qName);
+		// Parser calls this for each element in a document
+		public void startElement(
+			String namespaceURI,
+			String localName,
+			String qName,
+			Attributes atts)
+			throws SAXException {
+			//System.out.println("<"+qName);
+			// open a new namespace
+			tagSpace.addLast(qName);
 
-        // ist it an entry tag?
-        if (qName.equals(entryTag)) {
-          // is it inside a list tag?
-          if ((listTag.length() > 0)&&(nameSpace.search(listTag) < 0)) {
-            System.out.println("BOO: Entry "+entryTag+" not inside list "+listTag);
-            throw new SAXParseException("Entry "+entryTag+" not inside list "+listTag, null);
-          }
-          // get the attributes
-          String key = atts.getValue(keyAtt);
-          String val = atts.getValue(valueAtt);
-          if ((key == null)||(val == null)) {
-            System.out.println("BOO: Entry "+entryTag+" does not have Attributes "+keyAtt+", "+valueAtt);
-            throw new SAXParseException("Entry "+entryTag+" does not have Attributes "+keyAtt+", "+valueAtt, null);
-          }
-          // add the values
-          //System.out.println("DATA: "+key+" = "+val);
-          listData.put(key, val);
-        }
-    }
-
-    public void endElement(String namespaceURI, String localName,
-                             String qName)
-	throws SAXException
-    {
-      // exit the namespace
-      nameSpace.pop();
-    }
-
-    }
-
+			// ist it an entry tag?
+			if (qName.equals(entryTag)) {
+				// is it inside a list tag?
+				if ((listTag.length() > 0) && (!tagSpace.contains(listTag))) {
+					logger.error("BOO: Entry "
+							+ entryTag
+							+ " not inside list "
+							+ listTag);
+					throw new SAXParseException(
+						"Entry " + entryTag + " not inside list " + listTag,
+						null);
+				}
+				// get the attributes
+				String key = atts.getValue(keyAtt);
+				String val = atts.getValue(valueAtt);
+				if ((key == null) || (val == null)) {
+					logger.error("BOO: Entry "
+							+ entryTag
+							+ " does not have Attributes "
+							+ keyAtt
+							+ ", "
+							+ valueAtt);
+					throw new SAXParseException(
+						"Entry "
+							+ entryTag
+							+ " does not have Attributes "
+							+ keyAtt
+							+ ", "
+							+ valueAtt,
+						null);
+				}
+				// add the values
+				//System.out.println("DATA: "+key+" = "+val);
+				listData.put(key, val);
+			}
+		}
 
-    /**
-     *  load and parse a file (as URL)
-     *    returns Hashtable with list data
-     */
-    public Hashtable loadURL(String path) throws SAXException, IOException {
-        //System.out.println("loadurl ("+path+")");
-        // Create a JAXP SAXParserFactory and configure it
-        SAXParserFactory spf = SAXParserFactory.newInstance();
-        //spf.setNamespaceAware(true);
+		public void endElement(
+			String namespaceURI,
+			String localName,
+			String qName)
+			throws SAXException {
+			// exit the namespace
+			tagSpace.removeLast();
+		}
 
-        XMLReader xmlReader = null;
-        try {
-          // Create a JAXP SAXParser
-          SAXParser saxParser = spf.newSAXParser();
+	}
 
-          // Get the encapsulated SAX XMLReader
-          xmlReader = saxParser.getXMLReader();
-        }
-        catch (ParserConfigurationException e) {
-          throw new SAXException(e);
-        }
+	/**
+	 *  load and parse a file (as URL)
+	 *    returns HashMap with list data
+	 */
+	public Map<String, String> loadURL(String path) throws SAXException, IOException {
+		//System.out.println("loadurl ("+path+")");
+		// Create a JAXP SAXParserFactory and configure it
+		SAXParserFactory spf = SAXParserFactory.newInstance();
+		spf.setNamespaceAware(true);
 
-        // create a list parser (keeps the data!)
-        XMLListParser listParser = new XMLListParser();
+		SAXParser parser = null;
+		try {
+			// Create a JAXP SAXParser
+			parser = spf.newSAXParser();
 
-        // Set the ContentHandler of the XMLReader
-        xmlReader.setContentHandler(listParser);
+		} catch (ParserConfigurationException e) {
+			throw new SAXException(e);
+		}
 
-        // Tell the XMLReader to parse the XML document
-        xmlReader.parse(path);
+		// create a list parser (keeps the data!)
+		XMLListParser listParser = new XMLListParser();
 
-        return listParser.getData();
-    }
+		// Tell the SAXParser to parse the XML document
+		parser.parse(path, listParser);
+
+		return listParser.getData();
+	}
 
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/servlet/src/digilib/io/XMLMetaLoader.java	Fri Feb 05 20:58:38 2010 +0100
@@ -0,0 +1,287 @@
+/* XMLMetaLoader -- Load an XML format metadata into a Hashtable
+
+  Digital Image Library servlet components
+
+  Copyright (C) 2003 Robert Casties (robcast@mail.berlios.de)
+
+  This program is free software; you can redistribute  it and/or modify it
+  under  the terms of  the GNU General  Public License as published by the
+  Free Software Foundation;  either version 2 of the  License, or (at your
+  option) any later version.
+   
+  Please read license.txt for the full details. A copy of the GPL
+  may be found at http://www.gnu.org/copyleft/lgpl.html
+
+  You should have received a copy of the GNU General Public License
+  along with this program; if not, write to the Free Software
+  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+*/
+
+package digilib.io;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.Map;
+
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.parsers.SAXParser;
+import javax.xml.parsers.SAXParserFactory;
+
+import org.apache.log4j.Logger;
+import org.xml.sax.Attributes;
+import org.xml.sax.SAXException;
+import org.xml.sax.helpers.DefaultHandler;
+
+public class XMLMetaLoader {
+
+	private Logger logger = Logger.getLogger(this.getClass());
+	private String metaTag = "meta";
+	private String fileTag = "file";
+	private String fileNameTag = "name";
+	private String filePathTag = "path";
+	private String imgTag = "img";
+	private String collectTag = "context";
+
+	public XMLMetaLoader() {
+	}
+
+	/**
+	 *  inner class XMLMetaParser to be called by the parser
+	 */
+	private class XMLMetaParser extends DefaultHandler {
+
+		private LinkedList<String> tags;
+		private Map<String, MetadataMap> files;
+		private MetadataMap meta;
+		private StringBuffer content;
+		private boolean collecting;
+		private StringBuffer collectedContent;
+		private String fileName;
+		private String filePath;
+
+		/**
+		 * extracts the elements name from either localName ln or qName qn.
+		 * 
+		 * @param ln localName
+		 * @param qn qName
+		 * @return element name
+		 */
+		private String getName(String ln, String qn) {
+			if (ln != null) {
+				if (ln.length() > 0) {
+					return ln;
+				}
+			}
+			// else it's qName (or nothing)
+			return qn;
+		}
+
+		/**
+		 * returns all attributes as a String
+		 * 
+		 * @param attrs
+		 * @return 
+		 */
+		private String getAttrString(Attributes attrs) {
+			StringBuffer s = new StringBuffer();
+			for (int i = 0; i < attrs.getLength(); i++) {
+				String key = getName(attrs.getLocalName(i), attrs.getQName(i));
+				s.append(" "+key+"=\""+attrs.getValue(i)+"\"");
+			}
+			return s.toString();
+		}
+
+			
+		// Parser calls this once at the beginning of a document
+		public void startDocument() throws SAXException {
+			tags = new LinkedList<String>();
+			files = new HashMap<String, MetadataMap>();
+			collecting = false;
+			collectedContent = null;
+		}
+
+		// Parser calls this for each element in a document
+		public void startElement(
+			String namespaceURI,
+			String localName,
+			String qName,
+			Attributes atts)
+			throws SAXException {
+
+			String name = getName(localName, qName);
+			// open a new tag
+			tags.addLast(name);
+			// start new content (no nesting of tags and content)
+			content = new StringBuffer();
+
+			if (name.equals(metaTag)) {
+				// new meta tag
+				meta = new MetadataMap();
+				collectedContent = new StringBuffer();
+			} else if (name.equals(fileTag)) {
+				// new file tag
+				fileName = null;
+				filePath = null;
+				meta = new MetadataMap();
+				collectedContent = new StringBuffer();
+			} else if (name.equals(collectTag)) {
+				// start collecting
+				collecting = true;
+				if (collectedContent == null) {
+					collectedContent = new StringBuffer();
+				}
+			}
+			
+			// record mode
+			if (collecting) {
+				collectedContent.append("<"+name);
+				collectedContent.append(getAttrString(atts));
+				collectedContent.append(">");
+			}
+		}
+
+		// parser calls this for all tag content (possibly more than once)
+		public void characters(char[] ch, int start, int length)
+			throws SAXException {
+			// append data to current string buffer
+			if (content == null) {
+				content = new StringBuffer();
+			}
+			content.append(ch, start, length);
+		}
+
+		// parser calls this at the end of each element
+		public void endElement(
+			String namespaceURI,
+			String localName,
+			String qName)
+			throws SAXException {
+
+			String name = getName(localName, qName);
+			// exit the tag
+			tags.removeLast();
+			String lastTag = (tags.isEmpty()) ? "" : tags.getLast();
+
+			// was it a file/name tag?
+			if (name.equals(fileNameTag) && lastTag.equals(fileTag)) {
+				// save name as filename
+				if ((content != null) && (content.length() > 0)) {
+					fileName = content.toString().trim();
+				}
+				content = null;
+				return;
+			}
+
+			// was it a file/path tag?
+			if (name.equals(filePathTag) && lastTag.equals(fileTag)) {
+				// save path as filepath 
+				if ((content != null) && (content.length() > 0)) {
+					filePath = content.toString().trim();
+				}
+				content = null;
+				return;
+			}
+
+			// was it a file tag?
+			if (name.equals(fileTag)) {
+				// is there meta to save?
+				if ((meta != null) && (meta.size() > 0)) {
+					// file name is (optional file/path) / file/name
+					String fn = null;
+
+					if (fileName != null) {
+						if (filePath != null) {
+							fn = filePath + "/" + fileName;
+						} else {
+							fn = fileName;
+						}
+					} else {
+						// no file name, no file
+						content = null;
+						return;
+					}
+					// save meta in file list 
+					files.put(fn, meta);
+				}
+				content = null;
+				return;
+			}
+
+			// was it a meta tag outside a file tag?
+			if (name.equals(metaTag) && !tags.contains(fileTag)) {
+				// save meta as dir meta
+				if ((meta != null) && (meta.size() > 0)) {
+					files.put("", meta);
+				}
+				content = null;
+				return;
+			}
+
+			// is this inside an digilib info (=img) tag?
+			if (lastTag.equals(imgTag)) {
+				// then add whatever this is
+				if ((content != null) && (content.length() > 0)) {
+					meta.put(name, content.toString().trim());
+				}
+				content = null;
+				return;
+			}
+
+			// is this the end of collectTag?
+			if (name.equals(collectTag)) {
+				collecting = false;
+				collectedContent.append("</"+collectTag+">\n");
+				// store collected stuff
+				meta.put(collectTag, collectedContent.toString());
+				//logger.debug("collected: '"+collectedContent+"'");
+				content = null;
+				return;
+			}
+
+			// write collected content
+			if (collecting) {
+				String s = "";
+				if ((content != null) && (content.length() > 0)) {
+					s = content.toString().trim();
+				}
+				//logger.debug("collect:"+name+" = "+s);
+				collectedContent.append(s);
+				collectedContent.append("</"+name+">\n");
+				content = null;
+				return;
+			}
+		}
+
+	}
+
+	/**
+	 *  load and parse a file (as URL)
+	 *    returns HashMap with list data
+	 */
+	public Map<String, MetadataMap> loadURL(String path) throws SAXException, IOException {
+		logger.debug("loading meta: "+path);
+		// Create a JAXP SAXParserFactory and configure it
+		SAXParserFactory spf = SAXParserFactory.newInstance();
+		spf.setNamespaceAware(true);
+
+		SAXParser parser = null;
+		try {
+			// Create a JAXP SAXParser
+			parser = spf.newSAXParser();
+
+		} catch (ParserConfigurationException e) {
+			throw new SAXException(e);
+		}
+
+		// create a list parser (keeps the data!)
+		XMLMetaParser listParser = new XMLMetaParser();
+
+		// Tell the SAXParser to parse the XML document
+		parser.parse(path, listParser);
+
+		return listParser.files;
+	}
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/servlet/src/digilib/servlet/DigilibConfiguration.java	Fri Feb 05 20:58:38 2010 +0100
@@ -0,0 +1,265 @@
+/*
+ * DigilibConfiguration -- Holding all parameters for digilib servlet.
+ * 
+ * Digital Image Library servlet components
+ * 
+ * Copyright (C) 2001, 2002 Robert Casties (robcast@mail.berlios.de)
+ * 
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the Free
+ * Software Foundation; either version 2 of the License, or (at your option)
+ * any later version.
+ * 
+ * Please read license.txt for the full details. A copy of the GPL may be found
+ * at http://www.gnu.org/copyleft/lgpl.html
+ * 
+ * You should have received a copy of the GNU General Public License along with
+ * this program; if not, write to the Free Software Foundation, Inc., 59 Temple
+ * Place, Suite 330, Boston, MA 02111-1307 USA
+ *  
+ */
+
+package digilib.servlet;
+
+import java.io.File;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import javax.servlet.ServletConfig;
+import javax.servlet.ServletException;
+
+import org.apache.log4j.BasicConfigurator;
+import org.apache.log4j.Logger;
+
+import digilib.image.DocuImage;
+import digilib.image.DocuImageImpl;
+import digilib.io.FileOps;
+import digilib.io.XMLListLoader;
+
+/**
+ * Class to hold the digilib servlet configuration parameters. The parameters
+ * can be read from the digilib-config file and be passed to other servlets or
+ * beans. <br>errorImgFileName: image file to send in case of error. <br>
+ * denyImgFileName: image file to send if access is denied. <br>baseDirs:
+ * array of base directories in order of preference (prescaled versions first).
+ * <br>useAuth: use authentication information. <br>authConfPath:
+ * authentication configuration file. <br>... <br>
+ * 
+ * @author casties
+ *  
+ */
+public class DigilibConfiguration extends ParameterMap {
+
+	private static final long serialVersionUID = -6630487070791637120L;
+
+	/** DocuImage class instance */
+	private Class<DocuImageImpl> docuImageClass = null;
+
+	/** Log4J logger */
+	private Logger logger = Logger.getLogger("digilib.config");
+
+	/**
+	 * Default constructor defines all parameters and their default values.
+	 *  
+	 */
+	public DigilibConfiguration() {
+		// create HashMap(20)
+		super(20);
+		// we start with a default logger config
+		BasicConfigurator.configure();
+
+		/*
+		 * Definition of parameters and default values. System parameters that
+		 * are not read from config file have a type 's'.
+		 */
+
+		// digilib servlet version
+		newParameter(
+			"servlet.version",
+			digilib.servlet.Scaler.getVersion(),
+			null,
+			's');
+		// configuration file location
+		newParameter("servlet.config.file", null, null, 's');
+		// DocuDirCache instance
+		newParameter("servlet.dir.cache", null, null, 's');
+		// DocuImage class instance
+		newParameter(
+			"servlet.docuimage.class",
+			digilib.image.JAIDocuImage.class,
+			null,
+			's');
+		// AuthOps instance for authentication
+		newParameter("servlet.auth.op", null, null, 's');
+
+		/*
+		 * parameters that can be read from config file have a type 'f'
+		 */
+
+		// image file to send in case of error
+		newParameter(
+			"error-image",
+			new File("img/digilib-error.png"),
+			null,
+			'f');
+		// image file to send if access is denied
+		newParameter(
+			"denied-image",
+			new File("img/digilib-denied.png"),
+			null,
+			'f');
+		// image file to send if image file not found
+		newParameter(
+			"notfound-image",
+			new File("img/digilib-notfound.png"),
+			null,
+			'f');
+		// base directories in order of preference (prescaled versions last)
+		String[] bd = { "/docuserver/images", "/docuserver/scaled/small" };
+		newParameter("basedir-list", bd, null, 'f');
+		// use authentication information
+		newParameter("use-authorization", Boolean.FALSE, null, 'f');
+		// authentication configuration file
+		newParameter("auth-file", new File("digilib-auth.xml"), null, 'f');
+		// sending image files as-is allowed
+		newParameter("sendfile-allowed", Boolean.TRUE, null, 'f');
+		// Debug level
+		newParameter("debug-level", new Integer(5), null, 'f');
+		// Type of DocuImage instance
+		newParameter(
+			"docuimage-class",
+			"digilib.image.JAIDocuImage",
+			null,
+			'f');
+		// part of URL used to indicate authorized access
+		newParameter("auth-url-path", "authenticated/", null, 'f');
+		// degree of subsampling on image load
+		newParameter("subsample-minimum", new Float(2f), null, 'f');
+		// default scaling quality
+		newParameter("default-quality", new Integer(1), null, 'f');
+		// use mapping file to translate paths
+		newParameter("use-mapping", Boolean.FALSE, null, 'f');
+		// mapping file location
+		newParameter("mapping-file", new File("digilib-map.xml"), null, 'f');
+		// log4j config file location
+		newParameter("log-config-file", new File("log4j-config.xml"), null, 'f');
+		// maximum destination image size (0 means no limit)
+		newParameter("max-image-size", new Integer(0), null, 'f');
+		// use safe (but slower) directory indexing
+		newParameter("safe-dir-index", Boolean.FALSE, null, 'f');
+		// number of working threads
+		newParameter("worker-threads", new Integer(1), null, 'f');
+		// max number of waiting threads
+		newParameter("max-waiting-threads", new Integer(0), null, 'f');
+
+	}
+
+	/**
+	 * Constructor taking a ServletConfig. Reads the config file location from
+	 * an init parameter and loads the config file. Calls <code>readConfig()</code>.
+	 * 
+	 * @see readConfig()
+	 */
+	public DigilibConfiguration(ServletConfig c) throws Exception {
+		this();
+		readConfig(c);
+	}
+
+	/**
+	 * read parameter list from the XML file in init parameter "config-file"
+	 */
+	public void readConfig(ServletConfig c) throws Exception {
+
+		/*
+		 * Get config file name. The file name is first looked for as an init
+		 * parameter, then in a fixed location in the webapp.
+		 */
+		if (c == null) {
+			// no config no file...
+			return;
+		}
+		String fn = c.getInitParameter("config-file");
+		if (fn == null) {
+			fn = ServletOps.getConfigFile("digilib-config.xml", c);
+			if (fn == null) {
+				logger.fatal("readConfig: no param config-file");
+				throw new ServletException("ERROR: no digilib config file!");
+			}
+		}
+		File f = new File(fn);
+		// setup config file list reader
+		XMLListLoader lilo =
+			new XMLListLoader("digilib-config", "parameter", "name", "value");
+		// read config file into HashMap
+		Map<String,String> confTable = lilo.loadURL(f.toURL().toString());
+
+		// set config file path parameter
+		setValue("servlet.config.file", f.getCanonicalPath());
+
+		/*
+		 * read parameters
+		 */
+
+		for (Entry<String, String> confEntry: confTable.entrySet()) {
+			Parameter p = get(confEntry.getKey());
+			if (p != null) {
+				if (p.getType() == 's') {
+					// type 's' Parameters are not overwritten.
+					continue;
+				}
+				if (!p.setValueFromString(confEntry.getValue())) {
+					/*
+					 * automatic conversion failed -- try special cases
+					 */
+
+					// basedir-list
+					if (confEntry.getKey().equals("basedir-list")) {
+						// split list into directories
+						String[] sa = FileOps.pathToArray(confEntry.getValue());
+						if (sa != null) {
+							p.setValue(sa);
+						}
+					}
+
+				}
+			} else {
+				// parameter unknown -- just add
+				newParameter(confEntry.getKey(), null, confEntry.getValue(), 'f');
+			}
+		}
+
+	}
+
+	/**
+	 * Creates a new DocuImage instance.
+	 * 
+	 * The type of DocuImage is specified by docuImageType.
+	 * 
+	 * @return DocuImage
+	 */
+	@SuppressWarnings("unchecked")
+    public DocuImage getDocuImageInstance() {
+		DocuImageImpl di = null;
+		try {
+			if (docuImageClass == null) {
+				docuImageClass = (Class<DocuImageImpl>) Class.forName(getAsString("docuimage-class"));
+			}
+			di = docuImageClass.newInstance();
+		} catch (Exception e) {
+		}
+		return di;
+	}
+
+	/**
+	 * @return Returns the docuImageClass.
+	 */
+	public Class<DocuImageImpl> getDocuImageClass() {
+		return docuImageClass;
+	}
+	/**
+	 * @param docuImageClass The docuImageClass to set.
+	 */
+	public void setDocuImageClass(Class<DocuImageImpl> docuImageClass) {
+		this.docuImageClass = docuImageClass;
+	}
+}
--- a/servlet/src/digilib/servlet/DigilibImageWorker.java	Thu Jan 14 14:30:30 2010 +0100
+++ b/servlet/src/digilib/servlet/DigilibImageWorker.java	Fri Feb 05 20:58:38 2010 +0100
@@ -23,12 +23,9 @@
 
 import java.awt.Rectangle;
 import java.awt.geom.Rectangle2D;
-import java.io.BufferedOutputStream;
 import java.io.IOException;
 import java.io.OutputStream;
 
-import javax.servlet.http.HttpServletResponse;
-
 import digilib.image.DocuImage;
 import digilib.image.ImageOpException;
 import digilib.image.ImageOps;
--- a/servlet/src/digilib/servlet/DigilibInfoReader.java	Thu Jan 14 14:30:30 2010 +0100
+++ b/servlet/src/digilib/servlet/DigilibInfoReader.java	Fri Feb 05 20:58:38 2010 +0100
@@ -21,7 +21,7 @@
 	protected static Logger logger = Logger.getLogger("digilib.servlet");
 	
 	private String filename = null;
-	private static String base_element = "info";
+	//private static String base_element = "info";
 	
 	public DigilibInfoReader(String fn){
 		filename = fn;
@@ -33,16 +33,17 @@
 	 * @param attr
 	 * @return
 	 */
-	public String getAsString(String attr){
+	@SuppressWarnings("unchecked") // Element.getChildren() returns naked List
+    public String getAsString(String attr){
 		try{
 			SAXBuilder builder = new SAXBuilder();
 			Document doc = builder.build(new File(filename));
 			Element root = doc.getRootElement();
-			List mainElements = root.getChildren();
+			List<Element> mainElements = root.getChildren();
 			// logger.debug("XML mainElements:"+mainElements.toString());
 
 			for(int i=0; i<mainElements.size(); i++){
-				Element elem = (Element) mainElements.get(i);
+				Element elem = mainElements.get(i);
 				if(elem.getName()==attr){
 					// logger.debug(attr+" == "+(String)elem.getTextTrim());
 					return (String)elem.getTextTrim();
@@ -64,7 +65,7 @@
 	public boolean hasInfo(){
 		try {
 			SAXBuilder builder = new SAXBuilder();
-			Document doc = builder.build(new File(filename));
+			builder.build(new File(filename));
 			return true;
 		}
 		catch(Exception e){
--- a/servlet/src/digilib/servlet/DigilibRequest.java	Thu Jan 14 14:30:30 2010 +0100
+++ b/servlet/src/digilib/servlet/DigilibRequest.java	Fri Feb 05 20:58:38 2010 +0100
@@ -26,19 +26,14 @@
 
 package digilib.servlet;
 
-import java.io.StringReader;
 import java.io.UnsupportedEncodingException;
 import java.net.URLDecoder;
 import java.util.Enumeration;
-import java.util.Hashtable;
-import java.util.Iterator;
 import java.util.StringTokenizer;
 
 import javax.servlet.ServletRequest;
 import javax.servlet.http.HttpServletRequest;
 
-import org.apache.log4j.Logger;
-
 import digilib.image.DocuImage;
 import digilib.io.FileOps;
 
@@ -68,11 +63,7 @@
 
 	private static final long serialVersionUID = -4707707539569977901L;
 
-	private final static String ECHO = "http://echo.unibe.ch/digilib/rdf#";
-
-	private final static String DIGILIB = "Digilib";
-
-	private Logger logger = Logger.getLogger(this.getClass());
+	//private Logger logger = Logger.getLogger(this.getClass());
 
 	private boolean boolRDF = false; // use RDF Parameters
 
@@ -337,8 +328,7 @@
 	public String getAsString(int type) {
 		StringBuffer s = new StringBuffer(50);
 		// go through all values
-		for (Iterator i = this.values().iterator(); i.hasNext();) {
-			Parameter p = (Parameter) i.next();
+		for (Parameter p: this.values()) {
 			if ((type > 0) && (p.getType() != type)) {
 				// skip the wrong types
 				continue;
@@ -394,10 +384,11 @@
 	 * @param request
 	 *            ServletRequest to get parameters from.
 	 */
-	public void setWithParamRequest(ServletRequest request) {
+	@SuppressWarnings("unchecked") // ServletRequest.getParameterNames() returns naked Enumeration
+    public void setWithParamRequest(ServletRequest request) {
 		setValue("servlet.request", request);
 		// go through all request parameters
-		for (Enumeration i = request.getParameterNames(); i.hasMoreElements();) {
+		for (Enumeration<String> i = request.getParameterNames(); i.hasMoreElements();) {
 			String name = (String) i.nextElement();
 			// is this a known parameter?
 			if (this.containsKey(name)) {
--- a/servlet/src/digilib/servlet/DocumentBean.java	Thu Jan 14 14:30:30 2010 +0100
+++ b/servlet/src/digilib/servlet/DocumentBean.java	Fri Feb 05 20:58:38 2010 +0100
@@ -1,198 +1,314 @@
-/* DocumentBean -- Access control bean for JSP
-
-  Digital Image Library servlet components
-
-  Copyright (C) 2001, 2002 Robert Casties (robcast@mail.berlios.de)
-
-  This program is free software; you can redistribute  it and/or modify it
-  under  the terms of  the GNU General  Public License as published by the
-  Free Software Foundation;  either version 2 of the  License, or (at your
-  option) any later version.
-   
-  Please read license.txt for the full details. A copy of the GPL
-  may be found at http://www.gnu.org/copyleft/lgpl.html
-
-  You should have received a copy of the GNU General Public License
-  along with this program; if not, write to the Free Software
-  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
-
-*/
+/*
+ * DocumentBean -- Access control bean for JSP
+ *
+ * Digital Image Library servlet components
+ *
+ * Copyright (C) 2001, 2002, 2003 Robert Casties (robcast@mail.berlios.de)
+ *
+ * This program is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 2 of the License, or (at your option) any later
+ * version.
+ *
+ * Please read license.txt for the full details. A copy of the GPL may be found
+ * at http://www.gnu.org/copyleft/lgpl.html
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * this program; if not, write to the Free Software Foundation, Inc., 59 Temple
+ * Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ */
 
 package digilib.servlet;
 
+import java.util.List;
 
-import java.util.*;
-import javax.servlet.*;
-import javax.servlet.http.*;
+import javax.servlet.ServletConfig;
+import javax.servlet.ServletContext;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.log4j.Logger;
 
-import digilib.*;
-import digilib.io.*;
-import digilib.auth.*;
+import digilib.auth.AuthOpException;
+import digilib.auth.AuthOps;
+import digilib.image.ImageOps;
+import digilib.image.ImageSize;
+import digilib.io.DocuDirCache;
+import digilib.io.DocuDirectory;
+import digilib.io.FileOps;
+import digilib.io.ImageFile;
+import digilib.io.ImageFileset;
 
-public class DocumentBean implements AuthOps {
+public class DocumentBean {
+
+	// general logger
+	private static Logger logger = Logger.getLogger("digilib.docubean");
+
+	// AuthOps object to check authorization
+	private AuthOps authOp;
 
-  // Utils object for logging
-  private Utils util = new Utils(5);
-  // AuthOps object to check authorization
-  private AuthOps authOp;
-  // FileOps object
-  private FileOps fileOp = new FileOps(util);
+	// use authorization database
+	private boolean useAuthentication = true;
+
+	// path to add for authenticated access
+	private String authURLPath = "";
+
+	// DocuDirCache
+	private DocuDirCache dirCache = null;
+
+	// DigilibConfiguration object
+	private DigilibConfiguration dlConfig;
+
+	// DigilibRequest object
+	private DigilibRequest dlRequest = null;
 
-  // base directories in order of preference (prescaled versions first)
-  private String[] baseDirs = {"/docuserver/scaled/small", "/docuserver/images", "/docuserver/scans/quellen"};
-  // part of URL path to prepend for authenticated access
-  private String authURLpath = "authenticated/";
+	/**
+	 * Constructor for DocumentBean.
+	 */
+	public DocumentBean() {
+        logger.debug("new DocumentBean");
+	}
 
-
-  public DocumentBean() {
-  }
+	public DocumentBean(ServletConfig conf) {
+        logger.debug("new DocumentBean");
+		try {
+			setConfig(conf);
+		} catch (Exception e) {
+			logger.fatal("ERROR: Unable to read config: ", e);
+		}
+	}
 
-  public void setConfig(ServletConfig conf) throws ServletException {
-    util.dprintln(10, "setConfig");
-    // servletOps takes a ServletConfig to get the config file name
-    ServletOps servletOp = new ServletOps(util, conf);
-    /**
-     *  basedir-list : List of document directories
-     */
-    String bl = servletOp.tryToGetInitParam("basedir-list", null);
-    if ((bl != null)&&(bl.length() > 0)) {
-      // split list into directories
-      StringTokenizer dirs = new StringTokenizer(bl, ":");
-      int n = dirs.countTokens();
-      if (n > 0) {
-        // add directories into array
-        baseDirs = new String[n];
-        for (int i = 0; i < n; i++) {
-          baseDirs[i] = dirs.nextToken();
-        }
-      }
-      util.dprintln(3, "basedir-list: "+bl);
-    }
-    /**
-     *  auth-url-path : part of URL to indicate authenticated access
-     */
-    String au = servletOp.tryToGetInitParam("auth-url-path", null);
-    if ((au != null)&&(au.length() > 0)) {
-      authURLpath = au;
-      util.dprintln(3, "auth-url-path: "+au);
-    }
-    /**
-     *  authentication
-     */
-    try {
-      // DB version
-      //private AuthOps authOp = new DBAuthOpsImpl(util);
-      // XML version
-      String cp = servletOp.tryToGetInitParam("auth-file", "/docuserver/www/digitallibrary/WEB-INF/digilib-auth.xml");
-      util.dprintln(3, "auth-file: "+cp);
-      authOp = new XMLAuthOps(util, cp);
-    } catch (AuthOpException e) {
-      throw new ServletException(e);
-    }
-  }
+	public void setConfig(ServletConfig conf) throws ServletException {
+		logger.debug("setConfig");
+		// get our ServletContext
+		ServletContext context = conf.getServletContext();
+		// see if there is a Configuration instance
+		dlConfig = (DigilibConfiguration) context
+				.getAttribute("digilib.servlet.configuration");
+		if (dlConfig == null) {
+			// create new Configuration
+			throw new ServletException("ERROR: No configuration!");
+		}
+
+		// get cache
+		dirCache = (DocuDirCache) dlConfig.getValue("servlet.dir.cache");
+
+		/*
+		 * authentication
+		 */
+		useAuthentication = dlConfig.getAsBoolean("use-authorization");
+		authOp = (AuthOps) dlConfig.getValue("servlet.auth.op");
+		authURLPath = dlConfig.getAsString("auth-url-path");
+		if (useAuthentication && (authOp == null)) {
+			throw new ServletException(
+					"ERROR: use-authorization configured but no AuthOp!");
+		}
+	}
+
+	/**
+	 * check if the request must be authorized to access filepath
+	 */
+	public boolean isAuthRequired(DigilibRequest request)
+			throws AuthOpException {
+		logger.debug("isAuthRequired");
+		return useAuthentication ? authOp.isAuthRequired(request) : false;
+	}
+
+	/**
+	 * check if the request is allowed to access filepath
+	 */
+	public boolean isAuthorized(DigilibRequest request) throws AuthOpException {
+		logger.debug("isAuthorized");
+		return useAuthentication ? authOp.isAuthorized(request) : true;
+	}
+
+	/**
+	 * return a list of authorization roles needed for request to access the
+	 * specified path
+	 */
+	public List<String> rolesForPath(DigilibRequest request) throws AuthOpException {
+		logger.debug("rolesForPath");
+		return useAuthentication ? authOp.rolesForPath(request) : null;
+	}
+
+	/**
+	 * check request authorization against a list of roles
+	 */
+	public boolean isRoleAuthorized(List<String> roles, DigilibRequest request) {
+		logger.debug("isRoleAuthorized");
+		return useAuthentication ? authOp.isRoleAuthorized(roles, request)
+				: true;
+	}
+
+	/**
+	 * check for authenticated access and redirect if necessary
+	 */
+	public boolean doAuthentication(HttpServletResponse response)
+			throws Exception {
+        logger.debug("doAuthenication-Method");
+		return doAuthentication(dlRequest, response);
+	}
 
-  public String getDocuPath(HttpServletRequest request) {
-    util.dprintln(10, "getDocuPath");
-    // fetch query string
-    String qs = request.getQueryString();
-    String fn = "";
-    if (qs != null && qs.length() > 0) {
-       // the file name is in the request before the first "+"
-       int endfn = qs.indexOf("+");
-       if (endfn > 0) {
-         fn = qs.substring(0, endfn);
-       } else {
-	 fn = qs;
-       }
-    }
-    util.dprintln(4, "docuPath: "+fn);
-    return fn;
-  }
+	/**
+	 * check for authenticated access and redirect if necessary
+	 */
+	public boolean doAuthentication(DigilibRequest request,
+			HttpServletResponse response) throws Exception {
+		logger.debug("doAuthentication");
+		if (!useAuthentication) {
+			// shortcut if no authentication
+			return true;
+		}
+		// check if we are already authenticated
+		if (((HttpServletRequest) request.getServletRequest()).getRemoteUser() == null) {
+			logger.debug("unauthenticated so far");
+			// if not maybe we must?
+			if (isAuthRequired(request)) {
+				logger.debug("auth required, redirect");
+				// we are not yet authenticated -> redirect
+				response.sendRedirect(authURLPath
+						+ ((HttpServletRequest) request.getServletRequest())
+								.getServletPath()
+						+ "?"
+						+ ((HttpServletRequest) request.getServletRequest())
+								.getQueryString());
+			}
+		}
+		return true;
+	}
 
-  /**
-   *  check if the request must be authorized to access filepath
-   */
-  public boolean isAuthRequired(HttpServletRequest request) throws AuthOpException {
-    util.dprintln(10, "isAuthRequired");
-    return authOp.isAuthRequired(getDocuPath(request), request);
-  }
-
-  public boolean isAuthRequired(String filepath, HttpServletRequest request) throws AuthOpException {
-    util.dprintln(10, "isAuthRequired");
-    return authOp.isAuthRequired(filepath, request);
-  }
+	/**
+	 * Sets the current DigilibRequest. Also completes information in the request.
+	 * 
+	 * @param dlRequest
+	 *            The dlRequest to set.
+	 */
+	public void setRequest(DigilibRequest dlRequest) throws Exception {
+		this.dlRequest = dlRequest;
+		if (dirCache == null) {
+			return;
+		}
+		String fn = dlRequest.getFilePath();
+		// get information about the file
+		ImageFileset fileset = (ImageFileset) dirCache.getFile(fn, dlRequest
+				.getAsInt("pn"), FileOps.CLASS_IMAGE);
+		if (fileset == null) {
+			return;
+		}
+		// add file name
+		dlRequest.setValue("img.fn", fileset.getName());
+		// add dpi
+		dlRequest.setValue("img.dpix", new Double(fileset.getResX()));
+		dlRequest.setValue("img.dpiy", new Double(fileset.getResY()));
+		// get number of pages in directory
+		DocuDirectory dd = dirCache.getDirectory(fn);
+		if (dd != null) {
+			// add pt
+			dlRequest.setValue("pt", dd.size());
+		}
+		// get original pixel size
+		ImageFile origfile = fileset.getBiggest();
+		// check image for size if mo=hires
+		if ((! origfile.isChecked())&&dlRequest.hasOption("mo", "hires")) {
+			logger.debug("pre-checking image!");
+			ImageOps.checkFile(origfile);
+		}
+		ImageSize pixsize = origfile.getSize();
+		if (pixsize != null) {
+			// add pixel size
+			dlRequest.setValue("img.pix_x", new Integer(pixsize.getWidth()));
+			dlRequest.setValue("img.pix_y", new Integer(pixsize.getHeight()));
+		}
+	}
 
-  /**
-   *  check if the request is allowed to access filepath
-   */
-  public boolean isAuthorized(HttpServletRequest request) throws AuthOpException {
-    util.dprintln(10, "isAuthorized");
-    return authOp.isAuthorized(getDocuPath(request), request);
-  }
-
-  public boolean isAuthorized(String filepath, HttpServletRequest request) throws AuthOpException {
-    util.dprintln(10, "isAuthorized");
-    return authOp.isAuthorized(filepath, request);
-  }
+	/**
+	 * get the first page number in the directory (not yet functional)
+	 */
+	public int getFirstPage(DigilibRequest request) {
+		logger.debug("getFirstPage");
+		return 1;
+	}
 
-  /**
-   *  return a list of authorization roles needed for request
-   *  to access the specified path
-   */
-  public List rolesForPath(String filepath, HttpServletRequest request) throws AuthOpException {
-    util.dprintln(10, "rolesForPath");
-    return authOp.rolesForPath(filepath, request);
-  }
+	/**
+	 * get the number of pages/files in the directory
+	 */
+	public int getNumPages() throws Exception {
+		return getNumPages(dlRequest);
+	}
+
+	/**
+	 * get the number of pages/files in the directory
+	 */
+	public int getNumPages(DigilibRequest request) throws Exception {
+		logger.debug("getNumPages");
+		DocuDirectory dd = (dirCache != null) ? dirCache.getDirectory(request
+				.getFilePath()) : null;
+		if (dd != null) {
+			return dd.size();
+		}
+		return 0;
+	}
 
-  /**
-   * check request authorization against a list of roles
-   */
-  public boolean isRoleAuthorized(List roles, HttpServletRequest request) {
-    util.dprintln(10, "isRoleAuthorized");
-    return authOp.isRoleAuthorized(roles, request);
-  }
+	/**
+	 * Returns the dlConfig.
+	 * 
+	 * @return DigilibConfiguration
+	 */
+	public DigilibConfiguration getDlConfig() {
+		return dlConfig;
+	}
+
+	/**
+	 * returns if the zoom area in the request can be moved
+	 * 
+	 * @return
+	 */
+	public boolean canMoveRight() {
+		float ww = dlRequest.getAsFloat("ww");
+		float wx = dlRequest.getAsFloat("wx");
+		return (ww + wx < 1.0);
+	}
 
-  /**
-   * check for authenticated access and redirect if necessary
-   */
-  public boolean doAuthentication(HttpServletRequest request, HttpServletResponse response) throws Exception {
-    util.dprintln(10, "doAuthentication");
-    // check if we are already authenticated
-    if (request.getRemoteUser() == null) {
-      util.dprintln(3, "unauthenticated so far");
-      // if not maybe we must?
-      if (isAuthRequired(request)) {
-        util.dprintln(3, "auth required, redirect");
-	// we are not yet authenticated -> redirect
-	response.sendRedirect(authURLpath+request.getServletPath()+"?"+request.getQueryString());
-      }
-    }
-    return true;
-  }
+	/**
+	 * returns if the zoom area in the request can be moved
+	 * 
+	 * @return
+	 */
+	public boolean canMoveLeft() {
+		float ww = dlRequest.getAsFloat("ww");
+		float wx = dlRequest.getAsFloat("wx");
+		return ((ww < 1.0) && (wx > 0));
+	}
 
-  /**
-   *  get the first page number in the directory
-   *  (not yet functional)
-   */
-  public int getFirstPage(HttpServletRequest request) {
-    return getFirstPage(getDocuPath(request), request);
-  }
+	/**
+	 * returns if the zoom area in the request can be moved
+	 * 
+	 * @return
+	 */
+	public boolean canMoveUp() {
+		float wh = dlRequest.getAsFloat("wh");
+		float wy = dlRequest.getAsFloat("wy");
+		return ((wh < 1.0) && (wy > 0));
+	}
 
-  public int getFirstPage(String filepath, HttpServletRequest request) {
-    util.dprintln(10, "getFirstPage");
-    return 1;
-  }
+	/**
+	 * returns if the zoom area in the request can be moved
+	 * 
+	 * @return
+	 */
+	public boolean canMoveDown() {
+		float wh = dlRequest.getAsFloat("wh");
+		float wy = dlRequest.getAsFloat("wy");
+		return (wh + wy < 1.0);
+	}
 
-  /**
-   *  get the number of pages/files in the directory
-   */
-  public int getNumPages(HttpServletRequest request) throws Exception {
-    return getNumPages(getDocuPath(request), request);
-  }
-
-  public int getNumPages(String filepath, HttpServletRequest request) throws Exception {
-    util.dprintln(10, "getNumPages");
-    return fileOp.getNumFilesVariant(baseDirs, "/"+filepath, true);
-  }
+	/**
+	 * @return Returns the dlRequest.
+	 */
+	public DigilibRequest getRequest() {
+		return dlRequest;
+	}
 
 }
--- a/servlet/src/digilib/servlet/ImageJobInformation.java	Thu Jan 14 14:30:30 2010 +0100
+++ b/servlet/src/digilib/servlet/ImageJobInformation.java	Fri Feb 05 20:58:38 2010 +0100
@@ -6,7 +6,6 @@
 import java.io.IOException;
 import java.util.StringTokenizer;
 
-import javax.servlet.ServletRequest;
 import javax.servlet.http.HttpServletRequest;
 
 import org.apache.log4j.Logger;
@@ -15,7 +14,6 @@
 import digilib.image.ImageOps;
 import digilib.image.ImageSize;
 import digilib.io.DocuDirCache;
-import digilib.io.DocuDirent;
 import digilib.io.FileOpException;
 import digilib.io.FileOps;
 import digilib.io.ImageFile;
@@ -319,7 +317,7 @@
 				float sx = getAsFloat("ddpix") / origResX;
 				float sy = getAsFloat("ddpiy") / origResY;
 				// currently only same scale :-(
-				put("scale", (sx + sy)/2f);
+				setValue("scale", (sx + sy)/2f);
 			}
 			
 		}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/servlet/src/digilib/servlet/Initialiser.java	Fri Feb 05 20:58:38 2010 +0100
@@ -0,0 +1,161 @@
+/* Initialiser.java -- initalisation servlet for setup tasks
+ * 
+ * Digital Image Library servlet components
+ * 
+ * Copyright (C) 2004 Robert Casties (robcast@mail.berlios.de)
+ * 
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the Free
+ * Software Foundation; either version 2 of the License, or (at your option)
+ * any later version.
+ * 
+ * Please read license.txt for the full details. A copy of the GPL may be found
+ * at http://www.gnu.org/copyleft/lgpl.html
+ * 
+ * You should have received a copy of the GNU General Public License along with
+ * this program; if not, write to the Free Software Foundation, Inc., 59 Temple
+ * Place, Suite 330, Boston, MA 02111-1307 USA
+ *  
+ * Created on 18.10.2004
+ */
+package digilib.servlet;
+
+import java.io.File;
+
+import javax.servlet.ServletConfig;
+import javax.servlet.ServletContext;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+
+import org.apache.log4j.Logger;
+import org.apache.log4j.xml.DOMConfigurator;
+
+import digilib.auth.AuthOps;
+import digilib.auth.XMLAuthOps;
+import digilib.image.DocuImage;
+import digilib.image.ImageOps;
+import digilib.io.AliasingDocuDirCache;
+import digilib.io.DocuDirCache;
+import digilib.io.FileOps;
+
+/**
+ * Initalisation servlet for setup tasks.
+ * 
+ * @author casties
+ *  
+ */
+public class Initialiser extends HttpServlet {
+
+	private static final long serialVersionUID = -5126621114382549343L;
+
+	/** servlet version */
+	public static final String iniVersion = "0.1b2";
+
+	/** gengeral logger for this class */
+	private static Logger logger = Logger.getLogger("digilib.init");
+
+	/** AuthOps instance */
+	AuthOps authOp;
+
+	/** DocuDirCache instance */
+	DocuDirCache dirCache;
+
+	/** DigilibConfiguration instance */
+	DigilibConfiguration dlConfig;
+
+	/** use authorization database */
+	boolean useAuthentication = false;
+
+	/**
+	 * Initialisation on first run.
+	 * 
+	 * @see javax.servlet.Servlet#init(javax.servlet.ServletConfig)
+	 */
+	public void init(ServletConfig config) throws ServletException {
+		super.init(config);
+
+		System.out
+				.println("***** Digital Image Library Initialisation Servlet (version "
+						+ iniVersion + ") *****");
+
+		// get our ServletContext
+		ServletContext context = config.getServletContext();
+		// see if there is a Configuration instance
+		dlConfig = (DigilibConfiguration) context
+				.getAttribute("digilib.servlet.configuration");
+		if (dlConfig == null) {
+			// create new Configuration
+			try {
+				dlConfig = new DigilibConfiguration(config);
+
+				/*
+				 * further initialization
+				 */
+
+				// set up the logger
+				File logConf = ServletOps.getConfigFile((File) dlConfig
+						.getValue("log-config-file"), config);
+				DOMConfigurator.configure(logConf.getAbsolutePath());
+				dlConfig.setValue("log-config-file", logConf);
+				// say hello in the log file
+				logger
+						.info("***** Digital Image Library Initialisation Servlet (version "
+								+ iniVersion + ") *****");
+				// directory cache
+				String[] bd = (String[]) dlConfig.getValue("basedir-list");
+				int[] fcs = { FileOps.CLASS_IMAGE, FileOps.CLASS_TEXT,
+						FileOps.CLASS_SVG };
+				if (dlConfig.getAsBoolean("use-mapping")) {
+					// with mapping file
+					File mapConf = ServletOps.getConfigFile((File) dlConfig
+							.getValue("mapping-file"), config);
+					dirCache = new AliasingDocuDirCache(bd, fcs, mapConf,
+							dlConfig);
+					dlConfig.setValue("mapping-file", mapConf);
+				} else {
+					// without mapping
+					dirCache = new DocuDirCache(bd, fcs, dlConfig);
+				}
+				dlConfig.setValue("servlet.dir.cache", dirCache);
+				// useAuthentication
+				if (dlConfig.getAsBoolean("use-authorization")) {
+					// DB version
+					//authOp = new DBAuthOpsImpl(util);
+					// XML version
+					File authConf = ServletOps.getConfigFile((File) dlConfig
+							.getValue("auth-file"), config);
+					authOp = new XMLAuthOps(authConf);
+					dlConfig.setValue("servlet.auth.op", authOp);
+					dlConfig.setValue("auth-file", authConf);
+				}
+				// DocuImage class
+				DocuImage di = dlConfig.getDocuImageInstance();
+				dlConfig.setValue("servlet.docuimage.class", di.getClass().getName());
+                ImageOps.setDocuImage(di);
+				// worker threads
+				int nt = dlConfig.getAsInt("worker-threads");
+				DigilibWorker.setSemaphore(nt, true);
+				int mt = dlConfig.getAsInt("max-waiting-threads");
+				DigilibWorker.setMaxWaitingThreads(mt);
+				// set as the servlets main config
+				context.setAttribute("digilib.servlet.configuration", dlConfig);
+
+			} catch (Exception e) {
+				throw new ServletException(e);
+			}
+		} else {
+			// say hello in the log file
+			logger
+					.info("***** Digital Image Library Initialisation Servlet (version "
+							+ iniVersion + ") *****");
+			logger.warn("Already initialised?");
+			// set our AuthOps
+			useAuthentication = dlConfig.getAsBoolean("use-authorization");
+			// AuthOps instance
+			authOp = (AuthOps) dlConfig.getValue("servlet.auth.op");
+			// DocuDirCache instance
+			dirCache = (DocuDirCache) dlConfig.getValue("servlet.dir.cache");
+		}
+	}
+
+}
--- a/servlet/src/digilib/servlet/PDFCache.java	Thu Jan 14 14:30:30 2010 +0100
+++ b/servlet/src/digilib/servlet/PDFCache.java	Fri Feb 05 20:58:38 2010 +0100
@@ -4,8 +4,6 @@
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.IOException;
-import java.io.InputStream;
-import java.util.HashMap;
 
 import javax.servlet.RequestDispatcher;
 import javax.servlet.ServletConfig;
@@ -58,8 +56,12 @@
 	public void init(ServletConfig config) throws ServletException{
 		super.init(config);
 		
-		logger.info("initialized PDFCache v."+version);
-		
+        System.out.println("***** Digital Image Library Image PDF-Cache Servlet (version "
+                + version + ") *****");
+// say hello in the log file
+        logger.info("***** Digital Image Library Image PDF-Cache Servlet (version "
+                + version + ") *****");
+
 		context = config.getServletContext();
 		
 		dlConfig = (DigilibConfiguration) context.getAttribute("digilib.servlet.configuration");
@@ -117,23 +119,19 @@
 		
 		int status = getStatus(docid);
 		
-		
-		
-		if(status == STATUS_NONEXISTENT){
-			createNewPdfDocument(pdfji, docid); 
-			notifyUser(status, docid, request, response);
-		}
-		else if (status == STATUS_DONE){
-			try {
-				sendFile(docid, downloadFilename(pdfji), response);
-			} catch (IOException e) {
-				e.printStackTrace();
-				logger.error(e.getMessage());
-			}
-		}
-		else {
-			notifyUser(status, docid, request, response);			
-		}
+        if (status == STATUS_NONEXISTENT) {
+            createNewPdfDocument(pdfji, docid);
+            notifyUser(status, docid, request, response);
+        } else if (status == STATUS_DONE) {
+            try {
+                sendFile(docid, downloadFilename(pdfji), response);
+            } catch (IOException e) {
+                e.printStackTrace();
+                logger.error(e.getMessage());
+            }
+        } else {
+            notifyUser(status, docid, request, response);
+        }
 	}
 
 	/**
--- a/servlet/src/digilib/servlet/PDFJobInformation.java	Thu Jan 14 14:30:30 2010 +0100
+++ b/servlet/src/digilib/servlet/PDFJobInformation.java	Fri Feb 05 20:58:38 2010 +0100
@@ -1,8 +1,8 @@
 package digilib.servlet;
 
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
 import java.util.ArrayList;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
 
 import javax.servlet.http.HttpServletRequest;
 
@@ -80,7 +80,14 @@
 			String dh = image_info.getAsString("dh");
 			String pgs = getAsString("pgs");
 			
-			id = "fn=" + fn + "&dh=" + dh + "&pgs=" + pgs + ".pdf";		
+			id = "fn=" + fn + "&dh=" + dh + "&pgs=" + pgs + ".pdf";
+			// make safe to use as filename by urlencoding
+			try {
+                id = URLEncoder.encode(id, "UTF-8");
+            } catch (UnsupportedEncodingException e) {
+                // TODO Auto-generated catch block
+                e.printStackTrace();
+            }
 		}
 		else {
 			id = null;
--- a/servlet/src/digilib/servlet/PDFMaker.java	Thu Jan 14 14:30:30 2010 +0100
+++ b/servlet/src/digilib/servlet/PDFMaker.java	Fri Feb 05 20:58:38 2010 +0100
@@ -1,13 +1,9 @@
 package digilib.servlet;
 
 import java.io.File;
-import java.io.IOException;
-import java.util.HashMap;
 
-import javax.servlet.ServletConfig;
 import javax.servlet.ServletContext;
 import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletResponse;
 
 import org.apache.log4j.Logger;
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/servlet/src/digilib/servlet/Parameter.java	Fri Feb 05 20:58:38 2010 +0100
@@ -0,0 +1,275 @@
+/* Parameter -- General digilib parameter class.
+
+ Digital Image Library servlet components
+
+ Copyright (C) 2003 Robert Casties (robcast@mail.berlios.de)
+
+ This program is free software; you can redistribute  it and/or modify it
+ under  the terms of  the GNU General  Public License as published by the
+ Free Software Foundation;  either version 2 of the  License, or (at your
+ option) any later version.
+ 
+ Please read license.txt for the full details. A copy of the GPL
+ may be found at http://www.gnu.org/copyleft/lgpl.html
+
+ You should have received a copy of the GNU General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ 
+ *
+ * Created on 02.09.2003 by casties
+ * 
+ */
+package digilib.servlet;
+
+import java.io.File;
+import java.io.IOException;
+
+/**
+ * General digilib parameter class.
+ * 
+ * @author casties
+ *  
+ */
+public class Parameter {
+	/** real value */
+	protected Object value = null;
+
+	/** default value */
+	protected Object defval = null;
+
+	/** parameter name (e.g. in config file) */
+	protected String name = null;
+
+	/** parameter type */
+	protected int type = 0;
+
+	/**
+	 * Default constructor.
+	 *  
+	 */
+	public Parameter() {
+		super();
+	}
+
+	/**
+	 * Constructor with name, default, and value.
+	 * 
+	 * @param value
+	 * @param defval
+	 */
+	public Parameter(String name, Object defval, Object value) {
+		this.name = name;
+		this.value = value;
+		this.defval = defval;
+	}
+
+	/**
+	 * Constructor with name, default, value, and type.
+	 * 
+	 * @param value
+	 * @param defval
+	 */
+	public Parameter(String name, Object defval, Object value, int type) {
+		this.name = name;
+		this.value = value;
+		this.defval = defval;
+		this.type = type;
+	}
+
+	/**
+	 * Is the value valid.
+	 * 
+	 * @return
+	 */
+	public boolean hasValue() {
+		return (value != null);
+	}
+
+	/**
+	 * Try to set the value from a String.
+	 * 
+	 * Tries to convert the String to the same type as the default value. Sets
+	 * the value anyway if the default is null. Returns if the value could be
+	 * set.
+	 * 
+	 * @param val
+	 * @return
+	 */
+	public boolean setValueFromString(String val) {
+		// no default matches all
+		if (defval == null) {
+			this.value = val;
+			return true;
+		}
+		Class<? extends Object> c = defval.getClass();
+		// take String as is
+		if (c == String.class) {
+			this.value = val;
+			return true;
+		}
+		// set File
+		if (c == File.class) {
+			this.value = new File(val);
+			return true;
+		}
+		// set Boolean if string == "true"
+		if (c == Boolean.class) {
+			this.value = new Boolean(val.compareToIgnoreCase("true") == 0);
+			return true;
+		}
+		try {
+			// set Integer
+			if (c == Integer.class) {
+				this.value = new Integer(Integer.parseInt(val));
+				return true;
+			}
+			// set Float
+			if (c == Float.class) {
+				this.value = new Float(Float.parseFloat(val));
+				return true;
+			}
+		} catch (NumberFormatException e) {
+		}
+		// then it's unknown
+		return false;
+	}
+
+	/**
+	 * Get the default as Object.
+	 * 
+	 * @return
+	 */
+	public Object getDefault() {
+		return defval;
+	}
+
+	/**
+	 * Set the default.
+	 * 
+	 * @param defval
+	 */
+	public void setDefault(Object defval) {
+		this.defval = defval;
+	}
+
+	/**
+	 * Get the value as Object.
+	 * 
+	 * Returns the default if the value is not set.
+	 * 
+	 * @return
+	 */
+	public Object getValue() {
+		return (value != null) ? value : defval;
+	}
+
+	public int getAsInt() {
+		Integer i = (Integer) getValue();
+		return (i != null) ? i.intValue() : 0;
+	}
+
+	public float getAsFloat() {
+		Float f = (Float) getValue();
+		return (f != null) ? f.floatValue() : 0f;
+	}
+
+	public String getAsString() {
+		Object s = getValue();
+		if (s == null) {
+			return "";
+		}
+		if (s.getClass() == File.class) {
+			try {
+				return ((File) s).getCanonicalPath();
+			} catch (IOException e) {
+				return "ERR: " + s.toString();
+			}
+		}
+		return s.toString();
+	}
+
+	public boolean getAsBoolean() {
+		Boolean b = (Boolean) getValue();
+		return (b != null) ? b.booleanValue() : false;
+	}
+
+	public String[] parseAsArray(String separator) {
+		String s = getAsString();
+		String[] sa = s.split(separator);
+		return sa;
+	}
+
+	public float[] parseAsFloatArray(String separator) {
+		String s = getAsString();
+		String[] sa = s.split(separator);
+		float[] fa = null;
+		try {
+			int n = sa.length;
+			fa = new float[n];
+			for (int i = 0; i < n; i++) {
+				float f = Float.parseFloat(sa[i]);
+				fa[i] = f;
+			}
+		} catch (Exception e) {
+		}
+
+		return fa;
+	}
+
+	/**
+	 * Set the value.
+	 * 
+	 * @param value
+	 */
+	public void setValue(Object value) {
+		this.value = value;
+	}
+
+	/**
+	 * Set the value.
+	 * 
+	 * @param value
+	 */
+	public void setValue(int value) {
+		this.value = new Integer(value);
+	}
+
+	/**
+	 * Set the value.
+	 * 
+	 * @param value
+	 */
+	public void setValue(float value) {
+		this.value = new Float(value);
+	}
+
+	/**
+	 * @return
+	 */
+	public String getName() {
+		return name;
+	}
+
+	/**
+	 * @param name
+	 */
+	public void setName(String name) {
+		this.name = name;
+	}
+
+	/**
+	 * @return
+	 */
+	public int getType() {
+		return type;
+	}
+
+	/**
+	 * @param type
+	 */
+	public void setType(int type) {
+		this.type = type;
+	}
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/servlet/src/digilib/servlet/ParameterMap.java	Fri Feb 05 20:58:38 2010 +0100
@@ -0,0 +1,251 @@
+/* ParameterMap.java -- HashMap of Parameters.
+
+  Digital Image Library servlet components
+
+  Copyright (C) 2003 Robert Casties (robcast@mail.berlios.de)
+
+  This program is free software; you can redistribute  it and/or modify it
+  under  the terms of  the GNU General  Public License as published by the
+  Free Software Foundation;  either version 2 of the  License, or (at your
+  option) any later version.
+   
+  Please read license.txt for the full details. A copy of the GPL
+  may be found at http://www.gnu.org/copyleft/lgpl.html
+
+  You should have received a copy of the GNU General Public License
+  along with this program; if not, write to the Free Software
+  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+ * Created on 02.09.2003 by casties
+ *
+ */
+package digilib.servlet;
+
+import java.util.HashMap;
+
+/** HashMap of digilib.servlet.Parameter's.
+ * 
+ * Keys are Strings. Values are Parameters.
+ * 
+ * @author casties
+ *
+ */
+public class ParameterMap extends HashMap<String, Parameter> {
+
+	private static final long serialVersionUID = 1530820988748391313L;
+
+	/** Default constructor.
+	 * 
+	 */
+	public ParameterMap() {
+		super();
+	}
+
+	/** Construcotr with initial size.
+	 * @param arg0
+	 */
+	public ParameterMap(int arg0) {
+		super(arg0);
+	}
+
+	/** Get the Parameter with the corresponding key.
+	 * 
+	 * Returns null if no element is associated with key.
+	 * 
+	 * @param key
+	 * @return
+	 */
+	public Parameter get(String key) {
+		return super.get(key);
+	}
+
+	/** Get the Parameter with the corresponding key.
+	 * 
+	 * Returns null if no element is associated with key.
+	 * 
+	 * @param key
+	 * @return
+	 */
+	public Object getValue(String key) {
+		Parameter p = super.get(key);
+		return (p != null) ? p.getValue() : null;
+	}
+	
+	/** Get the Parameter with the corresponding key.
+	 * 
+	 * Returns null if no element is associated with key.
+	 * 
+	 * @param key
+	 * @return
+	 */
+	public String getAsString(String key) {
+		Parameter p = super.get(key);
+		return (p != null) ? p.getAsString() : null;
+	}
+
+	/** Get the Parameter with the corresponding key.
+	 * 
+	 * Returns null if no element is associated with key.
+	 * 
+	 * @param key
+	 * @return
+	 */
+	public int getAsInt(String key) {
+		Parameter p = super.get(key);
+		return (p != null) ? p.getAsInt() : 0;
+	}
+
+	/** Get the Parameter with the corresponding key.
+	 * 
+	 * Returns null if no element is associated with key.
+	 * 
+	 * @param key
+	 * @return
+	 */
+	public float getAsFloat(String key) {
+		Parameter p = super.get(key);
+		return (p != null) ? p.getAsFloat() : 0f;
+	}
+
+	/** Get the Parameter with the corresponding key.
+	 * 
+	 * Returns null if no element is associated with key.
+	 * 
+	 * @param key
+	 * @return
+	 */
+	public boolean getAsBoolean(String key) {
+		Parameter p = super.get(key);
+		return (p != null) ? p.getAsBoolean() : false;
+	}
+
+	/** Returns if the Parameter's value has been set.
+	 * 
+	 * @param key
+	 * @return
+	 */
+	public boolean hasValue(String key) {
+		Parameter p = super.get(key);
+		return (p != null) ? p.hasValue() : false;
+	}
+	
+	/** Add the Parameter to the map with a certain key.
+	 * 
+	 * Returns the value that was previously associated with key. 
+	 * 
+	 * @param key
+	 * @param val
+	 * @return
+	 */
+	public Parameter put(String key, Parameter val) {
+		return super.put(key, val);
+	}
+
+	/** Add the Parameter val to the map, using val's name.
+	 * 
+	 * Returns the value that was previously associated with val's name. 
+	 * 
+	 * @param val
+	 * @return
+	 */
+	public Parameter put(Parameter val) {
+		return super.put(val.getName(), val);
+	}
+	
+	/** Add a new Parameter with name, default and value.
+	 * 
+	 * Returns the key that was previously associated with name. 
+	 * 
+	 * @param name
+	 * @param def
+	 * @param val
+	 * @return
+	 */
+	public Parameter newParameter(String name, Object def, Object val) {
+		Parameter p = new Parameter(name, def, val);
+		return super.put(name, p);
+	}
+
+	/** Add a new Parameter with name, default, value and type.
+	 * 
+	 * Returns the key that was previously associated with name. 
+	 * 
+	 * @param name
+	 * @param def
+	 * @param val
+	 * @param type
+	 * @return
+	 */
+	public Parameter newParameter(String name, Object def, Object val, int type) {
+		Parameter p = new Parameter(name, def, val, type);
+		return super.put(name, p);
+	}
+
+	/** Set the value of an existing parameter.
+	 * 
+	 * Sets the value and returns true if the parameter exists.
+	 * 
+	 * @param key
+	 * @param val
+	 * @return
+	 */
+	public boolean setValue(String key, Object val) {
+		Parameter p = get(key);
+		if (p != null) {
+			p.setValue(val);
+			return true;
+		}
+		return false;
+	}
+
+	/** Set the value of an existing parameter.
+	 * 
+	 * Sets the value and returns true if the parameter exists.
+	 * 
+	 * @param key
+	 * @param val
+	 * @return
+	 */
+	public boolean setValue(String key, int val) {
+		Parameter p = get(key);
+		if (p != null) {
+			p.setValue(val);
+			return true;
+		}
+		return false;
+	}
+
+	/** Set the value of an existing parameter.
+	 * 
+	 * Sets the value and returns true if the parameter exists.
+	 * 
+	 * @param key
+	 * @param val
+	 * @return
+	 */
+	public boolean setValue(String key, float val) {
+		Parameter p = get(key);
+		if (p != null) {
+			p.setValue(val);
+			return true;
+		}
+		return false;
+	}
+
+	/** Set the value of an existing parameter.
+	 * 
+	 * Sets the value and returns true if the parameter exists.
+	 * 
+	 * @param key
+	 * @param val
+	 * @return
+	 */
+	public boolean setValueFromString(String key, String val) {
+		Parameter p = get(key);
+		if (p != null) {
+			p.setValueFromString(val);
+			return true;
+		}
+		return false;
+	}
+}
--- a/servlet/src/digilib/servlet/RequestHandler.java	Thu Jan 14 14:30:30 2010 +0100
+++ b/servlet/src/digilib/servlet/RequestHandler.java	Fri Feb 05 20:58:38 2010 +0100
@@ -1,7 +1,5 @@
 package digilib.servlet;
 
-import java.io.InputStream;
-
 import javax.servlet.ServletConfig;
 import javax.servlet.ServletException;
 import javax.servlet.http.HttpServlet;
--- a/servlet/src/digilib/servlet/Scaler.java	Thu Jan 14 14:30:30 2010 +0100
+++ b/servlet/src/digilib/servlet/Scaler.java	Fri Feb 05 20:58:38 2010 +0100
@@ -1,11 +1,7 @@
 package digilib.servlet;
 
-import java.awt.Image;
-import java.awt.geom.Rectangle2D;
-import java.io.BufferedOutputStream;
 import java.io.File;
 import java.io.IOException;
-import java.io.InputStream;
 import java.io.OutputStream;
 import java.util.List;
 
@@ -15,8 +11,6 @@
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 
-import org.apache.log4j.Logger;
-
 import digilib.auth.AuthOpException;
 import digilib.auth.AuthOps;
 import digilib.image.ImageOpException;
@@ -26,7 +20,6 @@
 import digilib.io.FileOpException;
 import digilib.io.FileOps;
 import digilib.io.ImageFile;
-import digilib.io.ImageFileset;
 
 
 // TODO digilibError is not used anymore and may need to get reintegrated
@@ -248,7 +241,7 @@
 			/* check permissions */
 			if (useAuthorization) {
 				// get a list of required roles (empty if no restrictions)
-				List rolesRequired;
+				List<String> rolesRequired;
 				try {
 					rolesRequired = authOp.rolesForPath(jobdeclaration.getFilePath(), request);
 					if (rolesRequired != null) {