Thorough rework of paths. Now every album (including top level) end in name.album, where name is the name of the directory.

Removed EntryDao and moved most of the logic to DirectoryEntry
This commit is contained in:
knut 2009-01-11 08:57:28 +00:00
parent 167e214a38
commit 01e246023d
11 changed files with 601 additions and 561 deletions

View file

@ -8,21 +8,10 @@
<classpathentry kind="var" path="MAVEN_REPO/commons-digester/jars/commons-digester-1.7.jar"/> <classpathentry kind="var" path="MAVEN_REPO/commons-digester/jars/commons-digester-1.7.jar"/>
<classpathentry kind="var" path="MAVEN_REPO/commons-collections/jars/commons-collections-3.1.jar"/> <classpathentry kind="var" path="MAVEN_REPO/commons-collections/jars/commons-collections-3.1.jar"/>
<classpathentry kind="var" path="MAVEN_REPO/commons-lang/jars/commons-lang-2.0.jar"/> <classpathentry kind="var" path="MAVEN_REPO/commons-lang/jars/commons-lang-2.0.jar"/>
<classpathentry kind="var" path="MAVEN_REPO/postgresql/jars/postgresql-8.1-408.jdbc3.jar"/>
<classpathentry kind="var" path="MAVEN_REPO/velocity/jars/velocity-1.5.jar"/> <classpathentry kind="var" path="MAVEN_REPO/velocity/jars/velocity-1.5.jar"/>
<classpathentry kind="var" path="MAVEN_REPO/velocity-tools/jars/velocity-tools-view-1.2.jar"/> <classpathentry kind="var" path="MAVEN_REPO/velocity-tools/jars/velocity-tools-view-1.2.jar"/>
<classpathentry kind="var" path="MAVEN_REPO/com.drew/jars/metadata-extractor-2.3.1.jar"/> <classpathentry kind="var" path="MAVEN_REPO/com.drew/jars/metadata-extractor-2.3.1.jar"/>
<classpathentry kind="var" path="MAVEN_REPO/servletapi/jars/servletapi-2.4.jar"/> <classpathentry kind="var" path="MAVEN_REPO/servletapi/jars/servletapi-2.4.jar"/>
<classpathentry kind="var" path="MAVEN_REPO/junit/jars/junit-4.1.jar"/> <classpathentry kind="var" path="MAVEN_REPO/junit/jars/junit-4.4.jar"/>
<classpathentry kind="var" path="MAVEN_REPO/resin-3.1/jars/resin-3.1.5.jar"/>
<classpathentry kind="var" path="MAVEN_REPO/resin-3.1/jars/resin-util-3.1.5.jar"/>
<classpathentry kind="var" path="MAVEN_REPO/resin-3.1/jars/jta-101-3.1.5.jar"/>
<classpathentry kind="var" path="MAVEN_REPO/resin-3.1/jars/webbeans-16-3.1.5.jar"/>
<classpathentry kind="var" path="MAVEN_REPO/resin-3.1/jars/ejb-15-3.1.5.jar"/>
<classpathentry kind="var" path="MAVEN_REPO/resin-3.1/jars/jpa-15-3.1.5.jar"/>
<classpathentry kind="var" path="MAVEN_REPO/resin-3.1/jars/jsdk-15-3.1.5.jar" sourcepath="/MAVEN_REPO/resin-3.1/sources/resin-3.1.5-src.zip"/>
<classpathentry kind="var" path="MAVEN_REPO/resin-3.1/jars/hessian-3.1.5.jar"/>
<classpathentry kind="var" path="MAVEN_REPO/resin-3.1/jars/jca-15-3.1.5.jar"/>
<classpathentry kind="var" path="MAVEN_REPO/resin-3.1/jars/jstl-11-3.1.5.jar"/>
<classpathentry kind="output" path="build/eclipse"/> <classpathentry kind="output" path="build/eclipse"/>
</classpath> </classpath>

View file

@ -1,84 +0,0 @@
/**
*
*/
package org.forkalsrud.album.exif;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
/**
* @author knut
*
*/
public class Album {
Entry cover;
List<Entry> contents = new ArrayList<Entry>();
public Entry getCover() {
return cover;
}
public List<Entry> getContents() {
return contents;
}
public void setCover(Entry cover) {
this.cover = cover;
}
public void addContents(Entry entry) {
contents.add(entry);
}
public void sort() {
Collections.sort(contents, new Comparator<Entry>() {
public int compare(Entry e1, Entry e2) {
if (!e1.isFile() && e2.isFile()) {
return -1;
} else if (e1.isFile() && !e2.isFile()) {
return +1;
}
Date d1 = e1.getDate();
Date d2 = e2.getDate();
if (d1 != null && d2 != null) {
return d1.compareTo(d2);
} else if (d1 != null) {
return -1;
} else if (d2 != null) {
return +1;
} else {
return 0;
}
}
});
fillLinkedList();
}
void fillLinkedList() {
Entry prev = null;
for (Entry e : contents) {
e.prev = prev;
if (prev != null) {
prev.next = e;
}
prev = e;
}
if (prev != null) {
prev.next = null;
}
}
}

View file

@ -0,0 +1,331 @@
/**
*
*/
package org.forkalsrud.album.exif;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Properties;
import javax.imageio.ImageIO;
import javax.imageio.ImageReadParam;
import javax.imageio.ImageReader;
import javax.imageio.stream.ImageInputStream;
import com.drew.imaging.jpeg.JpegMetadataReader;
import com.drew.imaging.jpeg.JpegProcessingException;
import com.drew.metadata.Directory;
import com.drew.metadata.Metadata;
import com.drew.metadata.MetadataException;
import com.drew.metadata.exif.ExifDirectory;
import com.drew.metadata.jpeg.JpegDirectory;
/**
* @author knut
*/
public class DirectoryEntry extends Entry {
final static String CACHE_FILE = "cache.properties";
final static String OVERRIDE_FILE = "album.properties";
File cache;
List<Entry> children = new ArrayList<Entry>();
boolean childrenLoaded = false;
public DirectoryEntry(File file) {
super(file);
if (!file.isDirectory()) {
throw new RuntimeException("Use DirectoryEntry only for directories: " + file);
}
cache = new File(file, CACHE_FILE);
}
public DirectoryEntry(Entry parent, File file) {
this(file);
this.parent = parent;
}
public static Entry getEntry(File f) {
return new DirectoryEntry(f.getParentFile()).get(f);
}
@Override
public boolean isFile() {
return false;
}
public List<Entry> getContents() {
loadChildren();
return children;
}
@Override
public Thumbnail getThumbnail() {
loadChildren();
return super.getThumbnail();
}
@Override
public String getCaption() {
loadChildren();
return super.getCaption();
}
public void addContents(Entry entry) {
children.add(entry);
}
public Entry get(File f) {
loadChildren();
for (Entry e : children) {
if (f.equals(e.file)) {
return e;
}
}
return null;
}
protected void loadChildren() {
if (childrenLoaded) {
return;
}
try {
Properties cachedProps = isCacheCurrent() ? loadFromFile(cache) : generateCache();
Properties combined = new Properties();
combined.putAll(cachedProps);
File override = new File(file, OVERRIDE_FILE);
if (override.exists() && override.isFile() && override.canRead()) {
Properties overrideProps = loadFromFile(override);
combined.putAll(overrideProps);
}
populate(combined);
sort();
fillLinkedList();
childrenLoaded = true;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
boolean isCacheCurrent() {
return cache.exists() && cache.isFile() && cache.canRead() && file.lastModified() <= cache.lastModified();
}
Properties loadFromFile(File propFile) throws IOException {
Properties props = new Properties();
props.load(new FileInputStream(propFile));
return props;
}
Properties generateCache() throws MetadataException, JpegProcessingException, IOException {
Properties props = new Properties();
generateFileEntries(props);
props.store(new FileOutputStream(cache), "Extra Comments");
return props;
}
void generateFileEntries(Properties props)
throws IOException, MetadataException, JpegProcessingException {
File[] files = file.listFiles();
for (File f : files) {
String name = f.getName();
if (f.isDirectory()) {
if ("CVS".equals(name)) {
continue;
}
generateDirectoryProperties(props, f);
continue;
}
if (file.isHidden()) {
continue;
}
if (CACHE_FILE.equals(name) || OVERRIDE_FILE.equals(name)) {
continue;
}
if (name.endsWith(".jpg") || name.endsWith(".jpeg") || name.endsWith(".JPG")) {
generateThumbnailProperties(props, f);
}
}
}
private void generateDirectoryProperties(Properties props, File f) {
String name = f.getName();
props.setProperty("dir." + name, "present");
}
private void generateThumbnailProperties(Properties props, File f)
throws JpegProcessingException, MetadataException, IOException {
String name = f.getName();
String base = "file." + name + ".";
boolean hasDate = false;
boolean hasOrientation = false;
boolean hasDim = false;
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd-HHmmss");
NumberFormat nf = new DecimalFormat("0");
Metadata metadata = JpegMetadataReader.readMetadata(f);
Directory exifDirectory = metadata.getDirectory(ExifDirectory.class);
if (exifDirectory.containsTag(ExifDirectory.TAG_ORIENTATION)) {
int orientation = exifDirectory.getInt(ExifDirectory.TAG_ORIENTATION);
props.setProperty(base + "orientation", nf.format(orientation));
hasOrientation = true;
}
if (exifDirectory.containsTag(ExifDirectory.TAG_EXIF_IMAGE_WIDTH) &&
exifDirectory.containsTag(ExifDirectory.TAG_EXIF_IMAGE_HEIGHT)) {
int width = exifDirectory.getInt(ExifDirectory.TAG_EXIF_IMAGE_WIDTH);
int height = exifDirectory.getInt(ExifDirectory.TAG_EXIF_IMAGE_HEIGHT);
props.setProperty(base + "dimensions", new Dimension(width, height).toString());
hasDim = true;
}
if (exifDirectory.containsTag(ExifDirectory.TAG_DATETIME_ORIGINAL)) {
Date captureDate = exifDirectory.getDate(ExifDirectory.TAG_DATETIME_ORIGINAL);
props.setProperty(base + "captureDate", sdf.format(captureDate));
hasDate = true;
}
if (exifDirectory.containsTag(ExifDirectory.TAG_USER_COMMENT)) {
String comment = exifDirectory.getString(ExifDirectory.TAG_USER_COMMENT);
props.setProperty(base + "comment", comment);
}
Directory jpegDirectory = metadata.getDirectory(JpegDirectory.class);
if (jpegDirectory.containsTag(JpegDirectory.TAG_JPEG_IMAGE_WIDTH) &&
jpegDirectory.containsTag(JpegDirectory.TAG_JPEG_IMAGE_HEIGHT)) {
int width = jpegDirectory.getInt(JpegDirectory.TAG_JPEG_IMAGE_WIDTH);
int height = jpegDirectory.getInt(JpegDirectory.TAG_JPEG_IMAGE_HEIGHT);
props.setProperty(base + "dimensions", new Dimension(width, height).toString());
hasDim = true;
}
props.setProperty(base + "etag", Integer.toHexString(name.hashCode() + Long.valueOf(f.lastModified()).hashCode()));
if (!hasDate) {
props.setProperty(base + "captureDate", sdf.format(new Date(f.lastModified())));
}
if (!hasOrientation) {
props.setProperty(base + "orientation", "1");
}
if (!hasDim) {
Dimension dim = decodeImageForDimensions(f);
if (dim != null) {
props.setProperty(base + "dimensions", dim.toString());
hasDim = true;
}
}
}
Dimension decodeImageForDimensions(File file) throws IOException {
String suffix = null;
String name = file.getName();
if (name.indexOf('.') > 0) {
suffix = name.substring(name.lastIndexOf('.') + 1);
}
Iterator<ImageReader> readers = ImageIO.getImageReadersBySuffix(suffix);
if (!readers.hasNext()) {
return null;
}
ImageReader reader = readers.next();
ImageInputStream iis = ImageIO.createImageInputStream(file);
reader.setInput(iis, true);
ImageReadParam param = reader.getDefaultReadParam();
BufferedImage img = reader.read(0, param);
return new Dimension(img.getWidth(), img.getHeight());
}
private void sort() {
Collections.sort(children, new Comparator<Entry>() {
public int compare(Entry e1, Entry e2) {
if (!e1.isFile() && e2.isFile()) {
return -1;
} else if (e1.isFile() && !e2.isFile()) {
return +1;
}
Date d1 = e1.getDate();
Date d2 = e2.getDate();
if (d1 != null && d2 != null) {
return d1.compareTo(d2);
} else if (d1 != null) {
return -1;
} else if (d2 != null) {
return +1;
} else {
return 0;
}
}
});
}
void fillLinkedList() {
Entry prev = null;
for (Entry e : children) {
e.prev = prev;
if (prev != null) {
prev.next = e;
}
prev = e;
}
if (prev != null) {
prev.next = null;
}
}
private void populate(Properties props) throws ParseException {
String coverFileName = props.getProperty("cover");
HashMap<String, Entry> entryMap = new HashMap<String, Entry>();
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd-HHmmss");
Iterator i = props.keySet().iterator();
while (i.hasNext()) {
String key = (String)i.next();
if (key.startsWith("file.") && key.endsWith(".dimensions")) {
String name = key.substring("file.".length(), key.length() - ".dimensions".length());
if (!entryMap.containsKey(name)) {
File f = new File(file, name);
FileEntry entry = new FileEntry(this, f);
Thumbnail thumbnail = new Thumbnail(f);
entry.setCaption(props.getProperty("file." + name + ".caption"));
entry.setDate(sdf.parse(props.getProperty("file." + name + ".captureDate")));
thumbnail.setSize(new Dimension(props.getProperty("file." + name + ".dimensions")));
thumbnail.setOrientation(Integer.parseInt(props.getProperty("file." + name + ".orientation")));
thumbnail.setEtag(props.getProperty("file." + name + ".etag"));
entry.setThumbnail(thumbnail);
children.add(entry);
if (name != null && (coverFileName == null || name.equals(coverFileName))) {
setThumbnail(thumbnail);
}
}
} else if (key.startsWith("dir.")) {
String name = key.substring("dir.".length());
children.add(new DirectoryEntry(this, new File(file, name)));
}
}
}
}

View file

@ -5,6 +5,7 @@
package org.forkalsrud.album.exif; package org.forkalsrud.album.exif;
import java.io.File;
import java.util.Date; import java.util.Date;
@ -13,67 +14,45 @@ import java.util.Date;
* *
* @author knut * @author knut
*/ */
public class Entry { public abstract class Entry {
boolean isFile; /**
String name; * The object in the file system that this entry represents
String path; */
Dimension size; File file;
Thumbnail thumbnail;
String caption; String caption;
Date date; Date date;
int orientation;
Entry next; Entry next;
Entry prev; Entry prev;
String etag; Entry parent;
protected Entry(File file) {
this.file = file;
}
public boolean isFile() { public abstract boolean isFile();
return isFile;
}
public void setFile(boolean isFile) {
this.isFile = isFile;
}
/** /**
* @return Returns the name. * @return Returns the name.
*/ */
public String getName() { public String getName() {
return name; return file.getName();
} }
/** public Thumbnail getThumbnail() {
* @param name The name to set. return thumbnail;
*/ }
public void setName(String name) {
this.name = name;
}
public void setThumbnail(Thumbnail thumbnail) {
/** this.thumbnail = thumbnail;
* @return Returns the size. }
*/
public Dimension getSize() {
return getOrientation() == 6 ? size.flip() : size;
}
/**
/**
* @param size The size to set.
*/
public void setSize(Dimension size) {
this.size = size;
}
/**
* @return Returns the caption. * @return Returns the caption.
*/ */
public String getCaption() { public String getCaption() {
@ -109,51 +88,16 @@ public class Entry {
/**
* @return Returns the orientation.
*/
public int getOrientation() {
return orientation;
}
/**
* @param orientation The orientation to set.
*/
public void setOrientation(int orientation) {
this.orientation = orientation;
}
public Entry prev() { public Entry prev() {
return prev; return prev;
} }
public Entry next() { public Entry next() {
return next; return next;
} }
public Entry parent() {
return parent;
public String getEtag() {
return etag;
} }
public File getPath() {
return file;
public void setEtag(String etag) {
this.etag = etag;
} }
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
} }

View file

@ -1,260 +0,0 @@
/*
* Copyright 2008 Estalea, Inc. All rights reserved.
* This software is the proprietary information of Estalea, Inc.
*/
package org.forkalsrud.album.exif;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Properties;
import javax.imageio.ImageIO;
import javax.imageio.ImageReadParam;
import javax.imageio.ImageReader;
import javax.imageio.stream.ImageInputStream;
import com.drew.imaging.jpeg.JpegMetadataReader;
import com.drew.imaging.jpeg.JpegProcessingException;
import com.drew.metadata.Directory;
import com.drew.metadata.Metadata;
import com.drew.metadata.MetadataException;
import com.drew.metadata.exif.ExifDirectory;
import com.drew.metadata.jpeg.JpegDirectory;
/**
* Responsible for translating entries to and from file
*
* @author knut
*/
public class EntryDao {
final static String CACHE_FILE = "cache.properties";
final static String OVERRIDE_FILE = "album.properties";
boolean isCurrent(File directory) {
return directory.lastModified() <= new File(directory, CACHE_FILE).lastModified();
}
public Album read(File directory) throws FileNotFoundException, IOException, JpegProcessingException, MetadataException, ParseException {
List<Entry> entries = new ArrayList<Entry>();
Properties cachedProps = new Properties();
File cache = new File(directory, CACHE_FILE);
if (cache.exists() && cache.isFile() && cache.canRead() && isCurrent(directory)) {
cachedProps.load(new FileInputStream(cache));
} else {
generateEntries(directory, cachedProps);
}
Properties overrideProps = new Properties();
File override = new File(directory, OVERRIDE_FILE);
if (override.exists() && override.isFile() && override.canRead()) {
overrideProps.load(new FileInputStream(override));
}
Properties combined = new Properties();
combined.putAll(cachedProps);
combined.putAll(overrideProps);
Entry cover = populate(combined, entries);
Album alb = new Album();
alb.setCover(cover);
for (Entry e : entries) {
alb.addContents(e);
}
generateDirectoryEntries(directory, alb);
alb.sort();
return alb;
}
private Entry populate(Properties cachedProps, List<Entry> entries) throws ParseException {
Entry cover = null;
String coverFileName = cachedProps.getProperty("cover");
HashMap<String, Entry> entryMap = new HashMap<String, Entry>();
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd-HHmmss");
Iterator i = cachedProps.keySet().iterator();
while (i.hasNext()) {
String key = (String)i.next();
if (key.startsWith("file.") && key.endsWith(".dimensions")) {
String name = key.substring("file.".length(), key.length() - ".dimensions".length());
if (!entryMap.containsKey(name)) {
Entry entry = new Entry();
entry.setFile(true);
entry.setName(name);
entry.setPath(name);
entry.setDate(sdf.parse(cachedProps.getProperty("file." + name + ".captureDate")));
entry.setSize(new Dimension(cachedProps.getProperty("file." + name + ".dimensions")));
entry.setCaption(cachedProps.getProperty("file." + name + ".caption"));
entry.setOrientation(Integer.parseInt(cachedProps.getProperty("file." + name + ".orientation")));
entry.setEtag(cachedProps.getProperty("file." + name + ".etag"));
entries.add(entry);
if (name != null && name.equals(coverFileName)) {
cover = entry;
}
}
}
}
return cover;
}
public Entry readFile(File file) throws FileNotFoundException, IOException, JpegProcessingException, MetadataException, ParseException {
List<Entry> dir = read(file.getParentFile()).getContents();
String name = file.getName();
for (Entry e : dir) {
if (name.equals(e.name)) {
return e;
}
}
return null;
}
void generateEntries(File directory, Properties cachedProps) throws JpegProcessingException, MetadataException, FileNotFoundException, IOException {
generateFileEntries(directory, cachedProps);
File dst = new File(directory, CACHE_FILE);
if (directory.canWrite()) {
cachedProps.store(new FileOutputStream(dst), "Extra Comments");
}
}
void generateDirectoryEntries(File directory, Album album) throws FileNotFoundException, JpegProcessingException, MetadataException, IOException, ParseException {
File[] dirs = directory.listFiles(new FileFilter() {
public boolean accept(File file) {
return !file.isHidden() && file.isDirectory() && !CACHE_FILE.equals(file.getName()) && !OVERRIDE_FILE.equals(file.getName());
}
});
for (File dir : dirs) {
Album childAlbum = read(dir);
Entry childCover = childAlbum.getCover();
if (childCover == null) {
Iterator<Entry> i = childAlbum.getContents().iterator();
childCover = i.hasNext() ? i.next() : null;
}
if (childCover != null) {
childCover.setFile(false);
childCover.setName(dir.getName());
childCover.setPath(dir.getName() + "/" + childCover.getPath());
album.addContents(childCover);
}
}
}
void generateFileEntries(File directory, Properties cachedProps)
throws JpegProcessingException, MetadataException, IOException {
File[] files = directory.listFiles(new FileFilter() {
public boolean accept(File file) {
String name = file.getName();
boolean isImageFile = name.endsWith(".jpg") || name.endsWith(".jpeg") || name.endsWith(".JPG");
return isImageFile && !file.isHidden() && !file.isDirectory() && !CACHE_FILE.equals(file.getName()) && !OVERRIDE_FILE.equals(file.getName());
}
});
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd-HHmmss");
NumberFormat nf = new DecimalFormat("0");
for (File f : files) {
String name = f.getName();
String base = "file." + name + ".";
boolean hasDate = false;
boolean hasOrientation = false;
boolean hasDim = false;
Metadata metadata = JpegMetadataReader.readMetadata(f);
Directory exifDirectory = metadata.getDirectory(ExifDirectory.class);
if (exifDirectory.containsTag(ExifDirectory.TAG_ORIENTATION)) {
int orientation = exifDirectory.getInt(ExifDirectory.TAG_ORIENTATION);
cachedProps.setProperty(base + "orientation", nf.format(orientation));
hasOrientation = true;
}
if (exifDirectory.containsTag(ExifDirectory.TAG_EXIF_IMAGE_WIDTH) &&
exifDirectory.containsTag(ExifDirectory.TAG_EXIF_IMAGE_HEIGHT)) {
int width = exifDirectory.getInt(ExifDirectory.TAG_EXIF_IMAGE_WIDTH);
int height = exifDirectory.getInt(ExifDirectory.TAG_EXIF_IMAGE_HEIGHT);
cachedProps.setProperty(base + "dimensions", new Dimension(width, height).toString());
hasDim = true;
}
if (exifDirectory.containsTag(ExifDirectory.TAG_DATETIME_ORIGINAL)) {
Date captureDate = exifDirectory.getDate(ExifDirectory.TAG_DATETIME_ORIGINAL);
cachedProps.setProperty(base + "captureDate", sdf.format(captureDate));
hasDate = true;
}
if (exifDirectory.containsTag(ExifDirectory.TAG_USER_COMMENT)) {
String comment = exifDirectory.getString(ExifDirectory.TAG_USER_COMMENT);
cachedProps.setProperty(base + "comment", comment);
}
Directory jpegDirectory = metadata.getDirectory(JpegDirectory.class);
if (jpegDirectory.containsTag(JpegDirectory.TAG_JPEG_IMAGE_WIDTH) &&
jpegDirectory.containsTag(JpegDirectory.TAG_JPEG_IMAGE_HEIGHT)) {
int width = jpegDirectory.getInt(JpegDirectory.TAG_JPEG_IMAGE_WIDTH);
int height = jpegDirectory.getInt(JpegDirectory.TAG_JPEG_IMAGE_HEIGHT);
cachedProps.setProperty(base + "dimensions", new Dimension(width, height).toString());
hasDim = true;
}
cachedProps.setProperty(base + "etag", Integer.toHexString(name.hashCode() + Long.valueOf(f.lastModified()).hashCode()));
if (!hasDate) {
cachedProps.setProperty(base + "captureDate", sdf.format(new Date(f.lastModified())));
}
if (!hasOrientation) {
cachedProps.setProperty(base + "orientation", "1");
}
if (!hasDim) {
Dimension dim = decodeImageForDimensions(f);
if (dim != null) {
cachedProps.setProperty(base + "dimensions", dim.toString());
hasDim = true;
}
}
}
}
Dimension decodeImageForDimensions(File file) throws IOException {
String suffix = null;
String name = file.getName();
if (name.indexOf('.') > 0) {
suffix = name.substring(name.lastIndexOf('.') + 1);
}
Iterator<ImageReader> readers = ImageIO.getImageReadersBySuffix(suffix);
if (!readers.hasNext()) {
return null;
}
ImageReader reader = readers.next();
ImageInputStream iis = ImageIO.createImageInputStream(file);
reader.setInput(iis, true);
ImageReadParam param = reader.getDefaultReadParam();
BufferedImage img = reader.read(0, param);
return new Dimension(img.getWidth(), img.getHeight());
}
}

View file

@ -0,0 +1,29 @@
/**
*
*/
package org.forkalsrud.album.exif;
import java.io.File;
/**
* @author knut
*
*/
public class FileEntry extends Entry {
public FileEntry(Entry parent, File file) {
super(file);
if (!file.isFile()) {
throw new RuntimeException("Use FileEntry only for files: " + file);
}
this.parent = parent;
}
@Override
public boolean isFile() {
return true;
}
}

View file

@ -0,0 +1,71 @@
/**
*
*/
package org.forkalsrud.album.exif;
import java.io.File;
/**
* @author knut
*
*/
public class Thumbnail {
File path;
Dimension size;
int orientation;
String etag;
public Thumbnail(File path) {
this.path = path;
}
/**
* @return Returns the orientation.
*/
public int getOrientation() {
return orientation;
}
/**
* @param orientation The orientation to set.
*/
public void setOrientation(int orientation) {
this.orientation = orientation;
}
/**
* @return Returns the size.
*/
public Dimension getSize() {
return getOrientation() == 6 ? size.flip() : size;
}
/**
* @param size The size to set.
*/
public void setSize(Dimension size) {
this.size = size;
}
public String getEtag() {
return etag;
}
public void setEtag(String etag) {
this.etag = etag;
}
public File getPath() {
return path;
}
}

View file

@ -5,9 +5,7 @@ import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.text.ParseException;
import java.util.Iterator; import java.util.Iterator;
import java.util.List;
import javax.imageio.ImageIO; import javax.imageio.ImageIO;
import javax.imageio.ImageReadParam; import javax.imageio.ImageReadParam;
@ -22,92 +20,120 @@ import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import org.forkalsrud.album.exif.Dimension; import org.forkalsrud.album.exif.Dimension;
import org.forkalsrud.album.exif.DirectoryEntry;
import org.forkalsrud.album.exif.Entry; import org.forkalsrud.album.exif.Entry;
import org.forkalsrud.album.exif.EntryDao; import org.forkalsrud.album.exif.FileEntry;
import org.forkalsrud.album.exif.Thumbnail;
import com.drew.imaging.jpeg.JpegProcessingException;
import com.drew.metadata.MetadataException;
public class AlbumServlet public class AlbumServlet
extends HttpServlet extends HttpServlet
{ {
String basePath = "photos"; File base;
EntryDao dao; String basePrefix;
@Override @Override
public void init() public void init()
throws ServletException throws ServletException
{ {
System.out.println("in init of Album"); System.out.println("in init of Album");
dao = new EntryDao(); base = new File(getServletConfig().getInitParameter("base")).getAbsoluteFile();
basePrefix = "/" + base.getName();
} }
@Override @Override
public void doGet(HttpServletRequest req, HttpServletResponse res) public void doGet(HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException throws ServletException, IOException
{ {
req.setAttribute("assets", req.getContextPath() + "/assets");
req.setAttribute("req", req);
req.setAttribute("base", req.getContextPath() + req.getServletPath());
req.setAttribute("mapper", new Mapper());
String pathInfo = req.getPathInfo(); String pathInfo = req.getPathInfo();
if (pathInfo != null && pathInfo.startsWith(basePrefix)) {
pathInfo = pathInfo.substring(basePrefix.length());
} else {
res.sendError(HttpServletResponse.SC_NOT_FOUND);
return;
}
String page = null; String page = null;
if (pathInfo == null || "/".equals(pathInfo)) {
page = "album";
} else if (pathInfo.endsWith(".album")) {
pathInfo = pathInfo.substring(0, pathInfo.length() - ".album".length());
page = "album";
}
if (page != null) {
DirectoryEntry entry = null;
entry = (DirectoryEntry)resolveEntry(pathInfo);
handleAlbum(req, res, entry);
return;
}
if (pathInfo.endsWith(".photo")) { if (pathInfo.endsWith(".photo")) {
pathInfo = pathInfo.substring(0, pathInfo.length() - ".photo".length()); pathInfo = pathInfo.substring(0, pathInfo.length() - ".photo".length());
page = "photo"; page = "photo";
handlePhoto(req, res, (FileEntry)resolveEntry(pathInfo));
return;
} }
/*
int parentPos = pathInfo.substring(0, pathInfo.length() - 1).lastIndexOf('/'); int parentPos = pathInfo.substring(0, pathInfo.length() - 1).lastIndexOf('/');
if (parentPos >= 0) { if (parentPos >= 0) {
req.setAttribute("parent", req.getServletPath() + pathInfo.substring(0, parentPos) + "/"); req.setAttribute("parent", req.getServletPath() + pathInfo.substring(0, parentPos) + "/");
} }
req.setAttribute("assets", "/" + req.getContextPath() + "assets"); req.setAttribute("assets", "/" + req.getContextPath() + "assets");
File file = new File(basePath + pathInfo); */
// System.out.println("path=" + req.getContextPath()); File file = new File(base, pathInfo);
if (!file.canRead()) { if (!file.canRead()) {
res.setStatus(HttpServletResponse.SC_FORBIDDEN); res.setStatus(HttpServletResponse.SC_FORBIDDEN);
return; return;
} }
if (file.isDirectory()) { if (req.getParameter("size") != null) {
if (!pathInfo.endsWith("/")) {
res.sendRedirect(req.getContextPath() + req.getRequestURI() + "/");
return;
}
try {
List<Entry> entries = dao.read(file).getContents();
res.setContentType("text/html");
req.setAttribute("directory", file.getName());
req.setAttribute("path", req.getServletPath());
req.setAttribute("entries", entries);
RequestDispatcher rd = req.getRequestDispatcher("/WEB-INF/velocity/directory.vm");
rd.forward(req, res);
} catch (Exception e) {
throw new RuntimeException("sadness", e);
}
} else if (req.getParameter("size") != null) {
try { try {
res.setContentType("image/jpeg"); res.setContentType("image/jpeg");
scaleImage(req, res, file, req.getParameter("size")); FileEntry e = (FileEntry)DirectoryEntry.getEntry(file);
scaleImage(req, res, file, e.getThumbnail(), req.getParameter("size"));
} catch (Exception e) { } catch (Exception e) {
throw new RuntimeException("sadness", e); throw new RuntimeException("sadness", e);
} }
} else if ("photo".equals(page)) { return;
try { }
Entry e = dao.readFile(file); res.setStatus(HttpServletResponse.SC_NOT_FOUND);
req.setAttribute("entry", e); }
req.setAttribute("prev", e.prev());
req.setAttribute("next", e.next()); void handlePhoto(HttpServletRequest req, HttpServletResponse res, FileEntry entry) {
RequestDispatcher rd = req.getRequestDispatcher("/WEB-INF/velocity/photo.vm"); try {
rd.forward(req, res); res.setContentType("text/html");
} catch (Exception e) { req.setAttribute("entry", entry);
throw new RuntimeException("sadness", e); RequestDispatcher rd = req.getRequestDispatcher("/WEB-INF/velocity/photo.vm");
} rd.forward(req, res);
} else { } catch (Exception ex) {
res.setStatus(HttpServletResponse.SC_NOT_FOUND); throw new RuntimeException("sadness", ex);
} }
} }
boolean etagMatches(HttpServletRequest req, HttpServletResponse res, Entry entry, String size) { void handleAlbum(HttpServletRequest req, HttpServletResponse res, DirectoryEntry entry) {
String reqEtag = req.getHeader("If-None-Match"); try {
String fileEtag = entry.getEtag() + "-" + size; res.setContentType("text/html");
req.setAttribute("entry", entry);
RequestDispatcher rd = req.getRequestDispatcher("/WEB-INF/velocity/photo.vm");
rd.forward(req, res);
} catch (Exception e) {
throw new RuntimeException("sadness", e);
}
}
boolean etagMatches(HttpServletRequest req, HttpServletResponse res, Thumbnail thumbnail, String size) {
String cacheControl = req.getHeader("Cache-Control");
if ("max-age=0".equals(cacheControl)) {
return false;
}
String reqEtag = req.getHeader("If-None-Match");
String fileEtag = thumbnail.getEtag() + "-" + size;
if (reqEtag != null) { if (reqEtag != null) {
return reqEtag.equals(fileEtag); return reqEtag.equals(fileEtag);
} }
@ -115,14 +141,13 @@ public class AlbumServlet
return false; return false;
} }
void scaleImage(HttpServletRequest req, HttpServletResponse res, File file, String size) throws IOException, JpegProcessingException, MetadataException, ParseException { void scaleImage(HttpServletRequest req, HttpServletResponse res, File file, Thumbnail thumbnail, String size) throws IOException {
Entry entry = dao.readFile(file); if (etagMatches(req, res, thumbnail, size)) {
if (etagMatches(req, res, entry, size)) {
res.setStatus(HttpServletResponse.SC_NOT_MODIFIED); res.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
return; return;
} }
Dimension orig = entry.getSize(); Dimension orig = thumbnail.getSize();
Dimension outd; Dimension outd;
if (size.endsWith("h")) { if (size.endsWith("h")) {
int height = Integer.parseInt(size.substring(0, size.length() - 1)); int height = Integer.parseInt(size.substring(0, size.length() - 1));
@ -152,7 +177,7 @@ public class AlbumServlet
// Recalculate scale after sub-sampling was applied // Recalculate scale after sub-sampling was applied
double scale; double scale;
AffineTransform xform; AffineTransform xform;
if (entry.getOrientation() == 6) { if (thumbnail.getOrientation() == 6) {
xform = AffineTransform.getTranslateInstance(outd.getWidth() / 2d, outd.getHeight() / 2d); xform = AffineTransform.getTranslateInstance(outd.getWidth() / 2d, outd.getHeight() / 2d);
xform.rotate(Math.PI / 2); xform.rotate(Math.PI / 2);
scale = (double)outd.getHeight() / img.getWidth(); scale = (double)outd.getHeight() / img.getWidth();
@ -176,11 +201,44 @@ public class AlbumServlet
} }
Entry resolveEntry(String pathInfo) {
if (pathInfo == null || "/".equals(pathInfo)) return resolve(base);
return resolve(new File(base, pathInfo));
}
Entry resolve(File file) {
if (base.equals(file.getAbsoluteFile())) {
return new DirectoryEntry(null, file);
} else {
return ((DirectoryEntry)resolve(file.getParentFile())).get(file);
}
}
@Override @Override
public String getServletInfo() { public String getServletInfo() {
return "Display of org.forkalsrud.album"; return "Display of org.forkalsrud.album";
} }
public class Mapper {
public String map(File file) {
StringBuilder buf = new StringBuilder();
return appendFile(buf, file).toString();
}
StringBuilder appendFile(StringBuilder buf, File file) {
if (base.equals(file.getAbsoluteFile())) {
return buf.append("/").append(base.getName());
} else {
return appendFile(buf, file.getParentFile()).append('/').append(file.getName());
}
}
}
} }
// eof // eof

View file

@ -1,62 +0,0 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<meta name="viewport" content="width = 600" />
<title>$directory</title>
<style type="text/css">
body {
font-size: 11px;
font-family: "Lucida Grande", Arial, sans-serif;
color: #6d6d6d;
background: #acc95f;
margin: 10px auto;
text-align: center;
}
h1 {
text-align: left;
}
a:link, a:visited {
text-decoration: none;
color: #4c4c4c;
font-weight: 700;
}
a:hover, a:focus {
text-decoration: underline;
color: #6a6a6a;
}
div.home_entry_table {
float: left;
height: 200px;
}
img {
padding: 5px;
}
.nav {
border: 0 none;
padding: 0px;
vertical-align: middle;
}
</style>
</head>
<body>
<h1>#if($prev && $prev.isFile())<a href="${prev.name}.photo"><img class="nav" src="${assets}/left.png"/></a>#else<img class="nav" src="${assets}/left-inactive.png"/>#end#if($parent)<a href="$parent"><img class="nav" src="${assets}/up.png"/></a>#else<img class="nav" src="${assets}/up-inactive.png"/>#end#if($next && $next.isFile())<a href="${next.name}.photo"><img class="nav" src="${assets}/right.png"/></a>#else<img class="nav" src="${assets}/right-inactive.png"/>#end$directory</h1>
<hr/>
#set($thmb = 150)
#foreach($entry in $entries)
#set($dim = $entry.size.scaled($thmb))
<div class="home_entry_table">
<span class="name">$entry.name</span><br/>
#if($entry.isFile())
<a href="${entry.name}.photo"><img src="$entry.path?size=$thmb" border="0" width="$dim.width" height="$dim.height"/></a><br/>
#else
<a href="${entry.name}/"><img src="$entry.path?size=$thmb" border="0" width="$dim.width" height="$dim.height"/></a><br/>
#end
<span class="caption">$!entry.caption</span>
</div>
#end
</body>
</html>

View file

@ -14,9 +14,9 @@
margin: 10px auto; margin: 10px auto;
text-align: center; text-align: center;
} }
h1 { h1 {
text-align: left; text-align: left;
} }
a:link, a:visited { a:link, a:visited {
text-decoration: none; text-decoration: none;
color: #4c4c4c; color: #4c4c4c;
@ -27,8 +27,12 @@
color: #6a6a6a; color: #6a6a6a;
} }
div.photo { div.photo {
align: center; align: center;
height: 200px; height: 200px;
}
div.grid {
float: left;
height: 150px;
} }
img { img {
padding: 5px; padding: 5px;
@ -36,21 +40,37 @@
.nav { .nav {
border: 0 none; border: 0 none;
padding: 0px; padding: 0px;
vertical-align: middle; vertical-align: middle;
} }
</style> </style>
</head> </head>
<body> <body>
<h1>#if($prev && $prev.isFile())<a href="${prev.name}.photo"><img class="nav" src="${assets}/left.png"/></a>#else<img class="nav" src="${assets}/left-inactive.png"/>#end#if($parent)<a href="$parent"><img class="nav" src="${assets}/up.png"/></a>#else<img class="nav" src="${assets}/up-inactive.png"/>#end#if($next && $next.isFile())<a href="${next.name}.photo"><img class="nav" src="${assets}/right.png"/></a>#else<img class="nav" src="${assets}/right-inactive.png"/>#end$entry.name</h1> #macro(navlink $entry)${base}$mapper.map(${entry.getPath()}).#if($entry.isFile())photo#{else}album#end#end
#macro(navbutton $entry $direction)#if($entry)<a href="#navlink($entry)"><img class="nav" width="26" height="32" src="${assets}/${direction}.png"/></a>#else<img class="nav" width="26" height="32" src="${assets}/${direction}-inactive.png"/>#end#end
<h1>#navbutton(${entry.prev()} "left")#navbutton(${entry.parent()} "up")#navbutton(${entry.next()} "right") $entry.name</h1>
<hr/> <hr/>
#if($entry.isFile())
#set($thmb = 480) #set($thmb = 480)
#set($dim = $entry.size.scaled($thmb)) #set($dim = $entry.thumbnail.size.scaled($thmb))
#set($thpath = $mapper.map(${entry.thumbnail.getPath()}))
#set($enpath = $mapper.map(${entry.getPath()}))
<div class="photo"> <div class="photo">
<span class="name"></span><br/> <a href="${base}${enpath}"><img src="${base}${thpath}?size=$thmb" border="0" width="$dim.width" height="$dim.height"/></a><br/>
<a href="$entry.name"><img src="$entry.name?size=$thmb" border="0" width="$dim.width" height="$dim.height"/></a><br/>
<span class="caption">$!entry.caption</span> <span class="caption">$!entry.caption</span>
</div> </div>
#else
#set($thmb = 150)
#foreach($en in $entry.getContents())
#set($dim = $en.thumbnail.size.scaled($thmb))
#set($thpath = $mapper.map(${en.thumbnail.getPath()}))
<div class="grid">
<span class="name">$en.name</span><br/>
<a href="#navlink($en)"><img src="${base}${thpath}?size=${thmb}" border="0" width="${dim.width}" height="${dim.height}"/></a><br/>
<span class="caption">$!en.caption</span>
</div>
#end
#end
</body> </body>
</html> </html>

View file

@ -15,6 +15,10 @@
<servlet> <servlet>
<servlet-name>album</servlet-name> <servlet-name>album</servlet-name>
<servlet-class>org.forkalsrud.album.web.AlbumServlet</servlet-class> <servlet-class>org.forkalsrud.album.web.AlbumServlet</servlet-class>
<init-param>
<param-name>base</param-name>
<param-value>photos</param-value>
</init-param>
</servlet> </servlet>
<servlet> <servlet>
@ -29,7 +33,7 @@
<servlet-mapping> <servlet-mapping>
<servlet-name>album</servlet-name> <servlet-name>album</servlet-name>
<url-pattern>/photos/*</url-pattern> <url-pattern>/album/*</url-pattern>
</servlet-mapping> </servlet-mapping>
</web-app> </web-app>