/** * The Assets class is used to manage cachable assets. */ package ecologylab.io; import java.io.File; import java.net.URLEncoder; import ecologylab.appframework.ApplicationEnvironment; import ecologylab.appframework.ApplicationProperties; import ecologylab.appframework.Environment; import ecologylab.appframework.EnvironmentGeneric; import ecologylab.appframework.PropertiesAndDirectories; import ecologylab.appframework.SingletonApplicationEnvironment; import ecologylab.appframework.StatusReporter; import ecologylab.appframework.types.AssetState; import ecologylab.appframework.types.AssetsState; import ecologylab.appframework.types.AssetsTranslations; import ecologylab.generic.Debug; import ecologylab.generic.StringBuilderPool; import ecologylab.net.ParsedURL; import ecologylab.serialization.SIMPLTranslationException; import ecologylab.serialization.SimplTypesScope; import ecologylab.serialization.formatenums.Format; /** * Used to manage cachable assets. * <p/> * This class must not be called until codeBase is properly set! * <p/> * Here's how it works: (1) There is a file called assets.xml. It lives only in * applicationDataDir(), the cache root. There is *no* version of this file in * <codeBase>/config/preferences, the assets root. * * This file stores the *current* cached version # of each asset. * * (2) The version of each Asset is a constant, which lives *in the code*. It is passed to * Assets.downloadZip(). (3) If there is no local versions of assets.xml (first time ap is run), or * if any version turns out to be stale, the assets.xml file will need to be written to the cache * root. The application *must* call updateAssetsXml() to do this, after it is finished reading all * Assets. This method will know if writing the file is needed or not. * * @author blake * @author andruid */ public class Assets extends Debug implements ApplicationProperties { /** * Source URL root of the tree of assets for this application. Default is the configDir(), which * in turn is the config subdir of codebase. * * * The source location of any asset is specified relative to here. */ static ParsedURL assetsRoot; protected static final String ASSETS_XML_NAME = "assets.xml"; static File assetsXmlFile; /** * Asset version number info! */ static AssetsState assetsState; static boolean needToWriteAssetsXml; /** * The root directory on the local machine where assets will be stored (cached). * * The cache destination of any asset is applied relative to here. */ static File cacheRoot; /* * Set-up assets and cache roots. Read currently downloaded */ static { cacheRoot = /* * ApplicationEnvironment.runningInEclipse() ? EnvironmentGeneric.configDir().file() * : */PropertiesAndDirectories.thisApplicationDir(); assetsXmlFile = new File(cacheRoot, ASSETS_XML_NAME); if (assetsXmlFile.exists()) { try { assetsState = (AssetsState) AssetsTranslations.get().deserialize(assetsXmlFile, Format.XML); } catch (SIMPLTranslationException e) { println("ERROR reading AssetsState from " + assetsXmlFile); e.printStackTrace(); } } else { println("Assets: no cached assets found at " + assetsXmlFile); } if (assetsState == null) assetsState = new AssetsState(); } // //////////////////////////////////////////////////////////// /** * No instances possible, static references only. */ private Assets() { } /** * Given a relative path, return a file reference to this path from the cache root. * * @param relativePath * A string representing the relative file path. * @return A file reference to the requested path */ public static File getAsset(String relativePath) { if (cacheRoot == null) return null; return new File(cacheRoot.getAbsolutePath() + File.separatorChar + relativePath); } /** * Same as getAsset(String), but allows additional relative file/directory to be specified against * the relativePath * * @param relativePath * A string representing the relative file path * @param additionalContext * A string representing an additional relative path. This path is relative to the * relativePath parameter (rather than the cache root). * @return A file reference to the requested path */ public static File getAsset(String relativePath, String additionalContext) { if (cacheRoot == null) return null; return new File(getAsset(relativePath), additionalContext); } /** * Same as getAsset(String), but creates the Asset location if it doesn't exist * * @param relativePath * A string representing the relative file path. * @return A file reference tot he requested path */ public static File getAndPerhapsCreateAsset(String relativePath) { File theAsset = getAsset(relativePath); if (!theAsset.exists()) theAsset.mkdirs(); return theAsset; } /** * Same as getAndPerhapsCreateAsset(String, String), but creates the Asset location if it doesn't * exist * * @param relativePath * A string representing the relative file path. * @param additionalContext * A string representing an additional relative path. This path is relative to the * relativePath parameter (rather than the cache root). * * @return A file reference to the requested path * @see #getAsset(String, String) */ public static File getAndPerhapsCreateAsset(String relativePath, String additionalContext) { File theAsset = getAsset(relativePath, additionalContext); if (!theAsset.exists()) theAsset.mkdirs(); return theAsset; } public static File getAsset(AssetsRoot assetsRoot, String relativePath) { return Files.newFile(assetsRoot.getCacheRoot(), relativePath); } public static File getAsset(AssetsRoot assetsRoot, String relativePath, String assetName, StatusReporter status, boolean forceDownload, float version) { File result = relativePath != null ? getAsset(assetsRoot, relativePath) : assetsRoot.getCacheRoot(); if (!SingletonApplicationEnvironment.runningInEclipse()) downloadZip(assetsRoot, assetName, status, forceDownload, version); return result; } /** * Version of getAsset that uses a non-singleton ApplicationEnvironment to know where to store * assets. * * @param assetsRoot * @param relativePath * @param assetName * @param status * @param forceDownload * @param version * @param env * @return */ public static File getAsset(AssetsRoot assetsRoot, String relativePath, String assetName, StatusReporter status, boolean forceDownload, float version, ApplicationEnvironment env) { File result = relativePath != null ? getAsset(assetsRoot, relativePath) : assetsRoot.getCacheRoot(); if (!env.isRunningInEclipse()) downloadZip(assetsRoot, assetName, status, forceDownload, version); return result; } public static void downloadZip(ParsedURL sourceZip, File targetFile, boolean forceDownload, float version) { downloadZip(sourceZip, targetFile, null, forceDownload, version); } public static void downloadZip(AssetsRoot assetsRoot, String assetName, StatusReporter status, boolean forceDownload, float version) { downloadZip(assetsRoot.getAssetRoot().getRelative(assetName + ".zip", "forming zip location"), assetsRoot.getCacheRoot(), status, forceDownload, version); } /** * Download and uncompress a zip file from a source to a target location with minimal effort, * unless the zip file already exists at the target location, in which case, do nothing. * * @param status * The Status object that provides a source of state change visibility; can be null. * @param forceDownload * @param version */ public static void downloadZip(ParsedURL sourceZip, File targetDir, StatusReporter status, boolean forceDownload, float version) { String zipFileName = sourceZip.url().getFile(); int lastSlash = zipFileName.lastIndexOf('\\'); if (lastSlash == -1) lastSlash = zipFileName.lastIndexOf('/'); zipFileName = zipFileName.substring(lastSlash + 1); File zipFileDestination = Files.newFile(targetDir, zipFileName); if (forceDownload || !zipFileDestination.canRead() || !localVersionIsUpToDate(zipFileName, version)) { if (targetDir.exists()) Files.deleteDirectory(targetDir); ZipDownload downloadingZip = ZipDownload.downloadAndPerhapsUncompress(sourceZip, targetDir, status, true); Debug.println("downloading zip: " + downloadingZip.toString()); if (downloadingZip != null) // null if already available locally or error { downloadingZip.waitForDownload(); } } else println("Using cached " + zipFileDestination); } /** * Get the source URL root of the tree of assets for this application. Default is the configDir(), * which in turn is the config subdir of codebase. * * @return ParsedURL referring to the root of the remote place we download assets from. */ public static ParsedURL assetsRoot() { return EnvironmentGeneric.configDir(); } /** * Get the source URL root of the tree of assets for this application. Default is the configDir(), * which in turn is the config subdir of codebase. * * @return ParsedURL referring to the root of the remote place we download assets from. */ public static ParsedURL assetsRoot(Environment e) { return EnvironmentGeneric.configDir(e); } /** * Get the root file path for caching. Assets are specified relative to this path. * * @return */ public static File cacheRoot() { return cacheRoot; } /** * Download XML from the sourcePath, within the assetsRoot (the application's config dir), to the * target path within the applicationDir. * * @param sourcePath * @param targetPath * @param status */ public static void downloadXML(String sourcePath, String targetPath, StatusReporter status) { File targetDir = cacheRoot(); if ((targetPath != null) && (targetPath.length() > 0)) targetDir = Files.newFile(targetDir, targetPath); downloadXML(assetsRoot().getRelative(sourcePath, "forming Asset path location"), targetDir, status); } /** * Download XML from the sourcePath, within the assetsRoot (the application's config dir), to the * target path within the applicationDir. * * @param sourcePath * @param targetPath * @param status */ public static void downloadXML(Environment e, String sourcePath, String targetPath, StatusReporter status) { File targetDir = cacheRoot(); if ((targetPath != null) && (targetPath.length() > 0)) targetDir = Files.newFile(targetDir, targetPath); downloadXML(EnvironmentGeneric.configDir(e).getRelative(sourcePath, "forming Asset path location"), targetDir, status); } /** * Download an XML file from a source to a target location with minimal effort, unless the XML * file already exists at the target location, in which case, do nothing. * * @param status * The Status object that provides a source of state change visiblity; can be null. * @param sourceXML * The location of the zip file to download and uncompress. * @param targetDir * The location where the zip file should be uncompressed. This directory structure will * be created if it doesn't exist. */ public static void downloadXML(ParsedURL sourceXML, File targetDir, StatusReporter status) { String xmlFileName = sourceXML.url().getFile(); int lastSlash = xmlFileName.lastIndexOf('\\'); if (lastSlash == -1) lastSlash = xmlFileName.lastIndexOf('/'); xmlFileName = xmlFileName.substring(lastSlash + 1); File xmlFileDestination = Files.newFile(targetDir, xmlFileName); if (!xmlFileDestination.canRead()) { // we just want to download it, not uncompress it... (using code from zip downloading stuff) ZipDownload downloadingZip = ZipDownload.downloadAndPerhapsUncompress(sourceXML, targetDir, status, false); if (downloadingZip != null) // null if already available locally or error { downloadingZip.waitForDownload(); } } else println("Using cached " + xmlFileDestination); } static public final float IGNORE_VERSION = 0f; /** * Determines if a file should be downloaded again, based upon it's file version. * * @param id * the name of the file to check * @param requiredVersion * the version of that file \ * @return false if the local asset is stale and to download true if the local version is fine and * we dont need to download */ public static boolean localVersionIsUpToDate(String id, float requiredVersion) { if (requiredVersion == IGNORE_VERSION) return true; AssetState assetState = assetsState.lookup(id); boolean result = assetState != null; if (result) { float localVersion = assetState.getVersion(); result = requiredVersion <= localVersion; } else { // create an entry to write later when the application developer calls updateAssetsXml(). assetState = assetsState.update(id); } if (!result) { needToWriteAssetsXml = true; assetState.setVersion(requiredVersion); // update the version in our data structure } return result; } /** * When necessary, re-write the local (and only) assets.xml file. * */ public static void updateAssetsXml(String sourceSpot) { try { sourceSpot = " from " + sourceSpot; if (needToWriteAssetsXml) { needToWriteAssetsXml = false; // assetsState.translateToXML(assetsXmlFile); SimplTypesScope.serialize(assetsState, assetsXmlFile, Format.XML); println("Saved Assets XML" + sourceSpot + ": " + assetsXmlFile); } else println("NO NEED to Save Assets XML" + sourceSpot + ": " + assetsXmlFile); } catch (SIMPLTranslationException e) { e.printStackTrace(); } } private static final StringBuilderPool stringPool = new StringBuilderPool(2, 255); /** * Derive a non-duplicate cache filename given a ParsedURL location * * @param location * The location of the file to be cached. * @param directory * The directory to cache the file * @param additional * An additional text to add to the end of the filename, but before the extension * @param separator * Separator used to divide sections of filename (e.g. "-", ".") * @param extension * Extension to add to file name if it doesn't already exist * * @return A string containing the new filename */ @SuppressWarnings("deprecation") public static String getCacheFilename(ParsedURL location, File directory, String additional, String separator, String extension) { StringBuilder filename = stringPool.nextBuffer(); filename.append(location.host()); filename.append(separator); String locationString = location.url().getPath(); String query = location.url().getQuery(); filename.append(locationString.substring(locationString.lastIndexOf('/') + 1)); if (query != null && query.length() > 0) { filename.append("%3F"); filename.append(URLEncoder.encode(query)); } if (additional != null) { filename.append(separator); filename.append(additional); } if (extension != null && !locationString.endsWith(extension)) { filename.append("."); filename.append(extension); } String filenameString = filename.toString(); // File localFile = new File(directory, filename.toString()); // if (localFile.exists()) // { // int extensionStart = filename.lastIndexOf("."); // int count = 1; // String pre = filename.substring(0, extensionStart); // String end = filename.substring(extensionStart); // // while(localFile.exists()) // { // StringBuilder newFilename = stringPool.nextBuffer(); // newFilename.append(pre); // newFilename.append(separator); // newFilename.append(count); // newFilename.append(end); // filenameString = newFilename.toString(); // localFile = new File(directory, filenameString); // count++; // stringPool.release(newFilename); // } // stringPool.release(filename); // } return filenameString; } /** * @return the assetsRoot */ public static ParsedURL getAssetsRoot() { return assetsRoot; } /** * @return the cacheRoot */ public static File getCacheRoot() { return cacheRoot; } }