jace/src/main/java/jace/library/MediaCache.java

476 lines
16 KiB
Java

/*
* 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");
}
}