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