package games.strategy.engine.framework.ui;
import java.awt.Component;
import java.io.File;
import java.io.IOException;
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.Enumeration;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import javax.swing.DefaultListModel;
import javax.swing.JOptionPane;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import com.google.common.collect.Sets;
import games.strategy.debug.ClientLogger;
import games.strategy.engine.ClientFileSystemHelper;
import games.strategy.engine.data.EngineVersionException;
import games.strategy.engine.data.GameParseException;
import games.strategy.engine.framework.startup.ui.MainFrame;
import games.strategy.ui.SwingAction;
public class NewGameChooserModel extends DefaultListModel<NewGameChooserEntry> {
private static final long serialVersionUID = -2044689419834812524L;
private enum ZipProcessingResult {
SUCCESS, ERROR
}
public NewGameChooserModel() {
final Set<NewGameChooserEntry> parsedMapSet = parseMapFiles();
final List<NewGameChooserEntry> entries = new ArrayList<>(parsedMapSet);
Collections.sort(entries, NewGameChooserEntry.getComparator());
for (final NewGameChooserEntry entry : entries) {
addElement(entry);
}
}
@Override
public NewGameChooserEntry get(final int i) {
return super.get(i);
}
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 Set<NewGameChooserEntry> parseMapFiles() {
final Set<NewGameChooserEntry> parsedMapSet = Sets.newHashSet();
for (final File map : allMapFiles()) {
if (map.isDirectory()) {
parsedMapSet.addAll(populateFromDirectory(map));
} else if (map.isFile() && map.getName().toLowerCase().endsWith(".zip")) {
parsedMapSet.addAll(populateFromZip(map));
}
}
return parsedMapSet;
}
private static List<NewGameChooserEntry> populateFromZip(final File map) {
boolean badMapZip = false;
final List<NewGameChooserEntry> entries = new ArrayList<>();
try (ZipFile zipFile = new ZipFile(map);
final URLClassLoader loader = new URLClassLoader(new URL[] {map.toURI().toURL()})) {
final Enumeration<? extends ZipEntry> zipEntryEnumeration = zipFile.entries();
while (zipEntryEnumeration.hasMoreElements()) {
final ZipEntry entry = zipEntryEnumeration.nextElement();
if (entry.getName().contains("games/") && entry.getName().toLowerCase().endsWith(".xml")) {
final ZipProcessingResult result = processZipEntry(loader, entry, entries);
if (result == ZipProcessingResult.ERROR) {
badMapZip = true;
break;
}
}
}
} catch (final IOException e) {
confirmWithUserAndThenDeleteCorruptZipFile(map, Optional.of(e.getMessage()));
}
if (badMapZip) {
confirmWithUserAndThenDeleteCorruptZipFile(map, Optional.empty());
}
return entries;
}
private static ZipProcessingResult processZipEntry(final URLClassLoader loader, final ZipEntry entry,
final List<NewGameChooserEntry> entries) {
final URL url = loader.getResource(entry.getName());
if (url == null) {
// not loading the URL means the XML is truncated or otherwise in bad shape
return ZipProcessingResult.ERROR;
}
try {
addNewGameChooserEntry(entries, new URI(url.toString().replace(" ", "%20")));
} catch (final URISyntaxException e) {
// only happens when URI couldn't be build and therefore no entry was added. That's fine ..
}
return ZipProcessingResult.SUCCESS;
}
/*
* Open up a confirmation dialog, if user says yes, delete the map specified by
* parameter, then show confirmation of deletion.
*/
private static void confirmWithUserAndThenDeleteCorruptZipFile(final File map, final Optional<String> errorDetails) {
final Runnable deleteMapRunnable = () -> {
final Component parentComponent = MainFrame.getInstance();
String message = "Could not parse map file correctly, would you like to remove it?\n" + map.getAbsolutePath()
+ "\n(You may see this error message again if you keep the file)";
String title = "Corrup Map File Found";
final int optionType = JOptionPane.YES_NO_OPTION;
int messageType = JOptionPane.WARNING_MESSAGE;
final int result = JOptionPane.showConfirmDialog(parentComponent, message, title, optionType, messageType);
if (result == JOptionPane.YES_OPTION) {
final boolean deleted = map.delete();
if (deleted) {
messageType = JOptionPane.INFORMATION_MESSAGE;
message = "File was deleted successfully.";
} else if (!deleted && map.exists()) {
message = "Unable to delete file, please remove it in the file system and restart tripleA:\n" + map
.getAbsolutePath();
if (errorDetails.isPresent()) {
message += "\nError details: " + errorDetails;
}
}
title = "File Removal Result";
JOptionPane.showMessageDialog(parentComponent, message, title, messageType);
}
};
SwingAction.invokeAndWait(deleteMapRunnable);
}
/**
* @param entries
* list of entries where to add the new entry.
* @param uri
* URI of the new entry
*/
private static void addNewGameChooserEntry(final List<NewGameChooserEntry> entries, final URI uri) {
try {
final NewGameChooserEntry newEntry = createEntry(uri);
if (newEntry != null && !entries.contains(newEntry)) {
entries.add(newEntry);
}
} catch (final EngineVersionException e) {
System.out.println(e.getMessage());
} catch (final SAXParseException e) {
final String msg =
"Could not parse:" + uri + " error at line:" + e.getLineNumber() + " column:" + e.getColumnNumber();
System.err.println(msg);
ClientLogger.logQuietly(e);
} catch (final Exception e) {
System.err.println("Could not parse:" + uri);
ClientLogger.logQuietly(e);
}
}
public NewGameChooserEntry findByName(final String name) {
for (int i = 0; i < size(); i++) {
if (get(i).getGameData().getGameName().equals(name)) {
return get(i);
}
}
return null;
}
private static NewGameChooserEntry createEntry(final URI uri)
throws IOException, GameParseException, SAXException, EngineVersionException {
return new NewGameChooserEntry(uri);
}
private static List<NewGameChooserEntry> populateFromDirectory(final File mapDir) {
final List<NewGameChooserEntry> entries = new ArrayList<>();
// use contents under a "mapDir/map" folder if present, otherwise use the "mapDir/" contents directly
final File mapFolder = new File(mapDir, "map");
final File parentFolder = mapFolder.exists() ? mapFolder : mapDir;
final File games = new File(parentFolder, "games");
if (!games.exists()) {
// no games in this map dir
return entries;
}
for (final File game : games.listFiles()) {
if (game.isFile() && game.getName().toLowerCase().endsWith("xml")) {
addNewGameChooserEntry(entries, game.toURI());
}
}
return entries;
}
public boolean removeEntry(final NewGameChooserEntry entryToBeRemoved) {
return this.removeElement(entryToBeRemoved);
}
}