/** * Copyright 2012 Jason Sorensen (sorensenj@smert.net) * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the * specific language governing permissions and limitations under the License. */ package net.smert.frameworkgl; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.net.MalformedURLException; import java.net.URISyntaxException; import java.net.URL; import java.util.Enumeration; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.jar.JarEntry; import java.util.jar.JarFile; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * * @author Jason Sorensen <sorensenj@smert.net> */ public class Files { private final static Logger log = LoggerFactory.getLogger(Files.class); private final static String AUDIO_LOCATION = "audio"; private final static String FONT_LOCATION = "fonts"; private final static String GUI_LOCATION = "gui"; private final static String MATERIAL_LOCATION = "materials"; private final static String MESH_LOCATION = "meshes"; private final static String SHADER_LOCATION = "shaders"; private final static String TEXTURE_LOCATION = "textures"; public final String DEFAULT_ASSETS_LOCATION = "/net/smert/frameworkgl/assets"; public final String INTERNAL_FILE_SEPARATOR = "/"; private boolean foundAsset; private boolean isInternal; private boolean useGlslPrefix; private FileType fileType; private final Map<String, FileAsset> filenameToFileAsset; public Files() { useGlslPrefix = true; filenameToFileAsset = new HashMap<>(); try { registerAssets(DEFAULT_ASSETS_LOCATION, true); } catch (IOException | URISyntaxException ex) { log.error("There was an problem loading the default assets location: " + DEFAULT_ASSETS_LOCATION, ex); System.exit(-1); } } private void register(String fullPath, String path, String separator) { int firstForwardSlash = path.indexOf(separator); String directory = path.substring(0, firstForwardSlash); String filename = path.substring(firstForwardSlash + 1); if (directory.isEmpty()) { throw new RuntimeException("The directory length was not greater than zero: " + path); } if (filename.isEmpty()) { throw new RuntimeException("The filename length was not greater than zero: " + path); } if (isInternal && (fileType == FileType.ZIP)) { throw new RuntimeException("The full path cannot be a zip file for internal assets: " + fullPath); } log.debug("Registering asset: File Type: {} Type: {} Filename: {} Full Path: {}", fileType, directory, filename, fullPath); // Create new file asset and key FileAsset fileAsset = new FileAsset(fileType, directory, filename, fullPath, separator); String key = directory + INTERNAL_FILE_SEPARATOR + filename.replace(separator, INTERNAL_FILE_SEPARATOR); // Save the file asset FileAsset oldFileAsset = filenameToFileAsset.put(key, fileAsset); if (oldFileAsset != null) { log.warn("Overwrote a entry in the hash table for the Key: {} New File Asset: {} Old File Asset: {}", key, fileAsset, oldFileAsset); } // We found an asset foundAsset = true; } private void registerExternalAssets(String fullPath) throws IOException { File file = new File(fullPath); registerFileAssets(file); } private void registerDirectoryAssets(String fullPath, String path) throws IOException { File directory = new File(path); File[] files = directory.listFiles(); // Add a trailing slash to this path String fullPathWithTrailingSlash = fullPath; if (!fullPath.endsWith(File.separator)) { fullPathWithTrailingSlash = fullPath + File.separator; } for (File file : files) { if (file.isDirectory()) { registerDirectoryAssets(fullPath, file.getAbsolutePath()); continue; } // Strip relativeFullPath from beginning of entry name String relativePathToBaseDirectory = file.getAbsolutePath().replace(fullPathWithTrailingSlash, ""); // If we are to register an asset then it must be inside a directory. We need the directory // name to determine what type of asset it belongs to. if (!relativePathToBaseDirectory.contains(File.separator)) { continue; } register(fullPath, relativePathToBaseDirectory, File.separator); } } private void registerFileAssets(File file) throws IOException { // Set the file type fileType = FileType.FILE; String absolutePath = file.getAbsolutePath(); if (file.isDirectory()) { registerDirectoryAssets(absolutePath, absolutePath); return; } registerZipFileAssets(file, absolutePath); } private void registerInternalAssets(String fullPath) throws IOException, URISyntaxException { URL url = this.getClass().getResource(fullPath); if (url == null) { throw new IllegalArgumentException("The file or path could not be found: " + fullPath); } switch (url.getProtocol()) { case "file": log.info("Found an internal FILE resource: {}", fullPath); // Try to convert the URL into a URI File file = new File(url.toURI()); registerFileAssets(file); break; case "jar": log.info("Found an internal JAR resource: {}", fullPath); // The url.getPath() method returns something like "file:C:\Program Files\Something\somejar.jar" String jarPath = url.getPath().substring(5, url.getPath().indexOf("!")); registerJarAssets(fullPath, jarPath); break; default: throw new RuntimeException("Unknown protocol: " + url.getProtocol()); } } private void registerJarAssets(String fullPath, String jarPath) throws IOException { // Set the file type fileType = FileType.JAR; // Strip all leading forward slashes (if any) String relativeFullPath = fullPath.replaceFirst("^/+(?!$)", ""); // Add a trailing slash to this path String relativeFullPathWithTrailingSlash = relativeFullPath; if (!relativeFullPath.endsWith(INTERNAL_FILE_SEPARATOR)) { relativeFullPathWithTrailingSlash = relativeFullPath + INTERNAL_FILE_SEPARATOR; } // Try to open the JAR file. try (JarFile jar = new JarFile(jarPath)) { boolean firstEntry = true; Enumeration<JarEntry> entries = jar.entries(); while (entries.hasMoreElements()) { JarEntry entry = entries.nextElement(); String name = entry.getName(); // Name should never start with a forward slash // If the path or file matches the curent entry if (!name.startsWith(relativeFullPath)) { continue; } if (firstEntry) { // If the first entry is not a directory then we have a problem and won't be able to // register assets if (!entry.isDirectory()) { throw new RuntimeException( "The full path was found inside a JAR file and must be a directory: " + fullPath); } firstEntry = false; continue; } // Skip entries that are just directories if (entry.isDirectory()) { continue; } // Strip relativeFullPath from beginning of entry name String relativePathToBaseDirectory = name.replace(relativeFullPathWithTrailingSlash, ""); // If we are to register an asset then it must be inside a directory. We need the directory // name to determine what type of asset it belongs to. if (!relativePathToBaseDirectory.contains(INTERNAL_FILE_SEPARATOR)) { continue; } register(fullPath, relativePathToBaseDirectory, INTERNAL_FILE_SEPARATOR); } } } private void registerZipFileAssets(File file, String fullPath) throws IOException { // Set the file type fileType = FileType.ZIP; // Try to open the file try (FileInputStream fis = new FileInputStream(file); ZipInputStream zip = new ZipInputStream(fis)) { while (true) { ZipEntry entry = zip.getNextEntry(); // If there are no more entries if (entry == null) { break; } // We don't do anything with directories if (entry.isDirectory()) { continue; } String name = entry.getName(); // Name should never start with a forward slash // If we are to register an asset then it must be inside a directory. We need the directory // name to determine what type of asset it belongs to. if (!name.contains(INTERNAL_FILE_SEPARATOR)) { continue; } register(fullPath, name, INTERNAL_FILE_SEPARATOR); } } } public boolean isUseGlslPrefix() { return useGlslPrefix; } public void setUseGlslPrefix(boolean useGlslPrefix) { this.useGlslPrefix = useGlslPrefix; } public FileAsset get(String resourceType, String filename) { String key = resourceType + INTERNAL_FILE_SEPARATOR + filename; if (!filenameToFileAsset.containsKey(key)) { throw new IllegalArgumentException("Unable to find asset for type: " + resourceType + " and path: " + filename); } return filenameToFileAsset.get(key); } public FileAsset getAudio(String filename) { return get(AUDIO_LOCATION, filename); } public FileAsset getFont(String filename) { return get(FONT_LOCATION, filename); } public FileAsset getGui(String filename) { return get(GUI_LOCATION, filename); } public FileAsset getMaterial(String filename) { return get(MATERIAL_LOCATION, filename); } public FileAsset getMesh(String filename) { return get(MESH_LOCATION, filename); } public FileAsset getShader(String filename) { if (useGlslPrefix) { filename = "v" + Fw.config.glslVersion + INTERNAL_FILE_SEPARATOR + filename; } return get(SHADER_LOCATION, filename); } public FileAsset getTexture(String filename) { return get(TEXTURE_LOCATION, filename); } public final void registerAssets(String fullPath, boolean useInternalPath) throws IOException, URISyntaxException { // Remove trailing slash while (fullPath.endsWith(INTERNAL_FILE_SEPARATOR) && (fullPath.length() > 0)) { fullPath = fullPath.substring(0, fullPath.length() - 1); } foundAsset = false; isInternal = useInternalPath; if (isInternal) { registerInternalAssets(fullPath); } else { registerExternalAssets(fullPath); } if (!foundAsset) { throw new RuntimeException("No assets were found for the given file or path: " + fullPath); } } public String trimLeftSlashes(String stringToTrim) { // I don't like this function here but since it is a one off thing I'll leave it for now. while (stringToTrim.startsWith(INTERNAL_FILE_SEPARATOR)) { stringToTrim = stringToTrim.substring(1); } return stringToTrim; } public void unregisterAssets(String fullPath) { Iterator<FileAsset> fileAssetIterator = filenameToFileAsset.values().iterator(); while (fileAssetIterator.hasNext()) { FileAsset fileAsset = fileAssetIterator.next(); String registeredFullPath = fileAsset.getRegisteredFullPath(); if (!registeredFullPath.equals(fullPath)) { continue; } log.debug("Unregistering asset: {}", fileAsset); fileAssetIterator.remove(); } } private static enum FileType { FILE, JAR, ZIP } public static class FileAsset { private final FileType fileType; private final String fullPathToFile; private final String registeredFullPath; private final String relativePath; private FileAsset(FileType fileType, String directory, String filename, String fullPath, String separator) { this.fileType = fileType; this.registeredFullPath = fullPath; switch (fileType) { case FILE: // Fall through case JAR: this.fullPathToFile = fullPath + separator + directory + separator + filename; this.relativePath = null; break; case ZIP: this.fullPathToFile = fullPath; this.relativePath = directory + separator + filename; break; default: throw new IllegalArgumentException("Unknown file type: " + fileType); } } private void advanceStreamToEntryInZipFile(ZipInputStream zip, String path) throws IOException { boolean entryFound = false; // Advance the input stream to the entry of the asset while (true) { ZipEntry entry = zip.getNextEntry(); // If there are no more entries if (entry == null) { break; } // We don't do anything with directories if (entry.isDirectory()) { continue; } String name = entry.getName(); // Name should never start with a forward slash if (name.equals(path)) { entryFound = true; break; } } if (!entryFound) { throw new FileNotFoundException("The path requested: " + path + " in the zip file: " + this.fullPathToFile + " does not exist"); } } public String getFullPathToFile() { return fullPathToFile; } public String getRegisteredFullPath() { return registeredFullPath; } public String getRelativePath() { return relativePath; } public InputStream openStream() throws IOException { switch (fileType) { case FILE: File file = new File(fullPathToFile); return new FileInputStream(file); case JAR: return this.getClass().getResourceAsStream(fullPathToFile); case ZIP: File zipFile = new File(fullPathToFile); FileInputStream fis = new FileInputStream(zipFile); ZipInputStream zip = new ZipInputStream(fis); advanceStreamToEntryInZipFile(zip, relativePath); return zip; default: throw new IllegalArgumentException("Unknown file type: " + fileType); } } public URL toURL() { switch (fileType) { case FILE: try { File file = new File(fullPathToFile); return file.toURI().toURL(); } catch (MalformedURLException ex) { throw new RuntimeException(ex); } case JAR: return this.getClass().getResource(fullPathToFile); case ZIP: throw new UnsupportedOperationException("Not supported"); default: throw new IllegalArgumentException("Unknown file type: " + fileType); } } @Override public String toString() { return "(fileType: " + fileType + " fullPathToFile: " + fullPathToFile + " registeredFullPath: " + registeredFullPath + " relativePath: " + relativePath + ")"; } } }