package com.laytonsmith.PureUtilities.ClassLoading; import com.laytonsmith.PureUtilities.Common.StringUtils; import com.laytonsmith.PureUtilities.ProgressIterator; import com.laytonsmith.PureUtilities.ZipReader; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.net.URL; import java.net.URLDecoder; import java.security.MessageDigest; import java.util.Enumeration; import java.util.jar.JarEntry; import java.util.jar.JarFile; import java.util.logging.Level; import java.util.logging.Logger; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; /** * This file represents a location on disk that can be used by the ClassDiscovery * class to facilitate caching. Files will be automatically managed by this class, * and it provides high level functions for getting a cache, regardless of whether * or not it actually exists yet. */ public class ClassDiscoveryCache { /** * This is the name of the jar annotation file. getResource(ClassDiscovery.OUTPUT_FILENAME) should * return the file that was output during the build, for this jar for sure. Third party libs * may not be using the same convention though, so this will fail. Regardless, the system should * still function, though it will have to do one time cache setup first. */ public static final String OUTPUT_FILENAME = "jarInfo.ser"; /** * How much of the file we read in to hash to check for collisions. */ private static final int READ_SIZE = 2048; private File cacheDir; private ProgressIterator progress; private Logger logger; /** * Creates a new ClassDiscoveryCache. The File is the location on * disk which is used to write the cache files to. * @param cacheDir */ public ClassDiscoveryCache(File cacheDir){ this.cacheDir = cacheDir; } /** * If not null, informational output is logged to this logger. * @param logger */ public void setLogger(Logger logger){ this.logger = logger; } /** * Given a file location, retrieves the ClassDiscoveryURLCache from it. * If it is a jar, the file is hashed, and checked for a local cache copy, * and if one exists, that cache is returned. * If not, the jar is scanned for a jarInfo.ser. If one exists, it is returned. * Otherwise, the jar is scanned, a local cache is saved to disk, then returned. * * No exceptions will be thrown from this class, if something fails, it will fall back * to ultimately just regenerating the cache from source. * @param fromClassLocation The jar to be cached. Note that if this doesn't * denote a jar, the cache will not be written to disk, however a URLCache will * be returned none-the-less. * @return */ public ClassDiscoveryURLCache getURLCache(URL fromClassLocation){ if(fromClassLocation.toString().endsWith(".jar")){ File cacheOutputName = null; try { File jarFile = new File(URLDecoder.decode(fromClassLocation.getFile(), "UTF8")); byte[] data; try (FileInputStream fis = new FileInputStream(jarFile)) { data = new byte[READ_SIZE]; fis.read(data); } MessageDigest digest = java.security.MessageDigest.getInstance("MD5"); digest.update(data); String fileName = StringUtils.toHex(digest.digest()); cacheOutputName = new File(cacheDir, fileName); if(cacheOutputName.exists()){ //Cool, already exists, so we'll just return this. //Note that we write the data out as a zip, since it is //huge otherwise, and compresses quite well, so we have //to read it in as a zip now. ZipReader cacheReader = new ZipReader(new File(cacheOutputName, "data")); return new ClassDiscoveryURLCache(fromClassLocation, cacheReader.getInputStream()); } //Doesn't exist, but we set cacheOutputName, so it will save it there //after it scans. } catch (Exception ex) { //Hmm. Ok, well, we'll just regenerate. } JarFile jfile; try { jfile = new JarFile(URLDecoder.decode(fromClassLocation.getFile(), "UTF8")); for (Enumeration<JarEntry> ent = jfile.entries(); ent.hasMoreElements();) { JarEntry entry = ent.nextElement(); if (entry.getName().equals(ClassDiscoveryCache.OUTPUT_FILENAME)) { InputStream is = jfile.getInputStream(entry); try { return new ClassDiscoveryURLCache(fromClassLocation, is); } catch (Exception ex) { //Do nothing, we'll just re-load from disk. } } } } catch (IOException ex) { Logger.getLogger(ClassDiscoveryCache.class.getName()).log(Level.SEVERE, null, ex); } if(logger != null){ logger.log(Level.INFO, "Performing one time scan of {0}, this may take a few moments.", fromClassLocation); } ClassDiscoveryURLCache cache = new ClassDiscoveryURLCache(fromClassLocation, progress); if(cacheOutputName != null){ try { try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(cacheOutputName, false))) { zos.putNextEntry(new ZipEntry("data")); cache.writeDescriptor(zos); } } catch (IOException ex) { //Well, we couldn't write it out, so report the error, but continue anyways. if(logger != null){ logger.log(Level.SEVERE, null, ex); } else { //Report errors even if the logger passed in is null. Logger.getLogger(ClassDiscoveryCache.class.getName()).log(Level.SEVERE, null, ex); } } } return cache; } else { return new ClassDiscoveryURLCache(fromClassLocation, progress); } } public void setProgressIterator(ProgressIterator progress){ this.progress = progress; } }