2 * Copyright 2005 the original author or authors.
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
16 package org.wamblee.photos.model.filesystem;
18 import java.awt.image.BufferedImage;
19 import java.io.BufferedOutputStream;
21 import java.io.FileOutputStream;
22 import java.io.IOException;
23 import java.io.InputStream;
24 import java.io.OutputStream;
25 import java.util.ArrayList;
26 import java.util.Arrays;
27 import java.util.List;
29 import org.apache.commons.logging.Log;
30 import org.apache.commons.logging.LogFactory;
31 import org.wamblee.cache.Cache;
32 import org.wamblee.cache.CachedObject;
33 import org.wamblee.general.Pair;
34 import org.wamblee.photos.model.Album;
35 import org.wamblee.photos.model.Path;
36 import org.wamblee.photos.model.Photo;
37 import org.wamblee.photos.model.PhotoEntry;
38 import org.wamblee.photos.utils.JpegUtils;
41 * Represents a photo album stored in a directory structure on the file system.
43 public class FileSystemAlbum implements Album {
45 private static final Log LOG = LogFactory.getLog(FileSystemAlbum.class);
48 * Subdirectory where the thumbnails are stored.
50 public static final String THUMBNAILS_DIR = "thumbnails";
53 * Subdirectory where the photos are stored in their full size.
55 public static final String PHOTOS_DIR = "fotos";
58 * Extension used for JPEG pictures.
60 private static final String JPG_EXTENSION = ".jpg";
63 * Last part of the file name that a thumbnail must end with.
65 private static final String THUMBNAIL_ENDING = "_thumb.jpg";
67 private static final int THUMBNAIL_WIDTH = 100;
69 private static final int THUMBNAIL_HEIGHT = 100;
71 private static final int JPG_QUALITY = 75;
74 * Array of photo entries.
76 private CachedObject<String, ArrayList<PhotoEntry>> _entries;
79 * Storage directory for this album.
84 * Relative path with respect to the root album.
88 private class CreateEntryCallback implements EntryFoundCallback {
89 public boolean photoFound(List<PhotoEntry> aEntries, File aThumbnail,
90 File aPhoto, String aPath) {
91 PhotoEntry entry = new FileSystemPhoto(aThumbnail, aPhoto, aPath);
96 public boolean albumFound(List<PhotoEntry> aEntries, File aAlbum,
97 String aPath) throws IOException {
98 PhotoEntry entry = new FileSystemAlbum(aAlbum, aPath,
109 * Directory where the album is located.
111 * Path that this album represents.
114 * @throws IOException
116 public FileSystemAlbum(File aDir, String aPath,
117 Cache<String, ArrayList<PhotoEntry>> aCache) throws IOException {
118 if (!aDir.isDirectory()) {
119 throw new IOException("Directory '" + aDir.getPath() +
120 "' does not exist.");
124 _entries = new CachedObject<String, ArrayList<PhotoEntry>>(aCache,
126 new CachedObject.Computation<String, ArrayList<PhotoEntry>>() {
127 public ArrayList<PhotoEntry> getObject(String aObjectKey) {
128 return FileSystemAlbum.this.compute();
135 * Computes the photo entries for this album based on the file system.
137 * @return List of photo entries.
139 private ArrayList<PhotoEntry> compute() {
140 ArrayList<PhotoEntry> result = new ArrayList<PhotoEntry>();
142 LOG.info("Initializing album for directory " + _dir);
143 traverse(getPath(), result, _dir, new CreateEntryCallback());
144 } catch (IOException e) {
145 LOG.fatal("IOException occurred: " + e.getMessage(), e);
151 * Initializes the album.
156 * Photo entries for the album.
158 * Directory where the album is stored.
159 * @throws IOException
161 static boolean traverse(String aPath, List<PhotoEntry> aEntries, File aDir,
162 EntryFoundCallback aCallback) throws IOException {
163 File[] files = listFilesInDir(aDir);
164 for (int i = 0; i < files.length; i++) {
165 File file = files[i];
166 if (file.isDirectory()) {
168 if (file.getName().equals(THUMBNAILS_DIR)) {
169 if (!traverseThumbnails(aPath, aEntries, aDir, aCallback,
173 } else if (file.getName().equals(PHOTOS_DIR)) {
174 // Skip the photos directory.
177 String newPath = aPath + "/" + file.getName();
178 newPath = newPath.replaceAll("//", "/");
179 if (!aCallback.albumFound(aEntries, file, newPath)) {
180 return false; // finished.
189 * Traverse the thumnails directory.
192 * Path of the photo album.
194 * Entries of the album.
196 * Directory of the album.
198 * Callback to call when a thumbnail has been found.
200 * Directory of the photo album.
202 private static boolean traverseThumbnails(String aPath,
203 List<PhotoEntry> aEntries, File aDir, EntryFoundCallback aCallback,
205 // Go inside the thumbnails directory to scan
206 // for available photos.
208 // Check if the corresponding photos directory
210 File photosDir = new File(aDir, PHOTOS_DIR);
211 if (photosDir.isDirectory()) {
212 if (!buildAlbum(aPath, aEntries, aFile, photosDir, aCallback)) {
216 LOG.info("Thumbnails director " + aFile.getPath() +
217 " exists but corresponding photo directory " +
218 photosDir.getPath() + " not found");
224 * Builds up the photo album for a given thumbnails and photo directory.
229 * Photo entries of the album.
230 * @param aThumbnailsDir
231 * Directory containing thumbnail pictures.
233 * Directory containing full size photos.
235 private static boolean buildAlbum(String aPath, List<PhotoEntry> aEntries,
236 File aThumbnailsDir, File aPhotosDir, EntryFoundCallback aCallback) {
238 File[] thumbnails = listFilesInDir(aThumbnailsDir);
239 for (int i = 0; i < thumbnails.length; i++) {
240 File thumbnail = thumbnails[i];
241 if (!isThumbNail(thumbnail)) {
242 LOG.info("Skipping " + thumbnail.getPath() +
243 " because it is not a thumbnail file.");
246 File photo = new File(aPhotosDir, photoName(thumbnail.getName()));
247 if (!photo.isFile()) {
248 LOG.info("Photo file " + photo.getPath() + " for thumbnail " +
249 thumbnail.getPath() + " does not exist.");
252 String photoPath = photo.getName();
253 photoPath = photoPath.substring(0, photoPath.length() -
254 JPG_EXTENSION.length());
255 photoPath = aPath + "/" + photoPath;
256 photoPath = photoPath.replaceAll("//", "/");
257 if (!aCallback.photoFound(aEntries, thumbnail, photo, photoPath)) {
261 return true; // continue.
267 * @see org.wamblee.photos.database.PhotoEntry#getPath()
269 public String getPath() {
274 * Checks if the file represents a thumbnail.
278 * @return True iff the file is a thumbnail.
280 private static boolean isThumbNail(File aFile) {
281 return aFile.getName().endsWith(THUMBNAIL_ENDING);
285 * Constructs the photo name based on the thumbnail name.
287 * @param aThumbnailName
289 * @return Photo name.
291 private static String photoName(String aThumbnailName) {
292 return aThumbnailName.substring(0, aThumbnailName.length() -
293 THUMBNAIL_ENDING.length()) +
300 * @see org.wamblee.photos.database.Album#getEntry(java.lang.String)
302 public PhotoEntry getEntry(String aPath) {
303 if (!aPath.startsWith("/")) {
304 throw new IllegalArgumentException(
305 "Path must start with / character");
307 if (aPath.equals("/")) {
310 String[] fields = aPath.substring(1).split("/");
311 return getEntry(fields, 0);
315 * Gets the entry for the given path.
317 public PhotoEntry getEntry(Path aPath) {
318 return getEntry(aPath.toString());
322 * Gets the entry at a given path.
325 * Array of components of the path.
327 * Current level in the path.
330 private PhotoEntry getEntry(String[] aPath, int aLevel) {
331 String id = aPath[aLevel];
332 PhotoEntry entry = find(id);
336 if (aLevel < aPath.length - 1) {
337 if (!(entry instanceof Album)) {
340 return ((FileSystemAlbum) entry).getEntry(aPath, aLevel + 1);
342 return entry; // end of the path.
347 * Finds an entry in the album with the given id.
351 * @return Photo entry.
353 private PhotoEntry find(String aId) {
354 return findInMap(aId).getFirst();
358 * Finds a photo entry in the map.
361 * Id of the photo entry.
362 * @return Pair of a photo entry and the index of the item.
364 private Pair<PhotoEntry, Integer> findInMap(String aId) {
365 List<PhotoEntry> entries = _entries.get();
366 for (int i = 0; i < entries.size(); i++) {
367 PhotoEntry entry = entries.get(i);
368 if (entry.getId().equals(aId)) {
369 return new Pair<PhotoEntry, Integer>(entry, i);
372 return new Pair<PhotoEntry, Integer>(null, -1);
378 * @see org.wamblee.photos.database.Album#getEntry(int)
380 public PhotoEntry getEntry(int aIndex) {
381 return _entries.get().get(aIndex);
387 * @see org.wamblee.photos.database.Album#size()
390 return _entries.get().size();
396 * @see org.wamblee.photos.database.PhotoEntry#getId()
398 public String getId() {
399 return _dir.getName();
402 public String toString() {
407 * Prints the album with a given indentation.
410 * Indentation string.
411 * @return String representation of the album.
413 private String print(String aIndent) {
414 String res = aIndent + "ALBUM: " + getId() + "\n";
416 for (int i = 0; i < size(); i++) {
417 PhotoEntry entry = getEntry(i);
418 if (entry instanceof FileSystemAlbum) {
419 res += ((FileSystemAlbum) entry).print(aIndent);
421 res += ((FileSystemPhoto) entry).print(aIndent);
431 * @see org.wamblee.photos.database.Album#addImage(java.lang.String,
434 public void addImage(String aId, InputStream aImageStream)
436 File photoDir = getPhotoDir();
437 File thumbnailDir = getThumbnailDir();
439 checkPhotoDirs(aId, photoDir, thumbnailDir);
443 image = JpegUtils.loadJpegImage(aImageStream);
444 } catch (InterruptedException e) {
445 throw new IOException("Loading image interruptedS: " +
448 BufferedImage thumbnailImage = JpegUtils.scaleImage(THUMBNAIL_WIDTH,
449 THUMBNAIL_HEIGHT, image);
451 File thumbnail = new File(thumbnailDir, aId + THUMBNAIL_ENDING);
452 File photo = new File(photoDir, aId + JPG_EXTENSION);
454 writeImage(photo, image);
455 writeImage(thumbnail, thumbnailImage);
457 Photo photoEntry = new FileSystemPhoto(thumbnail, photo, (getPath()
458 .equals("/") ? "" : getPath()) + "/" + aId);
459 addEntry(photoEntry);
463 * Checks if a given entry can be added to the album.
466 * Id of the new entry.
469 * @param aThumbnailDir
470 * Thumbnail directory.
471 * @throws IOException
473 private void checkPhotoDirs(String aId, File aPhotoDir, File aThumbnailDir)
475 // Create directories if they do not already exist.
477 aThumbnailDir.mkdir();
479 if (!aPhotoDir.isDirectory() || !aThumbnailDir.isDirectory()) {
480 throw new IOException("Album " + getId() +
481 " does not accept new images.");
484 if (getEntry("/" + aId) != null) {
485 throw new IOException("An entry with the name '" + aId +
486 "' already exists in the album " + getPath());
491 * Gets the thumbnail directory for the album.
495 private File getThumbnailDir() {
496 File thumbnailDir = new File(_dir, THUMBNAILS_DIR);
501 * Gets the photo directory for the album.
503 * @return Photo directory
505 private File getPhotoDir() {
506 File photoDir = new File(_dir, PHOTOS_DIR);
511 * Writes an image to a file.
517 * @throws IOException
519 private static void writeImage(File aFile, BufferedImage aImage)
521 OutputStream output = new BufferedOutputStream(new FileOutputStream(
523 JpegUtils.writeJpegImage(output, JPG_QUALITY, aImage);
530 * @see org.wamblee.photos.database.Album#addAlbum(java.lang.String)
532 public void addAlbum(String aId) throws IOException {
533 PhotoEntry entry = find(aId);
535 throw new IOException("Entry already exists in album " + getId() +
538 // Entry not yet found. Try to create it.
539 File albumDir = new File(_dir, aId);
540 if (!albumDir.mkdir()) {
541 throw new IOException("Could not create album: " + aId);
543 File photosDir = new File(albumDir, PHOTOS_DIR);
544 if (!photosDir.mkdir()) {
545 throw new IOException("Could not create photo storage dir: " +
546 photosDir.getPath());
548 File thumbnailsDir = new File(albumDir, THUMBNAILS_DIR);
549 if (!thumbnailsDir.mkdir()) {
550 throw new IOException("Coul dnot create thumbnails storage dir: " +
551 thumbnailsDir.getPath());
553 String newPath = _path + "/" + aId;
554 newPath = newPath.replaceAll("//", "/");
555 FileSystemAlbum album = new FileSystemAlbum(albumDir, newPath,
556 _entries.getCache());
563 * @see org.wamblee.photos.database.Album#removeAlbum(java.lang.String)
565 public void removeEntry(String aId) throws IOException {
566 PhotoEntry entry = find(aId);
568 throw new IOException("Entry " + aId + " not found.");
570 if (entry instanceof FileSystemAlbum) {
572 removeAlbum((FileSystemAlbum) entry);
580 * Removes a photo entry.
583 * Photo entry to remove.
584 * @throws IOException
586 private void removePhoto(PhotoEntry aEntry) throws IOException {
587 File photo = new File(_dir, PHOTOS_DIR);
588 photo = new File(photo, aEntry.getId() + JPG_EXTENSION);
589 File thumbnail = new File(_dir, THUMBNAILS_DIR);
590 thumbnail = new File(thumbnail, aEntry.getId() + THUMBNAIL_ENDING);
591 if (!photo.isFile()) {
592 throw new IOException("Photo file not found: " + photo.getPath());
594 if (!thumbnail.isFile()) {
595 throw new IOException("Thumbnail file not found: " +
596 thumbnail.getPath());
598 if (!thumbnail.delete()) {
599 throw new IOException("Could not delete thumbnail file: " +
600 thumbnail.getPath());
602 _entries.get().remove(aEntry);
603 if (!photo.delete()) {
604 throw new IOException("Could not delete photo file: " +
610 * Removes the album entry from this album.
614 * @throws IOException
616 private void removeAlbum(FileSystemAlbum aAlbum) throws IOException {
617 if (aAlbum.size() > 0) {
618 throw new IOException("Album " + aAlbum.getId() + " not empty.");
620 // album is empty, safe to remove.
621 File photosDir = new File(aAlbum._dir, PHOTOS_DIR);
622 File thumbnailsDir = new File(aAlbum._dir, THUMBNAILS_DIR);
623 if (!photosDir.delete() || !thumbnailsDir.delete() ||
624 !aAlbum._dir.delete()) {
625 throw new IOException("Could not delete directories for album:" +
628 _entries.get().remove(aAlbum);
637 private void addEntry(PhotoEntry aEntry) {
639 List<PhotoEntry> entries = _entries.get();
640 int nentries = entries.size();
641 while (i < nentries && aEntry.compareTo(entries.get(i)) > 0) {
644 entries.add(i, aEntry);
648 * @see java.lang.Object#equals(java.lang.Object)
651 public boolean equals(Object obj) {
652 if (!(obj instanceof Album)) {
655 return toString().equals(obj.toString());
659 * @see java.lang.Object#hashCode()
662 public int hashCode() {
663 return toString().hashCode();
669 * @see java.lang.Comparable#compareTo(java.lang.Object)
671 public int compareTo(PhotoEntry aEntry) {
672 return getId().compareTo(((PhotoEntry) aEntry).getId());
676 * Returns and alphabetically sorted list of files.
679 * @return Sorted list of files.
681 private static File[] listFilesInDir(File aDir) {
682 File[] lResult = aDir.listFiles();
683 Arrays.sort(lResult);
689 * @see org.wamblee.photos.model.Album#findPhotoBefore(java.lang.String)
691 public Photo findPhotoBefore(String aId) {
692 Pair<PhotoEntry, Integer> entry = findInMap(aId);
693 if (entry.getFirst() == null) {
694 throw new IllegalArgumentException("Entry with id '" + aId +
695 "' not found in album '" + getPath() + "'");
698 int i = entry.getSecond() - 1;
699 List<PhotoEntry> entries = _entries.get();
700 while (i >= 0 && i < entries.size() &&
701 !(entries.get(i) instanceof Photo)) {
705 return (Photo) entries.get(i);
712 * @see org.wamblee.photos.model.Album#findPhotoAfter(java.lang.String)
714 public Photo findPhotoAfter(String aId) {
715 Pair<PhotoEntry, Integer> entry = findInMap(aId);
716 if (entry.getFirst() == null) {
717 throw new IllegalArgumentException("Entry with id '" + aId +
718 "' not found in album '" + getPath() + "'");
720 int i = entry.getSecond() + 1;
721 List<PhotoEntry> entries = _entries.get();
722 while (i >= 0 && i < entries.size() &&
723 !(entries.get(i) instanceof Photo)) {
726 if (i < entries.size()) {
727 return (Photo) entries.get(i);