1 /* Copyright (C) 2005-2011 Fabio Riccardi */
2 
3 package com.lightcrafts.ui.browser.model;
4 
5 import com.lightcrafts.image.BadImageFileException;
6 import com.lightcrafts.image.ImageInfo;
7 import com.lightcrafts.image.UnknownImageTypeException;
8 import static com.lightcrafts.image.metadata.CoreTags.*;
9 import com.lightcrafts.image.metadata.*;
10 import static com.lightcrafts.image.metadata.TIFFTags.TIFF_XMP_PACKET;
11 import com.lightcrafts.image.metadata.values.ImageMetaValue;
12 import static com.lightcrafts.ui.browser.model.Locale.LOCALE;
13 import com.lightcrafts.utils.filecache.FileCache;
14 
15 import java.awt.*;
16 import java.awt.image.RenderedImage;
17 import java.io.*;
18 import java.lang.ref.SoftReference;
19 import java.util.Iterator;
20 import java.util.LinkedList;
21 
22 /**
23  * A holder for all data that are derived from an image for browser purposes,
24  * such as metadata, thumbnails, and a cache for ImageTasks.
25  */
26 public class ImageDatum {
27 
28     // The File defining the image
29     private File file;
30 
31     // The file's modification time when metadata were last cached
32     private long fileCacheTime;
33 
34     // The XMP File updating the image metadata
35     private File xmpFile;
36 
37     // The XMP file's modification time when metadata were last cached
38     private long xmpFileCacheTime;
39 
40     // Selected metadata, updated asynchronously
41     private ImageMetadata meta;
42 
43     // Thumbnail image, updated asynchronously
44     private SoftReference<RenderedImage> image;
45 
46     // This ImageDatum's LZN encoding info, computed lazily
47     private ImageDatumType type;
48 
49     // This flag indicates whether the ImageTask needs to run
50     private boolean isDirty;
51 
52     // The current runnable for background work
53     private ImageTask task;
54 
55     // The Thread container for the ImageTask
56     private ImageTaskQueue queue;
57 
58     // The cache used by the ImageTask
59     private FileCache cache;
60 
61     // The size for thumbnails, given to ImageTasks
62     private int size;
63 
64     // Observers for asynchronous replies to getImage() and getMetadata()
65     private LinkedList<ImageDatumObserver> observers;
66 
67     // PreviewUpdaters we are currently maintaining
68     private LinkedList<PreviewUpdater> previews;
69 
70     // ImageDatums can be logically associated into groups
71     private ImageGroup group;
72 
73     private boolean badFile = false;
74 
ImageDatum( File file, int size, ImageTaskQueue queue, FileCache cache )75     public ImageDatum(
76         File file, int size, ImageTaskQueue queue, FileCache cache
77     ) {
78         this.file = file;
79         this.size = size;
80         this.queue = queue;
81         this.cache = cache;
82 
83         markDirty();
84 
85         observers = new LinkedList<ImageDatumObserver>();
86         previews = new LinkedList<PreviewUpdater>();
87 
88         group = new ImageGroup(this);
89     }
90 
91     /**
92      * Get the image File backing the data in this ImageDatum.  This File is
93      * an immutable property of its ImageDatum.
94      */
getFile()95     public File getFile() {
96         return file;
97     }
98 
99     /**
100      * Get the XMP file which extends the metadata in this ImageDatum.  This
101      * may be null, if there was an error reading image file metadata.
102      */
getXmpFile()103     public File getXmpFile() {
104         return xmpFile;
105     }
106 
isBadFile()107     public boolean isBadFile() {
108         return badFile;
109     }
110 
setBadFile()111     void setBadFile() {
112         badFile = true;
113     }
114 
115     /**
116      * Rotate the image counterclockwise by 90 degrees, unless this image
117      * has LZN data, in which case throw an IOException.
118      */
rotateLeft()119     public void rotateLeft()
120         throws IOException, BadImageFileException, UnknownImageTypeException
121     {
122         if ((type == null) || type.hasLznData()) {
123             throw new IOException(LOCALE.get("CantRotateLzn"));
124         }
125         ImageInfo info = ImageInfo.getInstanceFor(file);
126         ImageMetadata meta = info.getMetadata();
127         meta.setOrientation(meta.getOrientation().get90CCW());
128         commitRotate(info, 3);
129     }
130 
131     /**
132      * Rotate the image clockwise by 90 degrees, unless this image has LZN
133      * data, in which case throw an IOException.
134      */
rotateRight()135     public void rotateRight()
136         throws IOException, BadImageFileException, UnknownImageTypeException
137     {
138         if ((type == null) || type.hasLznData()) {
139             throw new IOException(LOCALE.get("CantRotateLzn"));
140         }
141         ImageInfo info = ImageInfo.getInstanceFor(file);
142         ImageMetadata meta = info.getMetadata();
143         meta.setOrientation(meta.getOrientation().get90CW());
144         commitRotate(info, 1);
145     }
146 
147     /**
148      * Set the rating number on this image, 1 to 5.
149      */
setRating(int rating)150     public void setRating(int rating)
151         throws IOException, BadImageFileException, UnknownImageTypeException
152     {
153         ImageInfo info = ImageInfo.getInstanceFor(file);
154         ImageMetadata meta = info.getMetadata();
155         meta.setRating(rating);
156         writeToXmp(info);
157         rateInMemory(rating);
158     }
159 
160     /**
161      * Clear the rating number on this image.
162      */
clearRating()163     public void clearRating()
164         throws IOException, BadImageFileException, UnknownImageTypeException
165     {
166         ImageInfo info = ImageInfo.getInstanceFor(file);
167         ImageMetadata meta = info.getMetadata();
168         meta.clearRating();
169         writeToXmp(info);
170         rateInMemory(0);
171     }
172 
173     /**
174      * Discard all computed results for the File and enqueue a new task
175      * to recompute metadata and thumbnail data.
176      */
refresh(boolean useImageCache)177     public void refresh(boolean useImageCache) {
178         if ((! useImageCache) && (meta != null)) {
179             // Clear the cached preview, but only if the preview is older than
180             // ten seconds.  (Sometimes, a preview is deliberately cached
181             // right before an image file is modified.)
182             long now = System.currentTimeMillis();
183             long mod = PreviewUpdater.getCachedPreviewTime(meta, cache);
184             if (now - mod > 10000) {
185                 clearPreview();
186             }
187         }
188         meta = null;
189         type = null;
190         image = null;
191         clearMetadataCache();
192         restartTask(useImageCache);
193     }
194 
195     /**
196      * Remove this ImageDatum's ImageTask from the ImageTaskQueue, if it is
197      * not already running.
198      */
cancel()199     public void cancel() {
200         if (task != null) {
201             queue.removeTask(task);
202         }
203     }
204 
205     // Called from ImageList.
setSize(int size)206     void setSize(int size) {
207         if ((size != this.size) && (size > 0)) {
208             this.size = size;
209             restartTask(true);
210         }
211     }
212 
213     // Synchronized because the ImageTask modifies the image.
getImage(ImageDatumObserver observer)214     public synchronized RenderedImage getImage(ImageDatumObserver observer) {
215         if ((observer != null) && ! observers.contains(observer)) {
216             observers.add(observer);
217         }
218         RenderedImage image = (this.image != null) ? this.image.get() : null;
219 
220         if (! badFile) {
221             if ((task == null) || (image == null) || isDirty) {
222                 restartTask(true);  // queue slow thumbnailing things
223             }
224             queue.raiseTask(task);
225         }
226         if (image != null) {
227             return image;
228         }
229         return EggImage.getEggImage(size);
230     }
231 
232     // Synchronized because the ImageTask modifies the taskCache and the image.
getPreview( PreviewUpdater.Provider provider )233     public synchronized PreviewUpdater getPreview(
234         PreviewUpdater.Provider provider    // null is OK
235     ) {
236         // First, find the best preview currently available:
237         RenderedImage preview = getImage(null);
238 
239         // Don't assume that our metadata member "meta" is non-null--
240         // this method may get called after a refresh and before our task runs.
241         PreviewUpdater updater =
242             new PreviewUpdater(cache, preview, getMetadata(true), provider);
243 
244         previews.add(updater);
245 
246         return updater;
247     }
248 
disposePreviews()249     public void disposePreviews() {
250         for (PreviewUpdater preview : previews) {
251             preview.dispose();
252         }
253         previews.clear();
254     }
255 
256     // Called from ImageTask when a thumbnail is ready
setImage(RenderedImage image)257     synchronized void setImage(RenderedImage image) {
258         this.image = new SoftReference<RenderedImage>(image);
259     }
260 
getFileCacheTime()261     long getFileCacheTime() {
262         return fileCacheTime;
263     }
264 
getXmpFileCacheTime()265     long getXmpFileCacheTime() {
266         return xmpFileCacheTime;
267     }
268 
269     // Synchronized because the poller and the painting read it, and the
270     // task writes it.
getMetadata(boolean useCache)271     public synchronized ImageMetadata getMetadata(boolean useCache) {
272         // Backwards compatibility:
273         if (readRotateCache() != 0) {
274             migrateRotateCacheToXmp();
275             useCache = false;
276         }
277         if ((meta == null) && useCache) {
278             readMetadataCache();
279         }
280         if (meta != null) {
281             return meta;
282         }
283         File file = getFile();
284         ImageInfo info = ImageInfo.getInstanceFor(file);
285         try {
286             ImageMetadata meta = info.getMetadata();
287             try {
288                 xmpFile = new File(info.getXMPFilename());
289             }
290             catch (Throwable e) {
291                 badFile = true;
292                 logMetadataError(e);
293                 xmpFile = null;
294             }
295             // Limit the metadata to data used for sorting and display.
296             updateMetadata(meta);
297             // Note file modification times, used for metadata cache keys.
298             updateFileTimes();
299             // Write limited, timestamped metadata to the cache.
300             writeMetadataCache();
301         }
302         catch (Throwable e) {
303             badFile = true;
304             logMetadataError(e);
305             meta = EggImage.getEggMetadata(file);
306         }
307         return meta;
308     }
309 
getGroup()310     public ImageGroup getGroup() {
311         return group;
312     }
313 
setGroup(ImageGroup group)314     public void setGroup(ImageGroup group) {
315         this.group.removeImageDatum(this);
316         this.group = group;
317         group.addImageDatum(this);
318     }
319 
newGroup()320     public ImageGroup newGroup() {
321         group.removeImageDatum(this);
322         group = new ImageGroup(this);
323         return group;
324     }
325 
getType()326     public ImageDatumType getType() {
327         if (type == null) {
328             type = ImageDatumType.getTypeOf(this);
329         }
330         return type;
331     }
332 
333     // Called before enqueueing the task, so in case it gets cancelled,
334     // we know to resume later on.
markDirty()335     void markDirty() {
336         isDirty = true;
337     }
338 
339     // Called from ImageTask, when there is no more work to do.
markClean()340     void markClean() {
341         isDirty = false;
342         updatePreviews();
343         EventQueue.invokeLater(
344             new Runnable() {
345                 public void run() {
346                     notifyImageObservers();
347                 }
348             }
349         );
350     }
351 
352     // Keep only the metadata fields used for sorting and display.
updateMetadata(ImageMetadata meta)353     private void updateMetadata(ImageMetadata meta) {
354         this.meta = new ImageMetadata();
355 
356         ImageMetadataDirectory core =
357             meta.getDirectoryFor(CoreDirectory.class, true);
358         ImageMetadataDirectory thisCore =
359             this.meta.getDirectoryFor(CoreDirectory.class, true);
360 
361         // Tags used for presentation:
362         int[] tags = new int[] {
363             CORE_FILE_NAME,
364             CORE_DIR_NAME,
365             CORE_IMAGE_ORIENTATION,
366             CORE_RATING
367         };
368         for (int tag : tags) {
369             ImageMetaValue value = core.getValue(tag);
370             if (value != null) {
371                 thisCore.putValue(tag, value);
372             }
373         }
374         // Tags used for sorting:
375         ImageDatumComparator[] comps = ImageDatumComparator.getAll();
376         for (ImageDatumComparator comp : comps) {
377             int tagId = comp.getTagId();
378             ImageMetaValue value = core.getValue(tagId);
379             if (value != null) {
380                 thisCore.putValue(tagId, value);
381             }
382         }
383         // One more tag, used to determine the ImageDatumType for TIFFs
384         ImageMetaValue xmpValue = meta.getValue(
385             TIFFDirectory.class, TIFF_XMP_PACKET
386         );
387         if (xmpValue != null) {
388             ImageMetadataDirectory thisTiff =
389                 this.meta.getDirectoryFor(TIFFDirectory.class, true);
390             thisTiff.putValue(TIFF_XMP_PACKET, xmpValue);
391         }
392     }
393 
394     // Perform the operations common to rotateLeft() and rotateRight():
395     // rotate the in-memory thumbnail; notify observers, so the display will
396     // update; and write the modified metadata to XMP.
commitRotate(ImageInfo info, int multiple)397     private synchronized void commitRotate(ImageInfo info, int multiple)
398         throws IOException, BadImageFileException, UnknownImageTypeException
399     {
400         // Update the in-memory image immediately, for interactive response.
401         rotateInMemory(multiple);
402         // This triggers a refresh through the file modification polling
403         try {
404             writeToXmp(info);
405         }
406         // If the XMP write didn't work out, we better undo the rotation:
407         catch (IOException e) {
408             rotateInMemory(- multiple);
409             throw e;
410         }
411         catch (BadImageFileException e) {
412             rotateInMemory(- multiple);
413             throw e;
414         }
415         catch (UnknownImageTypeException e) {
416             rotateInMemory(- multiple);
417             throw e;
418         }
419     }
420 
421     // Rotate the in-memory thumbnail image directly.  This is called from
422     // commitRotate() to update the painted image quickly, until the polling
423     // can catch up with an authoritative image.
rotateInMemory(int multiple)424     private void rotateInMemory(int multiple) {
425         RenderedImage image = (this.image != null) ? this.image.get() : null;
426         if (image != null) {
427             image = Thumbnailer.rotateNinetyTimes(image, multiple);
428             this.image = new SoftReference<RenderedImage>(image);
429             EventQueue.invokeLater(
430                 new Runnable() {
431                     public void run() {
432                         notifyImageObservers();
433                     }
434                 }
435             );
436         }
437     }
438 
439     // Set the in-memory rating value directly.  This is called from
440     // setRating() to update the painted image quickly, until the polling
441     // can catch up with the authoritative metadata.
rateInMemory(int rating)442     private void rateInMemory(int rating) {
443         if (meta != null) {
444             if (rating > 0) {
445                 meta.setRating(rating);
446             }
447             else {
448                 meta.clearRating();
449             }
450             EventQueue.invokeLater(
451                 new Runnable() {
452                     public void run() {
453                         notifyImageObservers();
454                     }
455                 }
456             );
457         }
458     }
459 
restartTask(boolean useCache)460     private void restartTask(boolean useCache) {
461         if (task != null) {
462             queue.removeTask(task);
463         }
464         task = new ImageTask(this, cache, size, useCache);
465         markDirty();
466         queue.addTask(task);
467     }
468 
updatePreviews()469     private synchronized void updatePreviews() {
470         // Push a rotation change out to all running PreviewUpdaters.
471         LinkedList<PreviewUpdater> newRefs = new LinkedList<PreviewUpdater>();
472         for (Iterator<PreviewUpdater> i=previews.iterator(); i.hasNext(); ) {
473             PreviewUpdater updater = i.next();
474             if (updater != null) {
475                 RenderedImage image =
476                     (this.image != null) ? this.image.get() : null;
477                 if (image != null) {
478                     i.remove();
479                     updater = new PreviewUpdater(updater, image, meta);
480                     newRefs.add(updater);
481                 }
482             }
483         }
484         previews.addAll(newRefs);
485     }
486 
notifyImageObservers()487     private void notifyImageObservers() {
488         for (ImageDatumObserver observer : observers) {
489             observer.imageChanged(this);
490         }
491     }
492 
493     // Detect legacy user-commanded orientation changes, for files that were
494     // oriented through the browser before XMP support.
readRotateCache()495     private int readRotateCache() {
496         String key = getRotateKey();
497         int rotate = 0;
498         if ((cache != null) && cache.contains(key)) {
499             try {
500                 InputStream in = cache.getStreamFor(key);
501                 try {
502                     rotate = in.read();
503                 }
504                 finally {
505                     in.close();
506                 }
507             }
508             catch (IOException e) {
509                 // rotate defaults to zero
510             }
511         }
512         return rotate;
513     }
514 
515     // Detect cached orientation changes and migrate them to XMP.  This is
516     // called from getMetadata(), and exists for backwards compatibility.
migrateRotateCacheToXmp()517     private void migrateRotateCacheToXmp() {
518         int rotate = readRotateCache();
519         if (rotate != 0) {
520             try {
521                 ImageInfo info = ImageInfo.getInstanceFor(file);
522                 ImageMetadata meta = info.getMetadata();
523                 ImageOrientation orient = meta.getOrientation();
524                 switch (rotate) {
525                     case 1:
526                         orient = orient.get90CW();
527                         break;
528                     case 2:
529                         orient = orient.get180();
530                         break;
531                     case 3:
532                         orient = orient.get90CCW();
533                 }
534                 meta.setOrientation(orient);
535                 // Don't let migration clobber a preexisting XMP file.
536                 File xmpFile = new File(info.getXMPFilename());
537                 if (! xmpFile.isFile()) {
538                     writeToXmp(info);
539                     System.out.println(
540                         "Migrated rotate cache to XMP for " +
541                         file.getAbsolutePath()
542                     );
543                 }
544                 else {
545                     System.out.println(
546                         "Rotate cache migration aborted for " +
547                         file.getAbsolutePath() +
548                         " (" + xmpFile.getAbsolutePath() + " already exists)"
549                     );
550                 }
551             }
552             catch (Throwable t) {
553                 // BadImageFileException, IOException, UnknownImageTypeException
554                 System.err.println(
555                     "Failed to migrate rotate cache to XMP for " +
556                     file.getAbsolutePath()
557                 );
558                 t.printStackTrace();
559             }
560             String key = getRotateKey();
561             try {
562                 cache.remove(key);
563                 System.out.println(
564                     "Cleared rotate cache to XMP for " + file.getAbsolutePath()
565                 );
566             }
567             catch (IOException e) {
568                 // Try again next time.
569                 System.err.println(
570                     "Failed to clear rotate cache for " + file.getAbsolutePath()
571                 );
572                 e.printStackTrace();
573             }
574         }
575     }
576 
writeMetadataCache()577     private void writeMetadataCache() {
578         if (cache == null) {
579             return;
580         }
581         String metaKey = getMetadataKey();
582         ObjectOutputStream out = null;
583         try {
584             out = new ObjectOutputStream(cache.putToStream(metaKey));
585             out.writeObject(meta);
586         }
587         catch (IOException e) {
588             // metadata will be reread next time
589             System.err.println("metadata cache error: " + e.getMessage());
590         }
591         finally {
592             if (out != null) {
593                 try {
594                     out.close();
595                 }
596                 catch (IOException e) {
597                     System.err.println(
598                         "metadata cache error: " + e.getMessage()
599                     );
600                 }
601             }
602         }
603         String fileTimeKey = getFileTimeCacheKey();
604         try {
605             out = new ObjectOutputStream(cache.putToStream(fileTimeKey));
606             out.writeObject(fileCacheTime);
607         }
608         catch (IOException e) {
609             System.err.println("file time cache error: " + e.getMessage());
610         }
611         finally {
612             if (out != null) {
613                 try {
614                     out.close();
615                 }
616                 catch (IOException e) {
617                     System.err.println(
618                         "file time cache error: " + e.getMessage()
619                     );
620                 }
621             }
622         }
623         if (xmpFile == null) {
624             return;
625         }
626         String xmpFileKey = getXmpKey();
627         try {
628             out = new ObjectOutputStream(cache.putToStream(xmpFileKey));
629             out.writeObject(xmpFile);
630         }
631         catch (IOException e) {
632             System.err.println("file time cache error: " + e.getMessage());
633         }
634         finally {
635             if (out != null) {
636                 try {
637                     out.close();
638                 }
639                 catch (IOException e) {
640                     System.err.println(
641                         "file time cache error: " + e.getMessage()
642                     );
643                 }
644             }
645         }
646         String xmpFileTimeKey = getXmpFileTimeCacheKey();
647         try {
648             out = new ObjectOutputStream(cache.putToStream(xmpFileTimeKey));
649             out.writeObject(xmpFileCacheTime);
650         }
651         catch (IOException e) {
652             System.err.println("XMP file time cache error: " + e.getMessage());
653         }
654         finally {
655             if (out != null) {
656                 try {
657                     out.close();
658                 }
659                 catch (IOException e) {
660                     System.err.println(
661                         "XMP file time cache error: " + e.getMessage()
662                     );
663                 }
664             }
665         }
666     }
667 
readMetadataCache()668     private void readMetadataCache() {
669         if (cache == null) {
670             return;
671         }
672         String fileTimeKey = getFileTimeCacheKey();
673         if (cache.contains(fileTimeKey)) {
674             ObjectInputStream oin = null;
675             try {
676                 InputStream in = cache.getStreamFor(fileTimeKey);
677                 if (in != null) {
678                     oin = new ObjectInputStream(in);
679                     fileCacheTime = (Long) oin.readObject();
680                 }
681             }
682             catch (IOException e) {
683                 fileCacheTime = 0;
684             }
685             catch (ClassNotFoundException e) {
686                 fileCacheTime = 0;
687             }
688             finally {
689                 if (oin != null) {
690                     try {
691                         oin.close();
692                     }
693                     catch (IOException e) {
694                         // ignore
695                     }
696                 }
697             }
698         }
699         String xmpFileKey = getXmpKey();
700         if (cache.contains(xmpFileKey)) {
701             ObjectInputStream oin = null;
702             try {
703                 InputStream in = cache.getStreamFor(xmpFileKey);
704                 if (in != null) {
705                     oin = new ObjectInputStream(in);
706                     xmpFile = (File) oin.readObject();
707                 }
708             }
709             catch (IOException e) {
710                 xmpFile = null;
711             }
712             catch (ClassNotFoundException e) {
713                 xmpFile = null;
714             }
715             finally {
716                 if (oin != null) {
717                     try {
718                         oin.close();
719                     }
720                     catch (IOException e) {
721                         // ignore
722                     }
723                 }
724             }
725         }
726         if (xmpFile != null) {
727             String xmpFileTimeKey = getXmpFileTimeCacheKey();
728             if (cache.contains(xmpFileTimeKey)) {
729                 ObjectInputStream oin = null;
730                 try {
731                     InputStream in = cache.getStreamFor(xmpFileTimeKey);
732                     if (in != null) {
733                         oin = new ObjectInputStream(in);
734                         xmpFileCacheTime = (Long) oin.readObject();
735                     }
736                 }
737                 catch (IOException e) {
738                     xmpFileCacheTime = 0;
739                 }
740                 catch (ClassNotFoundException e) {
741                     xmpFileCacheTime = 0;
742                 }
743                 finally {
744                     if (oin != null) {
745                         try {
746                             oin.close();
747                         }
748                         catch (IOException e) {
749                             // ignore
750                         }
751                     }
752                 }
753             }
754         }
755         else {
756             xmpFileCacheTime = 0;
757         }
758         String metaKey = getMetadataKey();
759         if (cache.contains(metaKey)) {
760             ObjectInputStream oin = null;
761             try {
762                 InputStream in = cache.getStreamFor(metaKey);
763                 if (in != null) {
764                     oin = new ObjectInputStream(in);
765                     meta = (ImageMetadata) oin.readObject();
766                 }
767             }
768             catch (IOException e) {
769                 // getMetadata() will fall back to parsing out metadata
770             }
771             catch (ClassNotFoundException e) {
772                 // getMetadata() will fall back to parsing out metadata
773             }
774             finally {
775                 if (oin != null) {
776                     try {
777                         oin.close();
778                     }
779                     catch (IOException e) {
780                         // ignore
781                     }
782                 }
783             }
784         }
785     }
786 
clearMetadataCache()787     private void clearMetadataCache() {
788         if (cache == null) {
789             return;
790         }
791         String key = getMetadataKey();
792         if (cache.contains(key)) {
793             try {
794                 cache.remove(key);
795             }
796             catch (IOException e) {
797                 System.err.println(
798                     "metadata cache clear error: " + e.getMessage()
799                 );
800             }
801         }
802         String fileTimeKey = getFileTimeCacheKey();
803         if (cache.contains(fileTimeKey)) {
804             try {
805                 cache.remove(fileTimeKey);
806             }
807             catch (IOException e) {
808                 System.err.println(
809                     "metadata cache clear error: " + e.getMessage()
810                 );
811             }
812         }
813         String xmpFileKey = getXmpKey();
814         if (cache.contains(xmpFileKey)) {
815             try {
816                 cache.remove(xmpFileKey);
817             }
818             catch (IOException e) {
819                 System.err.println(
820                     "metadata cache clear error: " + e.getMessage()
821                 );
822             }
823         }
824         if (xmpFile == null) {
825             return;
826         }
827         String xmpFileTimeKey = getXmpFileTimeCacheKey();
828         if (cache.contains(xmpFileTimeKey)) {
829             try {
830                 cache.remove(xmpFileTimeKey);
831             }
832             catch (IOException e) {
833                 System.err.println(
834                     "metadata cache clear error: " + e.getMessage()
835                 );
836             }
837         }
838     }
839 
840     // The cache key for the rotate value.
getRotateKey()841     private String getRotateKey() {
842         StringBuffer buffer = new StringBuffer();
843         buffer.append(file.getAbsolutePath());
844         buffer.append("_rotate");
845         return buffer.toString();
846     }
847 
848     // The cache key for metadata.
getMetadataKey()849     private String getMetadataKey() {
850         long time = Math.max(fileCacheTime, xmpFileCacheTime);
851         return file.getAbsolutePath() + "_" + time;
852     }
853 
getXmpKey()854     private String getXmpKey() {
855         return file.getAbsolutePath() + "_xmp_file";
856     }
857 
858     // Observe modification times for file and xmpFile.  These times are used
859     // for modification polling in ImageListPoller and also to timestamp
860     // cached metadata.
updateFileTimes()861     private void updateFileTimes() {
862         if (cache == null) {
863             return;
864         }
865         fileCacheTime = file.lastModified();
866         if (xmpFile != null) {
867             xmpFileCacheTime = xmpFile.lastModified();
868         }
869         else {
870             xmpFileCacheTime = 0;
871         }
872     }
873 
getFileTimeCacheKey()874     private String getFileTimeCacheKey() {
875         return file.getAbsolutePath() + "_cache_time";
876     }
877 
getXmpFileTimeCacheKey()878     private String getXmpFileTimeCacheKey() {
879         return xmpFile.getAbsolutePath() + "_cache_time";
880     }
881 
clearPreview()882     private void clearPreview() {
883         PreviewUpdater.clearCachedPreviewForImage(meta, cache);
884     }
885 
logMetadataError(Throwable t)886     private void logMetadataError(Throwable t) {
887         StringBuffer buffer = new StringBuffer();
888         buffer.append(file.getAbsolutePath());
889         buffer.append(" reading metadata ");
890         buffer.append(t.getClass().getName());
891         if (t.getMessage() != null) {
892             buffer.append(": ");
893             buffer.append(t.getMessage());
894         }
895         System.err.println(buffer);
896     }
897 
writeToXmp(ImageInfo info)898     private void writeToXmp(ImageInfo info)
899         throws IOException, BadImageFileException, UnknownImageTypeException
900     {
901         info.getImageType().writeMetadata(info);
902         try {
903             this.xmpFile = new File(info.getXMPFilename());
904         }
905         catch (Throwable e) {
906             logMetadataError(e);
907             this.xmpFile = null;
908         }
909     }
910 }
911