/* $Id: Cache.java,v 1.34 2011/04/07 20:50:40 kiheru Exp $ */
/***************************************************************************
* (C) Copyright 2003-2010 - Stendhal *
***************************************************************************
***************************************************************************
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
***************************************************************************/
package games.stendhal.client;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import java.util.Properties;
import marauroa.common.Configuration;
import marauroa.common.ConfigurationParams;
import marauroa.common.io.Persistence;
import marauroa.common.net.message.TransferContent;
import org.apache.log4j.Logger;
/**
* <p>
* Manages a two level cache which one or both levels are optional:
* </p>
*
* <p>
* The first level is prefilled readonly cache in a .jar file on class path. At
* the time of writing we use this for Webstart because we are unsure how large
* the webstart PersistenceService may grow.
* </p>
*
* <p>
* The second level is a normal cache on filesystem.
* </p>
*/
public class Cache {
private static final String VERSION_KEY = "_VERSION";
private static Logger logger = Logger.getLogger(Cache.class);
private Configuration cacheManager;
private Properties prefilledCacheManager;
/**
* Return the client configuration instance
* (content of "stendhal.cache").
*
* @return the singleton instance
*/
public Configuration getConfiguration() {
return cacheManager;
}
/**
* Inits the cache.
*/
protected void init() {
try {
prefilledCacheManager = new Properties();
final URL url = this.getClass().getClassLoader().getResource("cache/stendhal.cache");
if (url != null) {
final InputStream is = url.openStream();
prefilledCacheManager.load(is);
is.close();
}
// init caching-directory
if (!stendhal.WEB_START_SANDBOX) {
// Create file object
File file = new File(stendhal.getGameFolder());
if (!file.exists() && !file.mkdirs()) {
logger.error("Can't create " + file.getAbsolutePath()
+ " folder");
} else if (file.exists() && file.isFile()) {
if (!file.delete() || !file.mkdirs()) {
logger.error("Can't removing file "
+ file.getAbsolutePath()
+ " and creating a folder instead.");
}
}
file = new File(stendhal.getGameFolder() + "cache");
if (!file.exists() && !file.mkdir()) {
logger.error("Can't create " + file.getAbsolutePath()
+ " folder");
}
}
initCacheManager();
cleanCacheOnUpdate();
cacheManager.set(VERSION_KEY, stendhal.VERSION);
} catch (final IOException e) {
logger.error("cannot create StendhalClient - IO exception", e);
} catch (final RuntimeException e) {
logger.error("cannot create StendhalClient", e);
}
}
/**
* Empty cache on update.
*
* Stendhal is known to crash, if incompatible stuff is in cache.
*
* @throws IOException
* in case the cache folder is not writeable
*/
private void cleanCacheOnUpdate() throws IOException {
if (!cacheManager.has(VERSION_KEY)
|| !stendhal.VERSION.equals(cacheManager.get(VERSION_KEY))) {
cleanCache();
cacheManager.clear();
initCacheManager();
}
}
/**
* initializes the low level cache manager.
*
* @throws IOException
* in case the cache folder is not readable
*/
private boolean initCacheManager() throws IOException {
final String cacheFile = stendhal.getGameFolder() + "cache"
+ File.separator + "stendhal.cache";
// create a new cache file if doesn't exist already
boolean ret = new File(cacheFile).createNewFile();
cacheManager = new Configuration(new ConfigurationParams(
false, stendhal.getGameFolder(), "cache/stendhal.cache"));
return ret;
}
/**
* Deletes the cache.
*/
private void cleanCache() {
final File cacheDir = new File(stendhal.getGameFolder() + "cache");
if (cacheDir.isDirectory()) {
final File[] files = cacheDir.listFiles();
for (final File file : files) {
if (!file.delete()) {
logger.debug("File was not deleted while cleaning cache:" +
file.getAbsolutePath());
}
}
}
}
private InputStream getItemFromPrefilledCache(final TransferContent item) {
final String name = "cache/" + item.name;
// note: timestamp may contain a checksum. So we have to do an
// "equal"-compare.
final String timestamp = prefilledCacheManager.getProperty(item.name);
if ((timestamp != null)
&& (Integer.parseInt(timestamp) == item.timestamp)) {
// get the stream
final URL url = this.getClass().getClassLoader().getResource(name);
if (url != null) {
try {
logger.debug("Content " + item.name
+ " is in prefilled cache.");
return url.openStream();
} catch (final IOException e) {
logger.error(e, e);
}
}
}
return null;
}
private InputStream getItemFromCache(final TransferContent item) {
try {
// check cache
if (cacheManager.has(item.name)
&& (Integer.parseInt(cacheManager.get(item.name)) == item.timestamp)) {
logger.debug("Content " + item.name
+ " is on cache. We save transfer");
// get the stream
try {
return Persistence.get().getInputStream(false, stendhal.getGameFolder() + "cache", item.name);
} catch (final IOException e) {
logger.warn("Cannot read cache file: " + item.name);
}
}
} catch (final NumberFormatException e) {
logger.error("Broken cache entry: " + item.name, e);
}
return null;
}
/**
* Gets an item from cache.
*
* @param item
* key
* @return InputStream or null if not in cache
*/
protected InputStream getItem(final TransferContent item) {
if (item.name.indexOf("..") > -1) {
logger.error("Cannot get item from cache because .. is not allowed in name " + item.name);
return null;
}
// 1. try to read it from stendhal-prefilled-cache.jar
InputStream is = getItemFromPrefilledCache(item);
// 2. try to read from our cache (if not in sandbox)
if (is == null) {
is = getItemFromCache(item);
}
return is;
}
/**
* Stores an item in cache.
*
* @param item
* key
* @param data
* data
*/
protected void store(final TransferContent item, final byte[] data) {
try {
if (item.name.indexOf("..") > -1) {
logger.error("Cannot store item to cache because .. is not allowed in name " + item.name);
return;
}
final OutputStream os = Persistence.get().getOutputStream(false,
stendhal.getGameFolder() + "cache", item.name);
os.write(data);
os.close();
logger.debug("Content " + item.name + " cached now. Timestamp: "
+ Integer.toString(item.timestamp));
cacheManager.set(item.name, Integer.toString(item.timestamp));
} catch (final java.io.IOException e) {
logger.error("store", e);
}
}
}