package eu.irreality.age.swing.newloader; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.net.MalformedURLException; import java.net.URL; import java.net.URLConnection; import java.nio.channels.Channels; import java.nio.channels.ReadableByteChannel; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import eu.irreality.age.filemanagement.Paths; import eu.irreality.age.i18n.UIMessages; import eu.irreality.age.swing.newloader.download.DownloadUtil; import eu.irreality.age.swing.newloader.download.ProgressKeepingDelegate; import eu.irreality.age.swing.newloader.download.ProgressKeepingReadableByteChannel; import eu.irreality.age.swing.newloader.download.Unzipper; public class GameResource { /** * The local path of the game resource, relative to the AGE worlds directory. Either this or localAbsolutePath must be non-null. */ private String localRelativePath; /** * The absolute path of the game resource. Either this or localRelativePath must be non-null. */ private String localAbsolutePath; /** * If this is not null, then it's the path to store the zipfile downloaded from the remoteURL * and the local relative path stores the path of the main resource inside that zipfile (which may * be in some subdirectory extracted from the zipfile). */ private String zipfileRelativePath; private URL localURL; private URL remoteURL; private boolean downloaded; private boolean downloadInProgress; /**Path to the local directory containing world files and world resources.*/ private static String pathToWorlds; /** * Obtains the absolute path to the local directory containing world files and world resources. * @return */ private static String getPathToWorlds() { if ( pathToWorlds == null ) { File cwd = new File ( Paths.getWorkingDirectory() ); pathToWorlds = cwd.getAbsolutePath() + File.separatorChar + Paths.WORLD_PATH; } return pathToWorlds; } /** * @return the local absolute path of the game resource. */ public File getLocalPath() { if ( localRelativePath != null ) return new File(getPathToWorlds(),localRelativePath); else return new File(localAbsolutePath); } /** * @return the local absolute URL of the game resource. */ public URL getLocalURL() { return localURL; } /** * @return The string indicating the local relative path to the resource. */ public String getLocalRelativePath() { return localRelativePath; } /** * @return The string indicating the full relative path to store the zipfile downloaded from the server, that we will * need to unzip in order to get the game resource. */ public String getZipfileRelativePath() { if ( zipfileRelativePath != null ) return zipfileRelativePath; else if ( localRelativePath == null ) return null; //this shouldn't happen, we shouldn't be asking for the zipfile path of a file in this situation else return localRelativePath + ".zip"; //example: vampiro/world.xml.zip } /** * @return the local absolute path to store the zipfile downloaded from the server, that we will need to unzip in order to * get the game resource. */ public File getZipfilePath() { return new File(getPathToWorlds(),getZipfileRelativePath()); } /** * @return the remote URL of the game resource. */ public URL getRemoteURL() { return remoteURL; } /** * @return whether the remote resource has been downloaded or not. */ public boolean isDownloaded() { return downloaded; } /** * Whether the download of the remote resource is currently in progress. */ public boolean isDownloadInProgress() { return downloadInProgress; } /** * Sets or unsets the flag saying that the resource's download is in progress. */ public void setDownloadInProgress ( boolean newValue ) { downloadInProgress = newValue; } /** * @param sets the remote resource as downloaded (or not). */ public void setDownloaded(boolean downloaded) { this.downloaded = downloaded; } /** * Sets the local absolute path to the resource. * This is used to build a game resource from a local disk file. * @param thePath */ public void setLocalAbsolutePath ( String thePath ) { localAbsolutePath = thePath; } /** * Gets the information about a game resource from an XML node. * @param n * @throws MalformedGameEntryException */ public void initFromXML ( Node n ) throws MalformedGameEntryException { try { Element e = (Element) n; if ( !e.hasAttribute("local") && !e.hasAttribute("localAbsolute") ) throw new MalformedGameEntryException("Game resource entry missing local path (attribute local or localAbsolute)"); else if ( e.hasAttribute("local") ) { URL localWorldsURL = new File(getPathToWorlds()).toURI().toURL(); localURL = new URL(localWorldsURL,e.getAttribute("local")); localRelativePath = e.getAttribute("local"); } else //has attribute localAbsolute { localAbsolutePath = e.getAttribute("localAbsolute"); localURL = new File(localAbsolutePath).toURI().toURL(); if ( e.hasAttribute("zip") || e.hasAttribute("remote") ) throw new MalformedGameEntryException("Malformed game resource: a resource without local relative path (attribute local) cannot have attributes zip or remote"); } if ( e.hasAttribute("zip") ) zipfileRelativePath = e.getAttribute("zip"); if ( e.hasAttribute("remote") ) remoteURL = new URL(e.getAttribute("remote")); if ( e.hasAttribute("downloaded") ) downloaded = Boolean.valueOf(e.getAttribute("downloaded")).booleanValue(); } catch ( MalformedURLException mue ) { throw new MalformedGameEntryException(mue); } } /** * Obtain an XML representation for this resource entry. * @param doc The document in which to create the XML element associated with this game resource. * @param isMainResource Whether the resource is the main resource of a game or not. Will be used to set the XML element name. * @return */ public Node getXML ( Document doc , boolean isMainResource ) { Element result; if ( isMainResource ) result = doc.createElement("main-resource"); else result = doc.createElement("resource"); if ( localRelativePath != null ) result.setAttribute("local",localRelativePath); if ( localAbsolutePath != null ) result.setAttribute("localAbsolute",localAbsolutePath); if ( remoteURL != null ) result.setAttribute("remote",remoteURL.toString()); if ( zipfileRelativePath != null ) result.setAttribute("zip", zipfileRelativePath); result.setAttribute("downloaded",String.valueOf(downloaded)); return result; } /** * Obtains only the filename (not complete path) from an URL. * @param u */ private String getFileNameFromURL ( URL u ) { String path = u.getFile(); int index = path.lastIndexOf('/'); if ( index < 0 ) return ""; else return path.substring(index+1); } private void downloadFileFromURL ( URL fromURL , File toFile , ProgressKeepingDelegate toNotify ) throws IOException { //TODO Could also try the default API class ProgressMonitorInputStream toNotify.progressUpdate(0.001 , UIMessages.getInstance().getMessage("gameloader.pre.download") + ": " + getFileNameFromURL(fromURL) ); URLConnection connection = fromURL.openConnection(); connection.setReadTimeout(5000); InputStream inStream = connection.getInputStream(); ReadableByteChannel rbc = Channels.newChannel(inStream); toNotify.progressUpdate(0.002 , UIMessages.getInstance().getMessage("gameloader.pre.download.connection") + ": " + getFileNameFromURL(fromURL) ); int contentLength = DownloadUtil.contentLength(fromURL); toNotify.progressUpdate(0.003 , UIMessages.getInstance().getMessage("gameloader.pre.download.length") + ": " + getFileNameFromURL(fromURL) ); ProgressKeepingReadableByteChannel prbc = new ProgressKeepingReadableByteChannel(rbc,contentLength,toNotify, UIMessages.getInstance().getMessage("gameloader.game.downloading") + ": " + getFileNameFromURL(fromURL) ); FileOutputStream fos = new FileOutputStream(toFile); fos.getChannel().transferFrom(prbc, 0, Long.MAX_VALUE); inStream.close(); fos.close(); } /** * Checks if the local file referenced in the resource exists (regardless of the contents of the "downloaded" flag) * @return */ public boolean checkLocalFileExists () { return getLocalPath().exists(); } /** * Downloads this resource from the remote URL (if it hasn't already been downloaded). * This method shouldn't be called when the resource has already been downloaded, as it will check the filesystem for the * resource, causing inefficiency. * @throws IOException */ public void download ( ProgressKeepingDelegate toNotify ) throws IOException { if ( downloaded && checkLocalFileExists() ) return; //no need to download, file is already there. else { try { boolean isZipped = remoteURL.toString().endsWith(".zip"); //if the download is zipped, we'll need to download the zip file and then decompress it setDownloadInProgress(true); File outputPath = getLocalPath(); if ( isZipped ) outputPath = getZipfilePath(); if ( !outputPath.getParentFile().exists() ) outputPath.getParentFile().mkdirs(); //create directory if it doesn't exist downloadFileFromURL ( remoteURL , outputPath , toNotify ); if ( isZipped ) { toNotify.progressUpdate(1.0, "Unzipping " + outputPath.getName()); Unzipper.unzip(outputPath.getAbsolutePath(), outputPath.getParentFile().getAbsolutePath()); //unzip outputPath.delete(); //delete the zipfile since we have extracted the contents } setDownloaded(true); setDownloadInProgress(false); } catch ( IOException e ) { setDownloadInProgress(false); throw e; } } } }