package games.strategy.triplea;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.StringTokenizer;
import games.strategy.debug.ClientLogger;
import games.strategy.engine.ClientFileSystemHelper;
import games.strategy.engine.framework.map.download.DownloadMapsWindow;
import games.strategy.engine.framework.startup.launcher.MapNotFoundException;
import games.strategy.ui.SwingComponents;
import games.strategy.util.UrlStreams;
/**
* Utility for managing where images and property files for maps and units should be loaded from.
* Based on java Classloaders.
*/
public class ResourceLoader implements Closeable {
private final URLClassLoader m_loader;
public static String RESOURCE_FOLDER = "assets";
private final ResourceLocationTracker resourceLocationTracker;
public static ResourceLoader getGameEngineAssetLoader() {
return getMapResourceLoader("");
}
/**
* Returns a resource loader that will find assets in a map directory.
*/
public static ResourceLoader getMapResourceLoader(final String mapName) {
File atFolder = ClientFileSystemHelper.getRootFolder();
File resourceFolder = new File(atFolder, RESOURCE_FOLDER);
while (!resourceFolder.exists() && !resourceFolder.isDirectory()) {
atFolder = atFolder.getParentFile();
resourceFolder = new File(atFolder, RESOURCE_FOLDER);
}
final List<String> dirs = getPaths(mapName);
if (mapName != null && dirs.isEmpty()) {
SwingComponents.promptUser("Download Map?",
"Map missing: " + mapName + ", could not join game.\nWould you like to download the map now?"
+ "\nOnce the download completes, you may reconnect to this game.",
() -> DownloadMapsWindow.showDownloadMapsWindow(mapName));
throw new MapNotFoundException();
}
dirs.add(resourceFolder.getAbsolutePath());
ClientLogger.logQuietly("Loading resources from the following paths: " + dirs);
return new ResourceLoader(mapName, dirs.toArray(new String[dirs.size()]));
}
protected static String normalizeMapZipName(final String zipName) {
final StringBuilder sb = new StringBuilder();
Character lastChar = null;
final String spacesReplaced = zipName.replace(' ', '_');
for (final char c : spacesReplaced.toCharArray()) {
// break up camel casing
if (lastChar != null && Character.isLowerCase(lastChar) && Character.isUpperCase(c)) {
sb.append("_");
}
sb.append(Character.toLowerCase(c));
lastChar = c;
}
return sb.toString();
}
private static List<String> getPaths(final String mapName) {
if (mapName == null) {
return new ArrayList<>();
}
// find the primary directory/file
final String dirName = File.separator + mapName;
final String zipName = dirName + ".zip";
final List<File> candidates = new ArrayList<>();
// prioritize user maps folder over root folder
candidates.add(new File(ClientFileSystemHelper.getUserMapsFolder(), dirName + File.separator + "map"));
candidates.add(new File(ClientFileSystemHelper.getUserMapsFolder(), dirName));
candidates.add(new File(ClientFileSystemHelper.getUserMapsFolder(), zipName));
final String normalizedZipName = normalizeMapZipName(zipName);
// clicking github 'clone or download' and downloading the zip gives a zip that ends with "-master.zip"
candidates.add(new File(ClientFileSystemHelper.getUserMapsFolder(), normalizeMapZipName(mapName) + "-master.zip"));
candidates.add(new File(ClientFileSystemHelper.getUserMapsFolder(), normalizedZipName));
final Optional<File> match = candidates.stream().filter(file -> file.exists()).findFirst();
if (!match.isPresent()) {
// if we get no results, we will eventually prompt the user to download the map
return new ArrayList<>();
}
ClientLogger.logQuietly("Loading map: " + mapName + ", from: " + match.get().getAbsolutePath());
final List<String> rVal = new ArrayList<>();
rVal.add(match.get().getAbsolutePath());
// find dependencies
try (final URLClassLoader url = new URLClassLoader(new URL[] {match.get().toURI().toURL()})) {
final URL dependencesURL = url.getResource("dependencies.txt");
if (dependencesURL != null) {
final java.util.Properties dependenciesFile = new java.util.Properties();
final Optional<InputStream> inputStream = UrlStreams.openStream(dependencesURL);
if (inputStream.isPresent()) {
try (final InputStream stream = inputStream.get()) {
dependenciesFile.load(stream);
final String dependencies = dependenciesFile.getProperty("dependencies");
final StringTokenizer tokens = new StringTokenizer(dependencies, ",", false);
while (tokens.hasMoreTokens()) {
// add the dependencies recursivly
rVal.addAll(getPaths(tokens.nextToken()));
}
}
}
}
} catch (final Exception e) {
ClientLogger.logQuietly(e);
throw new IllegalStateException(e.getMessage());
}
return rVal;
}
private ResourceLoader(final String mapName, final String[] paths) {
final URL[] urls = new URL[paths.length];
for (int i = 0; i < paths.length; i++) {
final File f = new File(paths[i]);
if (!f.exists()) {
ClientLogger.logQuietly(f + " does not exist");
}
if (!f.isDirectory() && !f.getName().endsWith(".zip")) {
ClientLogger.logQuietly(f + " is not a directory or a zip file");
}
try {
urls[i] = f.toURI().toURL();
} catch (final MalformedURLException e) {
ClientLogger.logQuietly(e);
throw new IllegalStateException(e.getMessage());
}
}
resourceLocationTracker = new ResourceLocationTracker(mapName, urls);
// Note: URLClassLoader does not always respect the ordering of the search URLs
// To solve this we will get all matching paths and then filter by what matched
// the assets folder.
m_loader = new URLClassLoader(urls);
}
@Override
public void close() {
try {
m_loader.close();
} catch (final IOException e) {
ClientLogger.logQuietly(e);
}
}
public boolean hasPath(final String path) {
final URL rVal = m_loader.getResource(path);
return rVal != null;
}
/**
* @param inputPath
* (The name of a resource is a '/'-separated path name that identifies the resource. Do not use '\' or
* File.separator)
*/
public URL getResource(final String inputPath) {
String path = resourceLocationTracker.getMapPrefix() + inputPath;
return getMatchingResources(path).stream().findFirst().orElse(
getMatchingResources(inputPath).stream().findFirst().orElseGet(
() -> null));
}
private List<URL> getMatchingResources(final String path) {
try {
return Collections.list(m_loader.getResources(path));
} catch (final IOException e) {
throw new IllegalStateException(e);
}
}
/**
* Ensure that you close the InputStream returned by this method when you are done using it.
*/
public InputStream getResourceAsStream(final String path) {
final URL url = getResource(path);
if (url == null) {
return null;
}
final Optional<InputStream> inputStream = UrlStreams.openStream(url);
if (inputStream.isPresent()) {
return inputStream.get();
} else {
throw new IllegalStateException("Failed to open an input stream to: " + path);
}
}
}