package games.strategy.engine.framework.headlessGameServer; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.net.URLClassLoader; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.TreeMap; import java.util.concurrent.atomic.AtomicReference; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; import games.strategy.debug.ClientLogger; import games.strategy.engine.ClientFileSystemHelper; import games.strategy.engine.data.GameData; import games.strategy.engine.data.GameParser; import games.strategy.triplea.Constants; import games.strategy.util.UrlStreams; /** * A list of all available games. We make sure we can parse them all, but we don't keep them in memory. */ public class AvailableGames { private static final boolean s_delayedParsing = false; private static final String ZIP_EXTENSION = ".zip"; private final TreeMap<String, URI> m_availableGames = new TreeMap<>(); private final Set<String> m_availableMapFolderOrZipNames = new HashSet<>(); public AvailableGames() { final Set<String> mapNamePropertyList = new HashSet<>(); populateAvailableGames(m_availableGames, m_availableMapFolderOrZipNames, mapNamePropertyList); } public List<String> getGameNames() { return new ArrayList<>(m_availableGames.keySet()); } public Set<String> getAvailableMapFolderOrZipNames() { return new HashSet<>(m_availableMapFolderOrZipNames); } /** * Can return null. */ public GameData getGameData(final String gameName) { return getGameDataFromXML(m_availableGames.get(gameName)); } /** * Returns the path to the file associated with the specified game. * * <p> * The "path" is actually a URI in string form. * </p> * * @param gameName The name of the game whose file path is to be retrieved; may be {@code null}. * * @return The path to the game file; or {@code null} if the game is not available. */ public String getGameFilePath(final String gameName) { return Optional.ofNullable(m_availableGames.get(gameName)).map(Object::toString).orElse(null); } private static void populateAvailableGames(final Map<String, URI> availableGames, final Set<String> availableMapFolderOrZipNames, final Set<String> mapNamePropertyList) { System.out.println("Parsing all available games (this could take a while). "); for (final File map : allMapFiles()) { if (map.isDirectory()) { populateFromDirectory(map, availableGames, availableMapFolderOrZipNames, mapNamePropertyList); } else if (map.isFile() && map.getName().toLowerCase().endsWith(ZIP_EXTENSION)) { populateFromZip(map, availableGames, availableMapFolderOrZipNames, mapNamePropertyList); } } System.out.println("Finished parsing all available game xmls. "); } private static List<File> allMapFiles() { final List<File> rVal = new ArrayList<>(); rVal.addAll(safeListFiles(ClientFileSystemHelper.getUserMapsFolder())); return rVal; } private static List<File> safeListFiles(final File f) { final File[] files = f.listFiles(); if (files == null) { return Collections.emptyList(); } return Arrays.asList(files); } private static void populateFromDirectory(final File mapDir, final Map<String, URI> availableGames, final Set<String> availableMapFolderOrZipNames, final Set<String> mapNamePropertyList) { final File games = new File(mapDir, "games"); if (!games.exists()) { // no games in this map dir return; } for (final File game : games.listFiles()) { if (game.toURI() != null && game.isFile() && game.getName().toLowerCase().endsWith("xml")) { final boolean added = addToAvailableGames(game.toURI(), availableGames, mapNamePropertyList); if (added) { availableMapFolderOrZipNames.add(mapDir.getName()); } } } } private static void populateFromZip(final File map, final Map<String, URI> availableGames, final Set<String> availableMapFolderOrZipNames, final Set<String> mapNamePropertyList) { try ( final FileInputStream fis = new FileInputStream(map); final ZipInputStream zis = new ZipInputStream(fis); final URLClassLoader loader = new URLClassLoader(new URL[] {map.toURI().toURL()});) { ZipEntry entry = zis.getNextEntry(); while (entry != null) { if (entry.getName().contains("games/") && entry.getName().toLowerCase().endsWith(".xml")) { final URL url = loader.getResource(entry.getName()); // we have to close the loader to allow files to be deleted on windows try { final boolean added = addToAvailableGames(new URI(url.toString().replace(" ", "%20")), availableGames, mapNamePropertyList); if (added && map.getName().length() > 4) { availableMapFolderOrZipNames .add(map.getName().substring(0, map.getName().length() - ZIP_EXTENSION.length())); } } catch (final URISyntaxException e) { // only happens when URI couldn't be build and therefore no entry was added. That's fine } } zis.closeEntry(); entry = zis.getNextEntry(); } } catch (final IOException e) { ClientLogger.logQuietly("Map: " + map, e); } } private static boolean addToAvailableGames(final URI uri, final Map<String, URI> availableGames, final Set<String> mapNamePropertyList) { if (uri == null) { return false; } final AtomicReference<String> gameName = new AtomicReference<>(); final Optional<InputStream> inputStream = UrlStreams.openStream(uri); if (inputStream.isPresent()) { try (InputStream input = inputStream.get()) { final GameData data = new GameParser(uri.toString()).parse(input, gameName, s_delayedParsing); final String name = data.getGameName(); final String mapName = data.getProperties().get(Constants.MAP_NAME, ""); if (!availableGames.containsKey(name)) { availableGames.put(name, uri); if (mapName.length() > 0) { mapNamePropertyList.add(mapName); } return true; } } catch (final Exception e) { ClientLogger.logError("Exception while parsing: " + uri.toString() + " : " + (gameName.get() != null ? gameName.get() + " : " : ""), e); } } return false; } private static GameData getGameDataFromXML(final URI uri) { if (uri == null) { return null; } final AtomicReference<String> gameName = new AtomicReference<>(); final Optional<InputStream> inputStream = UrlStreams.openStream(uri); if (inputStream.isPresent()) { try (InputStream input = inputStream.get()) { return new GameParser(uri.toString()).parse(input, gameName, false); } catch (final Exception e) { ClientLogger.logError("Exception while parsing: " + uri.toString() + " : " + (gameName.get() != null ? gameName.get() + " : " : ""), e); } } return null; } }