/* * Copyright (C) 2013 brobert. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * MA 02110-1301 USA */ package jace.library; import jace.core.Utility; import jace.library.MediaEntry.MediaFile; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; import java.net.MalformedURLException; import java.net.URI; import java.net.URISyntaxException; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.Map; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; /** * Holds all information about media titles, manages low-level operations of * downloading images from online sources, and also manages the persistence of * the media library * * @author brobert */ public class MediaCache implements Serializable { public static int DELAY_BEFORE_PERSISTING_LIBRARY = 2000; public static MediaCache LOCAL_LIBRARY; public static MediaEntry getMediaFromFile(File draggedFile) { MediaEntry entry = new MediaEntry(); MediaFile file = new MediaFile(); file.path = draggedFile; file.temporary = false; file.activeVersion = true; entry.files = new ArrayList<>(); entry.files.add(file); entry.isLocal = true; entry.type = DiskType.determineType(draggedFile); return entry; } public static MediaEntry getMediaFromUrl(String url) { throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. } public Set<Long> favorites; public Map<String, Set<Long>> nameLookup; public Map<String, Set<Long>> categoryLookup; public Map<String, Set<Long>> keywordLookup; public Map<Long, MediaEntry> mediaLookup; public long lastDirtyMarker; public MediaCache() { favorites = new HashSet<>(); nameLookup = new HashMap<>(); categoryLookup = new HashMap<>(); keywordLookup = new HashMap<>(); mediaLookup = new HashMap<>(); } public static MediaCache getLocalLibrary() { if (LOCAL_LIBRARY == null) { LOCAL_LIBRARY = new MediaCache(); LOCAL_LIBRARY.readLibraryFromDisk(); } return LOCAL_LIBRARY; } private void cleanup() { cleanup(nameLookup); cleanup(categoryLookup); cleanup(keywordLookup); } private void cleanup(Map<String, Set<Long>> lookup) { Set<String> remove = new HashSet<>(); lookup.entrySet().stream().forEach((entry) -> { if (entry.getValue() == null || entry.getValue().isEmpty()) { remove.add(entry.getKey()); } else { boolean hasSomething = false; for (Iterator<Long> l = entry.getValue().iterator(); l.hasNext();) { if (mediaLookup.containsKey(l.next())) { hasSomething = true; } else { l.remove(); } } if (!hasSomething) { remove.add(entry.getKey()); } } }); lookup.keySet().removeAll(remove); } public void add(MediaEntry e) { e.isLocal = this.equals(LOCAL_LIBRARY); // Randomize ID if it is not already set // This is a combination of the string hash as well as a 8-bit sequence number if (e.id == 0 || e.id == -1) { e.id = e.name.hashCode(); e.id <<= 8; while (mediaLookup.containsKey(e.id)) { e.id++; } } mediaLookup.put(e.id, e); cacheEntry(nameLookup, e.name, e.id); cacheEntry(categoryLookup, e.category, e.id); if (e.favorite) { favorites.add(e.id); } for (String s : e.keywords) { cacheEntry(keywordLookup, s, e.id); } markDirty(); } public void remove(MediaEntry e) { mediaLookup.remove(e.id); removeFiles(e); cleanup(); markDirty(); } public void update(MediaEntry e) { remove(e); add(e); } private void cacheEntry(Map<String, Set<Long>> cache, String key, long id) { Set<Long> ids = cache.get(key); if (ids == null) { ids = new HashSet<>(); cache.put(key, ids); } ids.add(id); } public static File getMediaLibraryFolder() { String userHome = System.getProperty("user.home"); if (userHome == null || userHome.equals("")) { userHome = "."; } File f = new File(new File(userHome, ".jace"), "mediaLibrary"); if (!f.exists()) { f.mkdirs(); } return f; } // Remove file(s) associated with media entry private void removeFiles(MediaEntry e) { if (e.files == null) { return; } e.files.stream().forEach((f) -> { f.path.delete(); }); Utility.gripe("All disk images for " + e.name + " have been deleted."); } public MediaEntry.MediaFile getCurrentFile(MediaEntry e, boolean isPermanent) { if (e == null) { return null; } if (e.files == null || e.files.isEmpty()) { e.files = new ArrayList<>(); getLocalLibrary().add(e); getLocalLibrary().createBlankFile(e, "Initial", !isPermanent); // getLocalLibrary().downloadImage(e, e.files.get(0), true); } for (MediaEntry.MediaFile f : e.files) { if (f.activeVersion) { return f; } } e.files.get(0).activeVersion = true; return e.files.get(0); } public void saveFile(MediaEntry e, InputStream data) { // saveFile(getCurrentFile(e, MediaLibrary.CREATE_LOCAL_ON_SAVE), data); } public void saveFile(MediaFile f, InputStream data) { // TODO: If file is temporary but is supposed to be created local when saved, then move the save to a permanent file!! // if (f.temporary && MediaLibrary.CREATE_LOCAL_ON_SAVE) { // f = convertTemporaryFileToLocal(f); // } FileOutputStream fos = null; f.lastWritten = System.currentTimeMillis(); try { fos = new FileOutputStream(f.path, false); byte[] b = new byte[4096]; while (data.available() > 0) { int read = data.read(b); fos.write(b, 0, read); } fos.close(); } catch (FileNotFoundException ex) { Logger.getLogger(MediaCache.class.getName()).log(Level.SEVERE, null, ex); Utility.gripe("Could not write disk for " + f.path + " -- File not found!"); } catch (IOException ex) { Logger.getLogger(MediaCache.class.getName()).log(Level.SEVERE, null, ex); Utility.gripe("Could not write disk for " + f.path + " -- I/O Exception: " + ex.getMessage()); } finally { try { if (fos != null) { fos.close(); } } catch (IOException ex) { Logger.getLogger(MediaCache.class.getName()).log(Level.SEVERE, null, ex); } } } boolean disableWrites = false; public void readLibraryFromDisk() { disableWrites = true; ObjectInputStream in = null; try { File mediaCatalogFile = getMediaLibaryCatalog(); if (!mediaCatalogFile.exists()) { return; } FileInputStream fileStream = new FileInputStream(mediaCatalogFile); in = new ObjectInputStream(fileStream); while (fileStream.available() > 0) { MediaEntry e = (MediaEntry) in.readObject(); add(e); } } catch (IOException | ClassNotFoundException ex) { Utility.gripe(ex.getMessage()); Logger.getLogger(MediaCache.class.getName()).log(Level.SEVERE, null, ex); } finally { disableWrites = false; try { if (in != null) { in.close(); } } catch (IOException ex) { Logger.getLogger(MediaCache.class.getName()).log(Level.SEVERE, null, ex); } } } public void writeLibraryToDisk() { ObjectOutputStream out = null; try { File mediaCatalogFile = getMediaLibaryCatalog(); out = new ObjectOutputStream(new FileOutputStream(mediaCatalogFile)); for (MediaEntry e : mediaLookup.values()) { out.writeObject(e); } out.close(); } catch (FileNotFoundException ex) { Logger.getLogger(MediaCache.class.getName()).log(Level.SEVERE, null, ex); } catch (IOException ex) { Logger.getLogger(MediaCache.class.getName()).log(Level.SEVERE, null, ex); } finally { try { if (out != null) { out.close(); } } catch (IOException ex) { Logger.getLogger(MediaCache.class.getName()).log(Level.SEVERE, null, ex); } } } Thread writerWorker; public void markDirty() { // We don't really care if a remote library is changing if (disableWrites || !this.equals(getLocalLibrary())) { return; } lastDirtyMarker = System.nanoTime(); if (writerWorker == null || !writerWorker.isAlive()) { writerWorker = new Thread(new Runnable() { long timeCheck = 0; @Override public void run() { while (true) { try { Thread.sleep(DELAY_BEFORE_PERSISTING_LIBRARY / 2); // Wait half the delay before capturing the value // this will help avoid unnecessary delays when the // library is updated a bunch in a small interval of time timeCheck = lastDirtyMarker; Thread.sleep(DELAY_BEFORE_PERSISTING_LIBRARY / 2); } catch (InterruptedException ex) { Logger.getLogger(MediaCache.class.getName()).log(Level.SEVERE, null, ex); continue; } // If the invalidation time stamp changed again wait longer if (timeCheck != lastDirtyMarker) { continue; } writeLibraryToDisk(); // If the invalidation stamp wasn't changed then the worker can exit if (timeCheck == lastDirtyMarker) { break; } } } }); writerWorker.start(); } } private MediaFile downloadTempCopy(MediaEntry e) { // downloadImage(e, getCurrentFile(e, false), isDownloading); return getCurrentFile(e, false); } static boolean isDownloading = false; // // public void downloadImage(final MediaEntry e, final MediaFile target, boolean wait) { // isDownloading = true; // Utility.runModalProcess("Loading disk image...", () -> { // InputStream in = null; // try { // URI uri = null; // try { // uri = new URI(e.source); // } catch (URISyntaxException ex) { // File f = new File(e.source); // if (f.exists()) { // uri = f.toURI(); // } // } // if (uri == null) { // Utility.gripe("Unable to resolve path: " + e.source); // return; // } // in = uri.toURL().openStream(); // saveFile(target, in); // } catch (MalformedURLException ex) { // Utility.gripe("Unable to resolve path: " + e.source); // Logger.getLogger(MediaCache.class.getName()).log(Level.SEVERE, null, ex); // } catch (IOException ex) { // Utility.gripe("Unable to download file: " + ex.getMessage()); // Logger.getLogger(MediaCache.class.getName()).log(Level.SEVERE, null, ex); // } finally { // isDownloading = false; // try { // if (in != null) { // in.close(); // } // } catch (IOException ex) { // Logger.getLogger(MediaCache.class.getName()).log(Level.SEVERE, null, ex); // } // } // }); // if (wait) { // int timeout = 10000; // while (timeout > 0 && isDownloading) { // try { // Thread.sleep(100); // } catch (InterruptedException ex) { // return; // } // timeout -= 100; // } // } // } private void createBlankFile(MediaEntry e, String label, boolean isTemporary) { MediaFile f = new MediaEntry.MediaFile(); e.files.add(f); f.activeVersion = true; f.checksum = 0L; f.label = label; f.lastWritten = System.currentTimeMillis(); f.temporary = isTemporary; // Now generate new file path File mediaFolder = isTemporary ? getTempDirectory() : getMediaLibraryFolder(); String name = e.name.replaceAll("[^0-9A-Za-z]", ""); String s1 = e.name.length() > 0 ? name.substring(0, 1) : "_"; String s2 = e.name.length() > 1 ? name.substring(1, 2) : "_"; File sub1 = new File(mediaFolder, "_" + s1); File sub2 = new File(sub1, "_" + s2); sub2.mkdirs(); f.path = new File(sub2, name + "_" + System.nanoTime()); if (isTemporary) { sub1.deleteOnExit(); sub2.deleteOnExit(); f.path.deleteOnExit(); } } private MediaFile resolveLocalCopy(MediaEntry e) { if (!e.isLocal) { e = findLocalEntry(e); } // If this is a local entry, load the current file if (e.isLocal) { MediaFile f = getCurrentFile(e, true); if (f != null && f.path.exists()) { return f; } } // If there is no current file, download it // if (MediaLibrary.CREATE_LOCAL_ON_LOAD) { // getLocalLibrary().add(e); // MediaFile f = getCurrentFile(e, true); // downloadImage(e, f, true); // return f; // } else { return downloadTempCopy(e); // } } public MediaEntry findLocalEntry(MediaEntry e) { for (MediaEntry entry : getLocalLibrary().mediaLookup.values()) { if (entry.source.equals(e.source)) return entry; } return null; } private File getTempDirectory() { String temp = System.getProperty("java.io.tmpdir"); File tempDir = null; if (temp != null) { tempDir = new File(temp); if (tempDir.exists() && tempDir.isDirectory()) { return tempDir; } tempDir = new File(getMediaLibraryFolder(), "temp"); tempDir.mkdirs(); tempDir.deleteOnExit(); } return tempDir; } private MediaFile convertTemporaryFileToLocal(MediaFile f) { throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. } private File getMediaLibaryCatalog() { return new File(getMediaLibraryFolder(), "mediacache.db"); } }