marytts.tools.install.ComponentDescription.java Source code

Java tutorial

Introduction

Here is the source code for marytts.tools.install.ComponentDescription.java

Source

/**
 * Copyright 2009 DFKI GmbH.
 * All Rights Reserved.  Use is subject to license terms.
 *
 * This file is part of MARY TTS.
 *
 * MARY TTS is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, version 3 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

package marytts.tools.install;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Observable;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.jar.JarFile;
import java.util.jar.JarInputStream;
import java.util.jar.Manifest;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import marytts.util.MaryUtils;
import marytts.util.dom.DomUtils;

import org.apache.commons.lang.StringUtils;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;

import com.twmacinta.util.MD5;

/**
 * @author marc
 *
 */
public class ComponentDescription extends Observable implements Comparable<ComponentDescription> {
    public enum Status {
        AVAILABLE, DOWNLOADING, PAUSED, VERIFYING, DOWNLOADED, INSTALLING, CANCELLED, ERROR, INSTALLED
    };

    public static final String installerNamespaceURI = "http://mary.dfki.de/installer";
    // Max size of download buffer.
    private static final int MAX_BUFFER_SIZE = 1024;

    private String name;
    private Locale locale;
    private String version;
    private String description;
    private URL license;
    private List<URL> locations;
    private String packageFilename;
    private int packageSize;
    private String packageMD5;
    private boolean isSelected = false;
    private Status status;
    private File archiveFile;
    private File infoFile;
    private int downloaded = 0;
    private int size = -1;
    private String installedFilesNames = null; // must be != null for installed components
    private Set<String> sharedFileNames;

    /**
     * An available update is non-null in the following circumstance:
     * 1. the present component (this) has status INSTALLED;
     * 2. a component is available that has the same name and type, but a higher version number.
     * Only the newest update will be considered, i.e. if there are more than one newer version available,
     * only the one with the highest version number will be remembered here.
     */
    private ComponentDescription availableUpdate = null;

    /**
     * Replace this component definition with its available update.
     * The object will stay the same but all data will be overwritten with 
     * the content of the update.
     * After this method returns, isUpdateAvailable() will return false.
     */
    public void replaceWithUpdate() {
        if (availableUpdate == null) {
            return;
        }
        this.name = availableUpdate.name;
        this.locale = availableUpdate.locale;
        this.version = availableUpdate.version;
        this.description = availableUpdate.description;
        this.license = availableUpdate.license;
        this.locations = availableUpdate.locations;
        this.packageFilename = availableUpdate.packageFilename;
        this.packageSize = availableUpdate.packageSize;
        this.packageMD5 = availableUpdate.packageMD5;
        this.isSelected = availableUpdate.isSelected;
        this.status = availableUpdate.status;
        this.archiveFile = availableUpdate.archiveFile;
        this.infoFile = availableUpdate.infoFile;
        this.downloaded = availableUpdate.downloaded;
        this.size = availableUpdate.size;
        this.installedFilesNames = availableUpdate.installedFilesNames;
        this.availableUpdate = null;
        stateChanged();
    }

    protected ComponentDescription(String name, String version, String packageFilename) {
        this.name = name;
        this.version = version;
        this.packageFilename = packageFilename;
    }

    protected ComponentDescription(Element xmlDescription) throws NullPointerException {
        this.name = xmlDescription.getAttribute("name");
        this.locale = MaryUtils.string2locale(xmlDescription.getAttribute("locale"));
        this.version = xmlDescription.getAttribute("version");
        Element descriptionElement = (Element) xmlDescription.getElementsByTagName("description").item(0);
        this.description = descriptionElement.getTextContent().trim();
        Element licenseElement = (Element) xmlDescription.getElementsByTagName("license").item(0);
        try {
            this.license = new URL(licenseElement.getAttribute("href").trim().replaceAll(" ", "%20"));
        } catch (MalformedURLException mue) {
            new Exception("Invalid license URL -- ignoring", mue).printStackTrace();
            this.license = null;
        }
        Element packageElement = (Element) xmlDescription.getElementsByTagName("package").item(0);
        packageFilename = packageElement.getAttribute("filename").trim();
        packageSize = Integer.parseInt(packageElement.getAttribute("size"));
        packageMD5 = packageElement.getAttribute("md5sum");
        NodeList locationElements = packageElement.getElementsByTagName("location");
        locations = new ArrayList<URL>(locationElements.getLength());
        for (int i = 0, max = locationElements.getLength(); i < max; i++) {
            Element aLocationElement = (Element) locationElements.item(i);
            try {
                String urlString = aLocationElement.getAttribute("href").trim().replaceAll(" ", "%20");
                boolean isFolder = true;
                if (aLocationElement.hasAttribute("folder")) {
                    isFolder = Boolean.valueOf(aLocationElement.getAttribute("folder"));
                }
                if (isFolder && !urlString.endsWith(packageFilename)) {
                    if (!urlString.endsWith("/")) {
                        urlString += "/";
                    }
                    urlString += packageFilename;
                }
                locations.add(new URL(urlString));
            } catch (MalformedURLException mue) {
                new Exception("Invalid location -- ignoring", mue).printStackTrace();
            }
        }
        archiveFile = new File(System.getProperty("mary.downloadDir"), packageFilename);
        String infoFilename = packageFilename.substring(0, packageFilename.lastIndexOf('.')) + "-component.xml";
        infoFile = new File(System.getProperty("mary.installedDir"), infoFilename);
        determineStatus();
        NodeList filesElements = xmlDescription.getElementsByTagName("files");
        if (filesElements.getLength() > 0) {
            Element filesElement = (Element) filesElements.item(0);
            installedFilesNames = filesElement.getTextContent();
        }
    }

    private void determineStatus() {
        File installedDir = new File(System.getProperty("mary.installedDir", "installed"));
        File downloadDir = new File(System.getProperty("mary.downloadDir", "download"));

        if (infoFile.exists()) {
            status = Status.INSTALLED;
        } else if (archiveFile.exists()) {
            if (archiveFile.length() == packageSize) {
                status = Status.DOWNLOADED;
            } else {
                status = Status.AVAILABLE;
            }
        } else if (locations.size() > 0) {
            status = Status.AVAILABLE;
        } else {
            status = Status.ERROR;
        }
    }

    public String getComponentTypeString() {
        return "component";
    }

    public String getName() {
        return name;
    }

    public Locale getLocale() {
        return locale;
    }

    public void setLocale(Locale aLocale) {
        this.locale = aLocale;
    }

    public String getVersion() {
        return version;
    }

    public void setVersion(String aVersion) {
        this.version = aVersion;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String aDescription) {
        this.description = aDescription;
    }

    public URL getLicenseURL() {
        return license;
    }

    public void setLicenseURL(URL aLicense) {
        this.license = aLicense;
    }

    public List<URL> getLocations() {
        return locations;
    }

    public void removeAllLocations() {
        locations.clear();
    }

    public void addLocation(URL aLocation) {
        if (this.locations == null) {
            this.locations = new ArrayList<URL>();
        }
        locations.add(aLocation);
    }

    public String getPackageFilename() {
        return packageFilename;
    }

    public void setPackageFilename(String aPackageFilename) {
        this.packageFilename = aPackageFilename;
    }

    public int getPackageSize() {
        return packageSize;
    }

    public void setPackageSize(int aSize) {
        this.packageSize = aSize;
    }

    public String getDisplayPackageSize() {
        return MaryUtils.toHumanReadableSize(packageSize);
    }

    public String getPackageMD5Sum() {
        return packageMD5;
    }

    public void setPackageMD5Sum(String aMD5) {
        this.packageMD5 = aMD5;
    }

    public boolean isSelected() {
        return isSelected;
    }

    public void setSelected(boolean value) {
        if (value != isSelected) {
            isSelected = value;
            stateChanged();
        }
    }

    public Status getStatus() {
        return status;
    }

    public LinkedList<String> getInstalledFileNames() {
        LinkedList<String> files = new LinkedList<String>();
        if (installedFilesNames != null) {
            StringTokenizer st = new StringTokenizer(installedFilesNames, ",");
            while (st.hasMoreTokens()) {
                String next = st.nextToken().trim();
                if (!"".equals(next)) {
                    files.addFirst(next); // i.e., reverse order
                }
            }
        }
        return files;
    }

    public void setSharedFiles(Set<String> fileList) {
        sharedFileNames = fileList;
    }

    public String toString() {
        return name;
    }

    @Override
    public boolean equals(Object obj) {
        if (!(obj instanceof ComponentDescription)) {
            return false;
        }
        ComponentDescription o = (ComponentDescription) obj;
        return name.equals(o.name) && locale.equals(o.locale) && version.equals(o.version);

    }

    // Pause this download.
    public void pause() {
        status = Status.PAUSED;
        stateChanged();
    }

    // Resume this download.
    public void resume(boolean synchronous) {
        status = Status.DOWNLOADING;
        stateChanged();
        download(synchronous);
    }

    // Cancel this download.
    public void cancel() {
        status = Status.CANCELLED;
        stateChanged();
    }

    // Mark this download as having an error.
    private void error() {
        status = Status.ERROR;
        stateChanged();
    }

    // Start or resume downloading.
    public void download(boolean synchronous) {
        Downloader d = new Downloader();
        if (synchronous) {
            d.run();
        } else {
            new Thread(d).start();
        }
    }

    // Notify observers that this download's status has changed.
    private void stateChanged() {
        setChanged();
        notifyObservers();
    }

    /**
     * Install this component, if the user accepts the license.
     */
    public void install(boolean synchronous) throws Exception {
        status = Status.INSTALLING;
        stateChanged();
        Installer inst = new Installer();
        if (synchronous) {
            inst.run();
        } else {
            new Thread(inst).start();
        }
    }

    /**
     * Uninstall this component.
     * @return true if component was successfully uninstalled, false otherwise.
     */
    public boolean uninstall() {
        if (status != Status.INSTALLED) {
            throw new IllegalStateException(
                    "Can only uninstall installed components, but status is " + status.toString());
        }
        assert installedFilesNames != null; // when we have an installed component, we must also have this information
        /*        int answer = JOptionPane.showConfirmDialog(null, "Completely remove "+getComponentTypeString()+" '"+toString()+"' from the file system?", "Confirm component uninstall", JOptionPane.YES_NO_OPTION);
                if (answer != JOptionPane.YES_OPTION) {
        return false;
                }
                */
        try {
            String maryBase = System.getProperty("mary.base");
            System.out.println("Removing " + name + "-" + version + " from " + maryBase + "...");
            LinkedList<String> files = getInstalledFileNames();
            for (String file : files) {
                if (file.trim().equals(""))
                    continue; // skip empty lines
                if (sharedFileNames != null && sharedFileNames.contains(file)) {
                    System.out.println("Keeping shared file: " + file);
                    continue; // don't uninstall shared files!
                }
                File f = new File(maryBase + "/" + file);
                if (f.isDirectory()) {
                    String[] kids = f.list();
                    if (kids.length == 0) {
                        System.err.println("Removing empty directory: " + file);
                        f.delete();
                    } else {
                        System.err.println("Cannot delete non-empty directory: " + file);
                    }
                } else if (f.exists()) { // not a directory
                    System.err.println("Removing file: " + file);
                    f.delete();
                } else { // else, file doesn't exist
                    System.err.println("File doesn't exist -- cannot delete: " + file);
                }
            }
            infoFile.delete();
        } catch (Exception e) {
            System.err.println("Cannot uninstall:");
            e.printStackTrace();
            return false;
        }
        determineStatus();
        return true;
    }

    public int getProgress() {
        if (status == Status.DOWNLOADING) {
            return (int) (100L * downloaded / size);
        } else if (status == Status.INSTALLING) {
            return -1;
        }
        return 100;
    }

    private void writeDownloadedComponentXML() throws Exception {
        File archiveFolder = archiveFile.getParentFile();
        String archiveFilename = archiveFile.getName();
        String compdescFilename = archiveFilename.substring(0, archiveFilename.lastIndexOf('.')) + "-component.xml";
        File compdescFile = new File(archiveFolder, compdescFilename);
        Document doc = createComponentXML();

        DomUtils.document2File(doc, compdescFile);
    }

    /**
     * Write a component xml file to the installed/ folder, containing the given list of installed files.
     * @param installedFilesList
     * @throws Exception
     */
    private void writeInstalledComponentXML() throws Exception {
        assert installedFilesNames != null;
        File installedFolder = infoFile.getParentFile();
        String archiveFilename = archiveFile.getName();
        String compdescFilename = archiveFilename.substring(0, archiveFilename.lastIndexOf('.')) + "-component.xml";
        File compdescFile = new File(installedFolder, compdescFilename);
        Document doc = createComponentXML();
        DomUtils.document2File(doc, compdescFile);
    }

    public Document createComponentXML() throws ParserConfigurationException {
        DocumentBuilderFactory fact = DocumentBuilderFactory.newInstance();
        fact.setNamespaceAware(true);
        Document doc = fact.newDocumentBuilder().newDocument();
        Element root = (Element) doc.appendChild(doc.createElementNS(installerNamespaceURI, "marytts-install"));
        Element desc = (Element) root
                .appendChild(doc.createElementNS(installerNamespaceURI, getComponentTypeString()));
        desc.setAttribute("locale", MaryUtils.locale2xmllang(locale));
        desc.setAttribute("name", name);
        desc.setAttribute("version", version);
        Element descriptionElt = (Element) desc
                .appendChild(doc.createElementNS(installerNamespaceURI, "description"));
        descriptionElt.setTextContent(description);
        Element licenseElt = (Element) desc.appendChild(doc.createElementNS(installerNamespaceURI, "license"));
        if (license != null) {
            licenseElt.setAttribute("href", license.toString());
        }
        Element packageElt = (Element) desc.appendChild(doc.createElementNS(installerNamespaceURI, "package"));
        packageElt.setAttribute("size", Integer.toString(packageSize));
        packageElt.setAttribute("md5sum", packageMD5);
        packageElt.setAttribute("filename", packageFilename);
        for (URL l : locations) {
            // Serialize the location without the filename:
            String urlString = l.toString();
            boolean isFolder = false;
            if (urlString.endsWith(packageFilename)) {
                urlString = urlString.substring(0, urlString.length() - packageFilename.length());
                isFolder = true;
            }
            Element lElt = (Element) packageElt.appendChild(doc.createElementNS(installerNamespaceURI, "location"));
            lElt.setAttribute("href", urlString);
            lElt.setAttribute("folder", String.valueOf(isFolder));
        }
        if (installedFilesNames != null) {
            Element filesElement = (Element) desc.appendChild(doc.createElementNS(installerNamespaceURI, "files"));
            filesElement.setTextContent(installedFilesNames);
        }
        return doc;
    }

    /**
     * Inform whether an update is available for this component.
     * @return
     */
    public boolean isUpdateAvailable() {
        return availableUpdate != null;
    }

    /**
     * If this component has an available update, get that update. 
     * @return null if no update is available.
     */
    public ComponentDescription getAvailableUpdate() {
        return availableUpdate;
    }

    /**
     * Set the given component description as the available update of this component.
     * If we already have an available update, then aDesc is only retained as the available update
     * if it has a higher version number than the previously set update.
     * @param aDesc an available update, or null to erase any previously set available updates.
     * @throws IllegalStateException if aDesc is not null and our status is not "INSTALLED".
     * @throws IllegalArgumentException if aDesc is not null and our name is not the same as aDesc's name, or if our version number
     * is not smaller than aDesc's version number.
     */
    public void setAvailableUpdate(ComponentDescription aDesc) {
        if (aDesc == null) {
            this.availableUpdate = null;
            stateChanged();
            return;
        }
        if (this.status != Status.INSTALLED) {
            throw new IllegalStateException(
                    "Can only set an available update if status is installed, but status is " + status.toString());
        }
        if (!aDesc.getName().equals(name)) {
            throw new IllegalArgumentException(
                    "Only a component with the same name can be an update of this component; but this has name "
                            + name + ", and argument has name " + aDesc.getName());
        }
        if (!(isVersionNewerThan(aDesc.getVersion(), version))) {
            throw new IllegalArgumentException(
                    "Version " + aDesc.getVersion() + " is not higher than installed version " + version);
        }
        if (availableUpdate != null) { // already have an available update: we will replace it with aDesc only if aDesc has a higher version number
            if (!(isVersionNewerThan(aDesc.getVersion(), availableUpdate.getVersion()))) {
                return;
            }
        }
        this.availableUpdate = aDesc;
        stateChanged();
    }

    /**
     * This is an update of other if and only if the following is true:
     * <ol>
     * <li>Both components have the same type (as identified by the class) and name;</li>
     * <li>other has status INSTALLED;</li>
     * <li>our version number is higher than other's version number.</li>
     * </ol>
     * @param other
     * @return
     */
    public boolean isUpdateOf(ComponentDescription other) {
        if (other == null || !this.getClass().equals(other.getClass()) || !name.equals(other.getName())
                || other.getStatus() != Status.INSTALLED || !(isVersionNewerThan(version, other.getVersion()))) {
            return false;
        }
        return true;
    }

    /**
     * Determine whether oneVersion is newer than otherVersion.
     * This performs a lexicographic comparison with the exception that a version with suffix "-SNAPSHOT"
     * is treated as older than the version without that suffix.
     * A version is not newer than itself.  
     * @param oneVersion
     * @param otherVersion
     * @return true if oneVersion is newer than otherVersion, false if they are equal or otherVersion is newer.
     */
    public static boolean isVersionNewerThan(String oneVersion, String otherVersion) {
        if (oneVersion == null || otherVersion == null) {
            return false;
        }
        // The only special cases we need to consider is when otherVersion is oneVersion suffixed with "-SNAPSHOT",
        // or the other way round: in this case the one with the suffix is newer.
        // All other cases are covered by lexicographic comparison.
        if (otherVersion.equals(oneVersion + "-SNAPSHOT")) {
            return true;
        }
        if (oneVersion.equals(otherVersion + "-SNAPSHOT")) {
            return false;
        }
        return oneVersion.compareTo(otherVersion) > 0;
    }

    /**
     * Define a natural ordering for component descriptions. Languages first, in alphabetic order, then voices, in alphabetic order.
     */
    public int compareTo(ComponentDescription o) {
        int myPos = 0;
        int oPos = 0;
        if (this instanceof LanguageComponentDescription) {
            myPos = 5;
        } else if (this instanceof VoiceComponentDescription) {
            myPos = 10;
        }
        if (o instanceof LanguageComponentDescription) {
            oPos = 5;
        } else if (o instanceof VoiceComponentDescription) {
            oPos = 10;
        }

        if (oPos - myPos != 0) {
            return (oPos - myPos);
        }

        // Same type, sort by name
        return name.compareTo(o.name);
    }

    public static final void copyInputStream(InputStream in, OutputStream out) throws IOException {
        byte[] buffer = new byte[1024];
        int len;

        while ((len = in.read(buffer)) >= 0)
            out.write(buffer, 0, len);

        in.close();
        out.close();
    }

    class Downloader implements Runnable {

        private HttpURLConnection openAndRedirectIfRequired(URL url) throws IOException {
            int maxRedirects = 5;
            for (int i = 0; i < maxRedirects; i++) {
                HttpURLConnection c = (HttpURLConnection) url.openConnection();
                c.setInstanceFollowRedirects(false);
                // Specify what portion of file to download.
                c.setRequestProperty("Range", "bytes=" + downloaded + "-");
                // Connect to server.
                c.connect();
                // Check for redirects
                int stat = c.getResponseCode();
                if (stat >= 300 && stat <= 307 && stat != 306 && stat != HttpURLConnection.HTTP_NOT_MODIFIED) {
                    URL base = c.getURL();
                    String location = c.getHeaderField("Location");
                    c.disconnect();
                    if (location == null) {
                        throw new SecurityException("No redirect location given.");
                    }
                    URL target = new URL(base, location);
                    String protocol = target.getProtocol();
                    if (!(protocol.equals("http") || protocol.equals("https"))) {
                        throw new SecurityException(
                                "Redirect supported to http and https protocols only, but found '" + protocol
                                        + "'");
                    }
                    url = target;
                } else {
                    return c;
                }
            }
            throw new SecurityException("More than five redirects, aborting");
        }

        public void run() {
            status = Status.DOWNLOADING;
            stateChanged();

            RandomAccessFile file = null;
            InputStream stream = null;

            HttpURLConnection connection = null;
            for (URL u : locations) {
                try {
                    System.out.println("Trying location " + u + "...");
                    // Open connection to URL.
                    connection = openAndRedirectIfRequired(u);
                    // Make sure response code is in the 200 range.
                    if (connection.getResponseCode() / 100 != 2) {
                        throw new IOException("Non-OK response code: " + connection.getResponseCode() + " ("
                                + connection.getResponseMessage() + ")");
                    }
                    System.out.println("...connected");
                    // Check for valid content length.
                    int contentLength = connection.getContentLength();
                    if (contentLength > -1) {
                        if (contentLength != packageSize) {
                            throw new IOException("Expected package size " + packageSize
                                    + ", but web server reports " + contentLength);
                        }
                    }

                    /* Set the size for this download if it
                       hasn't been already set. */
                    if (size == -1) {
                        size = packageSize;
                        stateChanged();
                    }
                    System.out.println("...downloading" + (downloaded > 0 ? " from byte " + downloaded : ""));
                    boolean success = tryToDownloadFromLocation(file, stream, connection);
                    if (success) {
                        break; // current location seems OK, leave loop
                    }
                } catch (Exception exc) {
                    exc.printStackTrace();
                }
            }

        }

        private boolean tryToDownloadFromLocation(RandomAccessFile file, InputStream stream,
                HttpURLConnection connection) {
            boolean success = false;
            try {
                // Open file and seek to the end of it.
                file = new RandomAccessFile(archiveFile, "rw");
                file.seek(downloaded);

                stream = connection.getInputStream();
                if (status == Status.ERROR) {
                    downloaded = 0;
                }
                status = Status.DOWNLOADING;
                byte[] buffer = new byte[MAX_BUFFER_SIZE];
                while (status == Status.DOWNLOADING) {
                    /* target number of bytes to download depends on how much of the
                       file is left to download. */
                    int len = Math.min(buffer.length, size - downloaded);
                    // Read from server into buffer.
                    int read = stream.read(buffer, 0, len);
                    if (read == -1)
                        break;

                    // Write buffer to file.
                    file.write(buffer, 0, read);
                    downloaded += read;
                    stateChanged();
                }

                /* Change status to complete if this point was
                   reached because downloading has finished. */
                if (status == Status.DOWNLOADING) {
                    System.err.println("Download of " + packageFilename + " has finished.");
                    System.err.print("Computing checksum...");
                    status = Status.VERIFYING;
                    stateChanged();
                    String hash = MD5.asHex(MD5.getHash(archiveFile));
                    if (hash.equals(packageMD5)) {
                        System.err.println("ok!");
                        writeDownloadedComponentXML();
                        status = Status.DOWNLOADED;
                        success = true;
                    } else {
                        System.err.println("failed!");
                        System.out.println("MD5 according to component description: " + packageMD5);
                        System.out.println("MD5 computed:   " + hash);
                        status = Status.ERROR;
                        downloaded = 0;
                    }
                    stateChanged();
                }
            } catch (Exception e) {
                e.printStackTrace();
                error();
            } finally {
                // Close file.
                if (file != null) {
                    try {
                        file.close();
                    } catch (Exception e) {
                    }
                }

                // Close connection to server.
                if (stream != null) {
                    try {
                        stream.close();
                    } catch (Exception e) {
                    }
                }
            }
            return success;
        }

    }

    class Installer implements Runnable {
        public void run() {
            String maryBase = System.getProperty("mary.base");
            System.out.println("Installing " + name + "-" + version + " in " + maryBase + "...");
            ArrayList<String> files = new ArrayList<String>();
            try {
                ZipFile zipfile = new ZipFile(archiveFile);
                Enumeration<? extends ZipEntry> entries = zipfile.entries();
                while (entries.hasMoreElements()) {
                    ZipEntry entry = entries.nextElement();
                    files.add(entry.getName()); // add to installed filelist; rely on uninstaller retaining shared files
                    File newFile = new File(maryBase + "/" + entry.getName());
                    if (entry.isDirectory()) {
                        System.err.println("Extracting directory: " + entry.getName());
                        newFile.mkdir();
                    } else {
                        if (newFile.exists()) {
                            // is existing file newer?
                            boolean existingIsNewer = false;
                            try {
                                // existing JAR:
                                JarFile existingJar = new JarFile(newFile);
                                Manifest existingManifest = existingJar.getManifest();
                                String existingVersion = existingManifest.getMainAttributes()
                                        .getValue("Specification-Version");
                                // packaged JAR:
                                JarInputStream packagedJar = new JarInputStream(zipfile.getInputStream(entry));
                                Manifest packagedManifest = packagedJar.getManifest();
                                String packagedVersion = packagedManifest.getMainAttributes()
                                        .getValue("Specification-Version");
                                // compare the version strings:
                                existingIsNewer = isVersionNewerThan(existingVersion, packagedVersion);
                                if (existingIsNewer) {
                                    // if we don't overwrite a newer existing JAR, then never log it as installed, otherwise we lose it during uninstall!
                                    files.remove(entry.getName());
                                }
                            } catch (Exception e) {
                                // we're not dealing with a JAR file
                                // TODO disabled this block on short notice because installed files are touched and will therefore always be newer:
                                //                                long existingDate;
                                //                                // fall back to last modification time:
                                //                                try {
                                //                                    existingDate = newFile.lastModified();
                                //                                } catch (SecurityException f) {
                                //                                    // WTF, we can't get the date from the existing file!?
                                //                                    e.printStackTrace();
                                //                                    // assume it's outdated, to trigger reinstall (which may well fail): 
                                //                                    existingDate = Long.MIN_VALUE;
                                //                                }
                                //                                long packagedDate = entry.getTime();
                                //                                if (existingDate > packagedDate) {
                                //                                    existingIsNewer = true;
                                //                                }
                            }
                            // do not overwrite existing files if they are newer:
                            if (existingIsNewer) {
                                System.err.println("NOT overwriting existing newer file: " + entry.getName());
                                continue;
                            }
                        }
                        if (!newFile.getParentFile().isDirectory()) {
                            System.err.println(
                                    "Creating directory tree: " + newFile.getParentFile().getAbsolutePath());
                            newFile.getParentFile().mkdirs();
                        }
                        System.err.println("Extracting file: " + entry.getName());
                        copyInputStream(zipfile.getInputStream(entry),
                                new BufferedOutputStream(new FileOutputStream(newFile)));
                        // better hack: try to set executable bit on files in bin/
                        if (entry.getName().startsWith("bin/")) {
                            try {
                                if (newFile.setExecutable(true, false)) {
                                    System.err.println("Setting executable bit on file: " + entry.getName());
                                }
                            } catch (SecurityException e) {
                                e.printStackTrace(); // but ignore
                            }
                        }
                    }
                }
                zipfile.close();
                installedFilesNames = StringUtils.join(files, ", ");
                writeInstalledComponentXML();
            } catch (Exception e) {
                System.err.println("... installation failed:");
                e.printStackTrace();
                status = Status.ERROR;
                stateChanged();
            }
            System.err.println("...done");
            status = Status.INSTALLED;
            stateChanged();
        }

    }

}