package org.mctourney.autoreferee;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.lang.reflect.Type;
import java.net.URL;
import java.util.Date;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.World;
import org.bukkit.WorldCreator;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.scheduler.BukkitRunnable;
import org.mctourney.autoreferee.event.match.MatchLoadEvent;
import org.mctourney.autoreferee.util.NullChunkGenerator;
import org.mctourney.autoreferee.util.QueryUtil;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.jdom2.Element;
import org.jdom2.JDOMException;
import org.jdom2.Namespace;
import org.jdom2.input.SAXBuilder;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
/**
* Represents a map object, available to be loaded by AutoReferee.
*
* @author authorblues
*/
public class AutoRefMap implements Comparable<AutoRefMap>
{
private static final File DOWNLOADING = new File("");
private static Set<AutoRefMap> _cachedRemoteMaps = null;
private static final long REMOTE_MAP_CACHE_LENGTH = 5000L;
private static long _cachedRemoteMapsTime = 0L;
private String name;
private String version;
private File zip = null;
private String filename;
private String md5sum;
protected AutoRefMap(String name, String version, File zip)
throws IOException
{
this.name = name; this.version = version; this.zip = zip;
this.md5sum = DigestUtils.md5Hex(new FileInputStream(zip));
}
protected AutoRefMap(String name, String version, String filename, String md5sum)
{
this.name = name; this.version = version;
this.filename = filename; this.md5sum = md5sum;
}
/**
* Gets the name of the map
*
* @return map name
*/
public String getName()
{ return name; }
/**
* Gets the version of the map
*
* @return map version
*/
public String getVersion()
{ return version; }
/**
* Gets human-readable name of map, including version number.
*
* @return map version string
*/
public String getVersionString()
{ return name + " v" + version; }
/**
* Gets whether the map has been installed.
*
* @return true if map is installed, otherwise false
*/
public boolean isInstalled()
{ return this.zip != null; }
/**
* Gets root zip for this map, downloading if necessary.
*
* @return root zip for map
* @throws IOException if map download fails
*/
public File getZip() throws IOException
{
boolean interrupted = false;
// if the map is already downloading, just be patient
while (this.zip == DOWNLOADING)
{
interrupted = Thread.interrupted();
try { Thread.sleep(500); }
catch (InterruptedException e) { interrupted = true; }
}
if (interrupted)
Thread.currentThread().interrupt();
if (!isInstalled()) download();
return this.zip;
}
private void download() throws IOException
{
// mark the file as downloading
this.zip = AutoRefMap.DOWNLOADING;
String bparts[] = filename.split("/"), basename = bparts[bparts.length - 1];
File zip = new File(AutoRefMap.getMapLibrary(), basename);
FileUtils.copyURLToFile(new URL(AutoRefMatch.getMapRepo() + filename), zip);
// if the md5s match, return the zip
String md5comp = DigestUtils.md5Hex(new FileInputStream(zip));
if (md5comp.equalsIgnoreCase(md5sum)) { this.zip = zip; return; }
// if the md5sum did not match, quit here
zip.delete(); throw new IOException("MD5 Mismatch: " + md5comp + " != " + md5sum);
}
/**
* Installs the map if it is not already installed.
*/
public void install()
{
// runnable downloads the map if it isn't installed
BukkitRunnable runnable = new BukkitRunnable()
{
@Override public void run()
{ try { getZip(); } catch (IOException e) { e.printStackTrace(); } }
};
// run the task asynchronously to avoid locking up the main thread on a download
runnable.runTaskAsynchronously(AutoReferee.getInstance());
}
@Override
public int hashCode()
{ return name.toLowerCase().hashCode(); }
@Override
public boolean equals(Object o)
{
if (!(o instanceof AutoRefMap)) return false;
AutoRefMap map = (AutoRefMap) o;
return name.equalsIgnoreCase(map.name);
}
@Override
public int compareTo(AutoRefMap other)
{ return name.compareTo(other.name); }
public static int compareVersionStrings(String v1, String v2)
{
return 0;
}
/**
* Creates match object given map name and an optional custom world name.
*
* @param map name of map, to be downloaded if necessary
* @param world custom name for world folder, or null
*
* @return match object for the loaded world
* @throws IOException if map download fails
*/
public static AutoRefMatch createMatch(String map, String world) throws IOException
{ return createMatch(AutoRefMap.getMap(map), world); }
/**
* Creates match object given map name and an optional custom world name.
*
* @param map map object, to be downloaded if necessary
* @param world custom name for world folder, or null
*
* @return match object for the loaded world
* @throws IOException if map download fails
*/
public static AutoRefMatch createMatch(AutoRefMap map, String world) throws IOException
{
World w = createMatchWorld(map, world);
return AutoReferee.getInstance().getMatch(w).temporary();
}
private static World createMatchWorld(AutoRefMap map, String world) throws IOException
{
if (world == null)
world = AutoReferee.WORLD_PREFIX + Long.toHexString(new Date().getTime());
// copy the files over and return the loaded world
map.unpack(new File(world));
return AutoReferee.getInstance().getServer().createWorld(
WorldCreator.name(world).generateStructures(false)
.generator(new NullChunkGenerator()));
}
/**
* Handles JSON object to initialize matches.
* @param json match parameters to be loaded
*
* @return true if matches were loaded, otherwise false
* @see AutoRefMatch.MatchParams
*/
public static boolean parseMatchInitialization(String json) // TODO
{
Type type = new TypeToken<List<AutoRefMatch.MatchParams>>() {}.getType();
List<AutoRefMatch.MatchParams> paramList = new Gson().fromJson(json, type);
try
{
// for each match in the list, go ahead and create the match
for (AutoRefMatch.MatchParams params : paramList) createMatch(params);
}
catch (IOException e) { return false; }
return true;
}
/**
* Generates a match from the given match parameters.
*
* @param params match parameters object
* @return generated match object
* @throws IOException if map download fails
*/
public static AutoRefMatch createMatch(AutoRefMatch.MatchParams params) throws IOException
{
AutoRefMatch m = AutoRefMap.createMatch(params.getMap(), null);
Iterator<AutoRefTeam> teamiter = m.getTeams().iterator();
for (AutoRefMatch.MatchParams.TeamInfo teaminfo : params.getTeams())
{
if (!teamiter.hasNext()) break;
AutoRefTeam team = teamiter.next();
team.setName(teaminfo.getName());
for (String name : teaminfo.getPlayers())
team.addExpectedPlayer(name);
}
return m;
}
/**
* Gets root folder of map library, generating folder if necessary.
*
* @return root folder of map library
*/
public static File getMapLibrary()
{
// maps library is a folder called `maps/`
File m = new File("maps");
// if it doesn't exist, make the directory
if (m.exists() && !m.isDirectory()) m.delete();
if (!m.exists()) m.mkdir();
// return the maps library
return m;
}
private static final int MAX_NAME_DISTANCE = 5;
/**
* Gets map object associated with given map name.
*
* @param name name of map
* @return map object associated with the name
*/
public static AutoRefMap getMap(String name)
{
// assume worldName exists
if (name == null) return null;
name = AutoRefMatch.normalizeMapName(name);
// if there is no map library, quit
File mapLibrary = AutoRefMap.getMapLibrary();
if (!mapLibrary.exists()) return null;
AutoRefMap bmap = null; int ldist = MAX_NAME_DISTANCE;
for (AutoRefMap map : getAvailableMaps())
{
String mapName = AutoRefMatch.normalizeMapName(map.name);
int namedist = StringUtils.getLevenshteinDistance(name, mapName);
if (namedist <= ldist) { bmap = map; ldist = namedist; }
}
// get best match
return bmap;
}
/**
* Gets map object associated with the zip file at the provided URL.
*
* @param url URL of map zip to be downloaded.
* @return generated map object
* @throws IOException if map cannot be unpackaged
*/
public static AutoRefMap getMapFromURL(String url) throws IOException
{
String filename = url.substring(url.lastIndexOf('/') + 1, url.length());
File zip = new File(AutoRefMap.getMapLibrary(), filename);
FileUtils.copyURLToFile(new URL(url), zip);
return AutoRefMap.getMapInfo(zip);
}
/**
* Gets all maps available to be loaded.
*
* @return Set of all maps available to be loaded
*/
public static Set<AutoRefMap> getAvailableMaps()
{
Set<AutoRefMap> maps = Sets.newHashSet();
maps.addAll(getInstalledMaps());
maps.addAll(getRemoteMaps());
return maps;
}
private static class MapUpdateTask extends BukkitRunnable
{
private CommandSender sender;
private boolean force;
public MapUpdateTask(CommandSender sender, boolean force)
{ this.sender = sender; this.force = force; }
@Override
public void run()
{
// get remote map directory
Map<String, AutoRefMap> remote = Maps.newHashMap();
for (AutoRefMap map : getRemoteMaps()) remote.put(map.name, map);
for (File folder : AutoRefMap.getMapLibrary().listFiles())
if (folder.isDirectory()) try
{
File arxml = new File(folder, AutoReferee.CFG_FILENAME);
if (!arxml.exists()) continue;
Element arcfg = new SAXBuilder().build(arxml).getRootElement();
Element meta = arcfg.getChild("meta");
if (meta != null)
{
AutoRefMap rmap = remote.get(AutoRefMatch.normalizeMapName(meta.getChildText("name")));
if (rmap != null && rmap.getZip() != null)
{
FileUtils.deleteQuietly(folder);
AutoReferee.getInstance().sendMessageSync(sender, String.format(
"Updated %s to new format (%s)", rmap.name, rmap.getVersionString()));
}
}
}
catch (IOException e) { e.printStackTrace(); }
catch (JDOMException e) { e.printStackTrace(); }
// check for updates on installed maps
for (AutoRefMap map : getInstalledMaps()) try
{
// get the remote version and check if there is an update
AutoRefMap rmap; if ((rmap = remote.get(map.name)) != null)
{
boolean needsUpdate = map.md5sum != null && !map.md5sum.equals(rmap.md5sum);
if (force || needsUpdate)
{
AutoReferee.getInstance().sendMessageSync(sender, String.format(
"UPDATING %s (%s -> %s)...", rmap.name, map.version, rmap.version));
if (rmap.getZip() == null) AutoReferee.getInstance()
.sendMessageSync(sender, "Update " + ChatColor.RED + "FAILED");
else
{
AutoReferee.getInstance().sendMessageSync(sender,
"Update " + ChatColor.GREEN + "SUCCESS: " +
ChatColor.RESET + rmap.getVersionString());
FileUtils.deleteQuietly(map.getZip());
}
}
}
}
catch (IOException e) { e.printStackTrace(); }
}
}
/**
* Downloads updates to all maps installed on the server.
*
* @param sender user receiving progress updates
* @param force force re-download of maps, irrespective of version
*/
public static void getUpdates(CommandSender sender, boolean force)
{ new MapUpdateTask(sender, force).runTaskAsynchronously(AutoReferee.getInstance()); }
/**
* Gets maps that are not installed, but may be downloaded.
*
* @return set of all maps available for download
*/
public static Set<AutoRefMap> getRemoteMaps()
{
// check the cache first, we might want to just use the cached value
long time = ManagementFactory.getRuntimeMXBean().getUptime() - _cachedRemoteMapsTime;
if (_cachedRemoteMaps != null && time < AutoRefMap.REMOTE_MAP_CACHE_LENGTH) return _cachedRemoteMaps;
Set<AutoRefMap> maps = Sets.newHashSet();
String repo = AutoRefMatch.getMapRepo();
try
{
Map<String, String> params = Maps.newHashMap();
params.put("prefix", "maps/");
for (;;)
{
String url = String.format("%s?%s", repo, QueryUtil.prepareParams(params));
Element listing = new SAXBuilder().build(new URL(url)).getRootElement();
assert "ListBucketResult".equals(listing.getName()) : "Unexpected response";
Namespace ns = listing.getNamespace();
String lastkey = null;
for (Element entry : listing.getChildren("Contents", ns))
{
lastkey = entry.getChildTextTrim("Key", ns);
if (!lastkey.endsWith(".zip")) continue;
String[] keyparts = lastkey.split("/");
String mapfile = keyparts[keyparts.length - 1];
String mapslug = mapfile.substring(0, mapfile.length() - 4);
String slugparts[] = mapslug.split("-v");
if (slugparts.length < 2)
{
AutoReferee.log("Invalid map filename: " + mapfile, Level.WARNING);
AutoReferee.log("Map files should be of the form \"MapName-vX.X.zip\"", Level.WARNING);
}
else
{
String etag = entry.getChildTextTrim("ETag", ns);
maps.add(new AutoRefMap(slugparts[0], slugparts[1],
lastkey, etag.substring(1, etag.length() - 1)));
}
}
// stop looping if the result says that it hasn't been truncated (no more results)
if (!Boolean.parseBoolean(listing.getChildText("IsTruncated", ns))) break;
// use the last key as a marker for the next pass
if (lastkey != null) params.put("marker", lastkey);
}
}
catch (IOException e) { e.printStackTrace(); }
catch (JDOMException e) { e.printStackTrace(); }
_cachedRemoteMapsTime = ManagementFactory.getRuntimeMXBean().getUptime();
return _cachedRemoteMaps = maps;
}
/**
* Gets maps installed locally on server.
*
* @return set of all maps available to load immediately
*/
public static Set<AutoRefMap> getInstalledMaps()
{
Set<AutoRefMap> maps = Sets.newHashSet();
// look through the zip files for what's already installed
for (File zip : AutoRefMap.getMapLibrary().listFiles())
{
AutoRefMap mapInfo = getMapInfo(zip);
if (mapInfo != null) maps.add(mapInfo);
}
return maps;
}
public static Element getConfigFileData(File zip) throws IOException, JDOMException
{
try
{
ZipFile zfile = new ZipFile(zip);
Enumeration<? extends ZipEntry> entries = zfile.entries();
// if it doesn't have an autoreferee config file
while (entries.hasMoreElements())
{
ZipEntry entry = entries.nextElement();
if (entry.getName().endsWith(AutoReferee.CFG_FILENAME))
return new SAXBuilder().build(zfile.getInputStream(entry)).getRootElement();
}
}
catch (Exception e)
{
// if the zip file is malformed in some way, tell which file caused the problem
AutoReferee.log("Error opening or processing " + zip.getName() + ": " + e.getMessage());
}
return null;
}
/**
* Get map info object associated with a zip
*
* @param zip zip file containing a configuration file
* @return map info object if zip contains a map, otherwise null
*/
public static AutoRefMap getMapInfo(File zip)
{
// skip non-directories
if (zip.isDirectory()) return null;
Element worldConfig;
try { worldConfig = getConfigFileData(zip); }
catch (IOException e) { e.printStackTrace(); return null; }
catch (JDOMException e) { e.printStackTrace(); return null; }
String mapName = "??", version = "1.0";
Element meta = worldConfig.getChild("meta");
if (meta != null)
{
mapName = AutoRefMatch.normalizeMapName(meta.getChildText("name"));
version = meta.getChildText("version");
}
try { return new AutoRefMap(mapName, version, zip); }
catch (IOException e) { e.printStackTrace(); return null; }
}
private File unpack(File dest) throws IOException
{
ZipFile zfile = new ZipFile(this.getZip());
Enumeration<? extends ZipEntry> entries = zfile.entries();
File f, basedir = null;
File tmp = FileUtils.getTempDirectory();
while (entries.hasMoreElements())
{
ZipEntry entry = entries.nextElement();
f = new File(tmp, entry.getName());
if (f.exists()) FileUtils.deleteQuietly(f);
if (entry.isDirectory()) FileUtils.forceMkdir(f);
else FileUtils.copyInputStreamToFile(zfile.getInputStream(entry), f);
if (entry.isDirectory() && (basedir == null ||
basedir.getName().startsWith(f.getName()))) basedir = f;
}
zfile.close();
if (dest.exists()) FileUtils.deleteDirectory(dest);
FileUtils.moveDirectory(basedir, dest);
return dest;
}
private static abstract class MapDownloader extends BukkitRunnable
{
protected CommandSender sender;
private String custom;
public MapDownloader(CommandSender sender, String custom)
{ this.sender = sender; this.custom = custom; }
public void loadMap(AutoRefMap map)
{
try
{
if (!map.isInstalled()) map.download();
new MapLoader(sender, map, custom).runTask(AutoReferee.getInstance());
}
catch (IOException e) { e.printStackTrace(); }
}
}
private static class MapRepoDownloader extends MapDownloader
{
private AutoRefMap map;
private String name = null;
public MapRepoDownloader(CommandSender sender, String mapName, String custom)
{ this(sender, AutoRefMap.getMap(mapName), custom); this.name = mapName; }
public MapRepoDownloader(CommandSender sender, AutoRefMap map, String custom)
{ super(sender, custom); this.map = map; }
@Override
public void run()
{
if (this.map == null)
{
AutoReferee plugin = AutoReferee.getInstance();
plugin.sendMessageSync(sender, "No such map: " + this.name);
}
else this.loadMap(this.map);
}
}
private static class MapURLDownloader extends MapDownloader
{
private String url;
public MapURLDownloader(CommandSender sender, String url, String custom)
{ super(sender, custom); this.url = url; }
@Override
public void run()
{
AutoRefMap map = null;
try { map = AutoRefMap.getMapFromURL(this.url); } catch (IOException e) { e.printStackTrace(); }
if (map == null) AutoReferee.getInstance()
.sendMessageSync(sender, "Could not load map from URL: " + this.url);
else this.loadMap(map);
}
}
private static class MapLoader extends BukkitRunnable
{
private CommandSender sender;
private AutoRefMap map;
private String custom;
public MapLoader(CommandSender sender, AutoRefMap map, String custom)
{ this.sender = sender; this.map = map; this.custom = custom; }
@Override
public void run()
{
AutoRefMatch match;
try { match = AutoRefMap.createMatch(this.map, this.custom); }
catch (IOException e) { e.printStackTrace(); return; }
MatchLoadEvent event = new MatchLoadEvent(match);
AutoReferee.callEvent(event);
AutoReferee plugin = AutoReferee.getInstance();
AutoReferee.log(String.format("%s loaded %s (%s)", sender.getName(),
match.getVersionString(), match.getWorld().getName()));
sender.sendMessage(ChatColor.DARK_GRAY + match.getVersionString() + " setup!");
if (sender instanceof Player) match.joinMatch((Player) sender);
if (sender == Bukkit.getConsoleSender()) plugin.setConsoleWorld(match.getWorld());
}
}
/**
* Loads a map by name.
*
* @param sender user receiving progress updates
* @param name name of map to be loaded
* @param worldname name of custom world folder, possibly null
*/
public static void loadMap(CommandSender sender, String name, String worldname)
{
sender.sendMessage(ChatColor.GREEN + "Loading map: " + name);
new MapRepoDownloader(sender, name, worldname)
.runTaskAsynchronously(AutoReferee.getInstance());
}
/**
* Loads a map by name.
*
* @param sender user receiving progress updates
* @param map map to be loaded
* @param worldname name of custom world folder, possibly null
*/
public static void loadMap(CommandSender sender, AutoRefMap map, String worldname)
{
sender.sendMessage(ChatColor.GREEN + "Loading map: " + map.getVersionString());
new MapRepoDownloader(sender, map, worldname)
.runTaskAsynchronously(AutoReferee.getInstance());
}
/**
* Loads a map by URL.
*
* @param sender user receiving progress updates
* @param url URL of map zip to be downloaded
* @param worldname name of custom world folder, possibly null
*/
public static void loadMapFromURL(CommandSender sender, String url, String worldname)
{
sender.sendMessage(ChatColor.GREEN + "Loading map from URL...");
new MapURLDownloader(sender, url, worldname)
.runTaskAsynchronously(AutoReferee.getInstance());
}
}