package com.indyforge.twod.engine.resources.assets; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStreamReader; import java.io.ObjectInputStream; import java.io.Serializable; import java.util.LinkedList; import java.util.List; import java.util.Set; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; import com.indyforge.twod.engine.io.IoRoutines; /** * This class represents a bundle of assets which are located in a single * archive file. An asset bundle can define dependencies to other asset bundles * (other archives) by including a specific file. Please check the * implementation to see which form this file must have. * * @author Christopher Probst * @see Asset * @see AssetLoader * @see AssetManager */ public final class AssetBundle implements Serializable { /** * */ private static final long serialVersionUID = 1L; /** * This implementation interpretates "include.txt" as an include file. */ public static final String INCLUDE_FILE_NAME = "include.txt"; /** * This method takes an archive and a set of files and collects all * dependencies of this archive recursively. Duplicate dependencies are * ignored sinec a set is used to store the archive files. * * @param archive * The archive file you want to check. * @param destination * The set where all dependencies will be stored. * @throws IOException * If an I/O exception occurs. */ public static void collectArchives(File archive, Set<File> destination) throws IOException { if (archive == null) { throw new NullPointerException("archive"); } else if (archive.isAbsolute()) { throw new IllegalArgumentException("Please use a relative path"); } else if (!archive.exists()) { throw new FileNotFoundException("Included archive \"" + archive.getAbsolutePath() + "\" cannot be found."); } else if (destination == null) { throw new NullPointerException("destination"); } /* * IMPORTANT: Check the zip file path. If the path is already included * we can stop here. Otherwise this could lead to infinite loops. */ if (destination.add(archive)) { // Create a new zip archive ZipFile zipArchive = new ZipFile(archive); try { // Try to find entry ZipEntry entry = zipArchive.getEntry(INCLUDE_FILE_NAME); /* * Does this file defines dependencies ? */ if (entry != null) { // Here we store dependencies List<String> dependencies = new LinkedList<String>(); // Open stream BufferedReader reader = new BufferedReader( new InputStreamReader( zipArchive.getInputStream(entry))); // Read all dependencies try { String line; while ((line = reader.readLine()) != null) { dependencies.add(line); } } finally { reader.close(); } // Crawl all dependencies for (String dependency : dependencies) { // Merge the archives collectArchives(new File(archive.getParent(), dependency), destination); } } } finally { zipArchive.close(); } } } /** * Used for {@link AssetBundle#validate()}. * * @author Christopher Probst */ public enum ValidationResult { ArchiveDoesNotExist, InvalidArchiveHash, ArchiveOk } /* * This is the archive which represents this asset bundle. */ private final File archive; /* * This is the archive hash. */ private final String archiveHash; private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { // Restore all vars in.defaultReadObject(); // Check archive when reading try { switch (validate()) { case InvalidArchiveHash: throw new IOException( "Asset bundle archive does not have the same hash"); case ArchiveDoesNotExist: throw new FileNotFoundException( "Asset bundle archive does not exist"); } } catch (Exception e) { throw new IOException("Archive cannot be validated", e); } } /** * Creates a new asset bundle using the give archive file. This method will * not validate the asset bundle. * * @param archive * The archive file. * @throws Exception * If an exception occurs. */ AssetBundle(File archive) throws Exception { // Do not allow absolute paths if (archive.isAbsolute()) { throw new IllegalArgumentException("Archive path must be absolute"); } // Calc the archive hash archiveHash = IoRoutines.calcHexHash(new FileInputStream(archive)); // Save the archive this.archive = archive; } /** * Validates this asset bundle which means that it will check the existence * and integrity of the archive file. * * @return the validation result. * @throws Exception * If an exception occurs. * @see {@link ValidationResult} */ public ValidationResult validate() throws Exception { try { // If the hash equals the calculated hash the archive is ok return IoRoutines.calcHexHash(new FileInputStream(archive)).equals( archiveHash) ? ValidationResult.ArchiveOk : ValidationResult.InvalidArchiveHash; } catch (FileNotFoundException e) { // The archive obviouly does not exist return ValidationResult.ArchiveDoesNotExist; } } /** * @return the archive hash string. */ public String getArchiveHash() { return archiveHash; } /** * @return the archive file. */ public File getArchive() { return archive; } /* * (non-Javadoc) * * @see java.lang.Object#hashCode() */ @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((archive == null) ? 0 : archive.hashCode()); result = prime * result + ((archiveHash == null) ? 0 : archiveHash.hashCode()); return result; } /* * (non-Javadoc) * * @see java.lang.Object#equals(java.lang.Object) */ @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (!(obj instanceof AssetBundle)) { return false; } AssetBundle other = (AssetBundle) obj; if (archive == null) { if (other.archive != null) { return false; } } else if (!archive.equals(other.archive)) { return false; } if (archiveHash == null) { if (other.archiveHash != null) { return false; } } else if (!archiveHash.equals(other.archiveHash)) { return false; } return true; } }