content: /*
   * Copyright (c) 2010 Pentaho Corporation.  All rights reserved. 
   * This software was developed by Pentaho Corporation and is provided under the terms 
   * of the GNU Lesser General Public License, Version 2.1. You may not use 
   * this file except in compliance with the license. If you need a copy of the license, 
   * please go to http://www.gnu.org/licenses/lgpl-2.1.txt. The Original Code is Pentaho 
   * Package Manager.  The Initial Developer is Pentaho Corporation.
   *
   * Software distributed under the GNU Lesser Public License is distributed on an "AS IS" 
   * basis, WITHOUT WARRANTY OF ANY KIND, either express or  implied. Please refer to 
   * the license for the specific language governing your rights and limitations.
   */
  
  /*
   *    DefaultPackageManager.java
   *    Copyright (c) 2009 Pentaho Corporation
   */
  
  package org.pentaho.packageManagement;
  
  import java.io.BufferedInputStream;
  import java.io.BufferedOutputStream;
  import java.io.BufferedReader;
  import java.io.ByteArrayOutputStream;
  import java.io.File;
  import java.io.FileInputStream;
  import java.io.FileOutputStream;
  import java.io.IOException;
  import java.io.InputStream;
  import java.io.InputStreamReader;
  import java.io.OutputStream;
  import java.io.PrintStream;
  import java.net.HttpURLConnection;
  import java.net.URL;
  import java.net.URLConnection;
  import java.util.ArrayList;
  import java.util.Enumeration;
  import java.util.HashMap;
  import java.util.Iterator;
  import java.util.List;
  import java.util.Map;
  import java.util.Properties;
  import java.util.zip.ZipEntry;
  import java.util.zip.ZipFile;
  import java.util.zip.ZipOutputStream;
  
  /**
   * A concrete implementation of PackageManager that uses Java properties files/class
   * to manage package meta data. Assumes that meta data for individual packages is
   * stored on the central repository (accessible via http) in properties files
   * that live in a subdirectory with the same name as the package. 
   * Furthermore, each property file is assumed to be named as the version
   * number of the package in question with a ".props" extension. A "Latest.props"
   * file should exist for each package and should always hold meta data on the 
   * latest version of a package.
   * 
   * @author Mark Hall (mhall{[at]}pentaho{[dot]}com)
   * @version $Revision: 44030 $
   */
  public class DefaultPackageManager extends PackageManager {
    
    /** buffer size for copying files */
    static final int BUFF_SIZE = 100000;
    
    /** buffer used in copying files */
    static final byte[] m_buffer = new byte[BUFF_SIZE];
    
  /*  protected File downloadPackage2(URL packageURL, 
        PrintStream... progress) throws Exception {
      String packageArchiveName = packageURL.toString();
      packageArchiveName = 
        packageArchiveName.substring(packageArchiveName.lastIndexOf('/'), 
            packageArchiveName.length());
      packageArchiveName = packageArchiveName.substring(0, packageArchiveName.lastIndexOf('.'));
      
      // make a temp file to hold the downloaded archive
      File tmpDownload = File.createTempFile(packageArchiveName, ".zip");
      
      for (int i = 0; i < progress.length; i++) {
        progress[i].println("[Package Manager] Tmp file: " + tmpDownload.toString());
      }
      
      System.err.println("Here in downloadPackage...");
      URLConnection conn = null;
      
      // setup the proxy (if we are using one) and open the connect
      if (setProxyAuthentication()) {
        conn = packageURL.openConnection(m_httpProxy);
      } else {
        conn = packageURL.openConnection();
      }
      
      if (conn instanceof HttpURLConnection) {
        System.err.println("We have a http url conn.");
      }
      
      BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream()));
      
      String line = null;
      
      while ((line = br.readLine()) != null) {
        System.err.println(line);
      }
      
      br.close();
      
      return null;
    } */
    
    protected File downloadPackage(URL packageURL, 
        PrintStream... progress) throws Exception {
      String packageArchiveName = packageURL.toString();
      
        packageArchiveName = 
          packageArchiveName.substring(0, packageArchiveName.lastIndexOf(".zip")+3);
        
      packageArchiveName = packageArchiveName.substring(0, packageArchiveName.lastIndexOf('.'));
      
      packageArchiveName = 
        packageArchiveName.substring(packageArchiveName.lastIndexOf('/'), 
            packageArchiveName.length());
      
      // make a temp file to hold the downloaded archive
      File tmpDownload = File.createTempFile(packageArchiveName, ".zip");
      
      for (int i = 0; i < progress.length; i++) {
        progress[i].println(packageURL.toString());
        progress[i].println("[DefaultPackageManager] Tmp file: " + tmpDownload.toString());
      }
      
      URLConnection conn = null;
      
      // setup the proxy (if we are using one) and open the connect
      if (setProxyAuthentication()) {
        conn = packageURL.openConnection(m_httpProxy);
      } else {
        conn = packageURL.openConnection();
      }        
      
      BufferedInputStream bi = new BufferedInputStream(conn.getInputStream());
      
      BufferedOutputStream bo = new BufferedOutputStream(new FileOutputStream(tmpDownload));
      
      // download the archive
      int totalBytesRead = 0;
      while (true) {
        synchronized (m_buffer) {
          int amountRead = bi.read(m_buffer);
          if (amountRead == -1) {
            for (int i = 0; i < progress.length; i++) {
              progress[i].println("[DefaultPackageManager] downloaded " 
                  + (totalBytesRead/1000) + " KB");
            }
            break;
          }        
          bo.write(m_buffer, 0, amountRead);
          totalBytesRead += amountRead;
          for (int i = 0; i < progress.length; i++) {
            progress[i].println("%%[DefaultPackageManager] downloaded " 
                + (totalBytesRead/1000) + " KB");
          }
        }
      }
      
      bi.close();
      bo.close();
      
      return tmpDownload;
    }
    
    /**
     * Get package information on the package at the given URL.
     * 
     * @param packageURL the URL to the package.
     * @return a Package object encapsulating the package meta data
     * @throws Exception if the package meta data can't be retrieved.
     */
    public Package getURLPackageInfo(URL packageURL) throws Exception {
      File downloaded = downloadPackage(packageURL);
      
      // return the package info
      return getPackageArchiveInfo(downloaded);
    }
        
    /**
     * Get package information on the named package from the repository. If multiple
     * versions of the package are available, it assumes that the most recent is
     * required.
     * 
     * @param packageName the name of the package to get information about.
     * @return a Package object encapsulating the package meta data.
     * @throws Exception if the package meta data can't be retrieved.
     */
    public Package getRepositoryPackageInfo(String packageName) throws Exception {
      
      return getRepositoryPackageInfo(packageName, "Latest");
    }
    
    /**
     * Get a list of available versions of the named package.
     * 
     * @param packageName the name of the package to get versions.
     * @return a list of available versions (or null if not applicable)
     * @throws Exception if something goes wrong while trying to retrieve the list
     * of versions.
     */
    public List<Object> getRepositoryPackageVersions(String packageName) 
      throws Exception {
      
      if (getPackageRepositoryURL() == null) {
        throw new Exception("[DefaultPackageManager] No package repository set!!");
      }
      
      String versionsS = m_packageRepository.toString() + "/" + packageName 
        + "/" + "versions.txt";
      
      URL packageURL = new URL(versionsS);
      URLConnection conn = null;
      
      // setup the proxy (if we are using one) and open the connect
      if (setProxyAuthentication()) {
        conn = packageURL.openConnection(m_httpProxy);
      } else {
        conn = packageURL.openConnection();
      }
      
      BufferedReader bi = 
        new BufferedReader(new InputStreamReader(conn.getInputStream()));
      
      ArrayList<Object> versions = new ArrayList<Object>();
      String versionNumber;
      while ((versionNumber = bi.readLine()) != null) {
        versions.add(versionNumber.trim());
      }
      
      bi.close();
      return versions;
    }
    
    /**
     * Get package information on the named package from the repository.
     * 
     * @param packageName the name of the package to get information about.
     * @param version the version of the package to retrieve (may be null if not applicable).
     * @return a Package object encapsulating the package meta data.
     * @throws Exception if the package meta data can't be retrieved.
     */
    public Package getRepositoryPackageInfo(String packageName, Object version) throws Exception {
      if (getPackageRepositoryURL() == null) {
        throw new Exception("[DefaultPackageManager] No package repository set!!");
      }
      
      if (version == null) {
        version = "Latest";
      }
      
      String packageS = m_packageRepository.toString() + "/" + packageName 
      + "/" + version.toString() + ".props";
      URL packageURL = new URL(packageS);
      URLConnection conn = null;
      
      // setup the proxy (if we are using one) and open the connect
      if (setProxyAuthentication()) {
        conn = packageURL.openConnection(m_httpProxy);
      } else {
        conn = packageURL.openConnection();
      }
      
  
      BufferedInputStream bi = new BufferedInputStream(conn.getInputStream());
      Properties packageProperties = new Properties();
      packageProperties.load(bi);
      bi.close();
      
      return new DefaultPackage(m_packageHome, this, packageProperties);
    }
    
    
    private Package getPackageArchiveInfo(File packageArchive) throws Exception {
      return getPackageArchiveInfo(packageArchive.getAbsolutePath());
    }
    
    /**
     * Get package information from the supplied package archive file.
     * 
     * @param packageArchivePath the path to the package archive file
     * @return a Package object encapsulating the package meta data.
     * @throws Exception if the package meta data can't be retrieved.
     */
    public Package getPackageArchiveInfo(String packageArchivePath) throws Exception {
      ZipFile zip = new ZipFile(new File(packageArchivePath));
  
      for (Enumeration e = zip.entries(); e.hasMoreElements();) {
        ZipEntry entry = (ZipEntry) e.nextElement();
        if (entry.getName().endsWith("Description.props")) {
          InputStream is = zip.getInputStream(entry);
          Properties packageProperties = new Properties();
          packageProperties.load(new BufferedInputStream(is));
          is.close();
          
          DefaultPackage pkg = new DefaultPackage(m_packageHome, this, packageProperties);
          
          return pkg;
        }
      }
      
      throw new Exception("Unable to find Description file in package archive!");
    }
    
    /**
     * Get package information on the named installed package.
     * 
     * @param packageName the name of the package to get information about.
     * @return a Package object encapsulating the package meta data or null if the
     * package is not installed.
     * 
     * @throws Exception if the package meta data can't be retrieved.
     */
    public Package getInstalledPackageInfo(String packageName) throws Exception {
      File packageDescription = new File(m_packageHome.getAbsoluteFile() + File.separator
          + packageName + File.separator + "Description.props");
      
      if (!packageDescription.exists()) {
        return null;
      }
      
      FileInputStream fis = 
        new FileInputStream(packageDescription);
      
      
      
      Properties packageProperties = new Properties();
      packageProperties.load(fis);
      fis.close();
      
      DefaultPackage pkg = new DefaultPackage(m_packageHome, this, packageProperties);
      return pkg;    
    }
    
    /**
     * Checks to see if the package home exists and creates it if
     * necessary.
     * 
     * @return true if the package home exists/was created successfully.
     */
    protected boolean establishPackageHome() {
      if (m_packageHome == null) {
        return false;
      }
      
      if (!m_packageHome.exists()) {
        // create it for the user
        if (!m_packageHome.mkdir()) {
          System.err.println("Unable to create packages directory (" +
          		m_packageHome.getAbsolutePath() + ")");
          return false;
        }
      }
      return true;
    }
    
    public static void deleteDir(File dir, PrintStream... progress) throws Exception {
      
      // get the contents    
      File[] contents = dir.listFiles();
  
      if (contents.length != 0) {
        // process contents
        for (File f : contents) {
          if (f.isDirectory()) {
            deleteDir(f);
          } else {
            for (int i = 0; i < progress.length; i++) {
              progress[i].println("[DefaultPackageManager] removing: " + f.toString());
            }
            if (!f.delete()) {
              System.err.println("[DefaultPackageManager] can't delete file " + f.toString());
              f.deleteOnExit();
            }
          }
        }
      }
      
      // delete this directory
      if (!dir.delete()) {
        System.err.println("[DefaultPackageManager] can't delete directory " + dir.toString());
        dir.deleteOnExit();
      }
      for (int i = 0; i < progress.length; i++) {
        progress[i].println("[DefaultPackageManager] removing: " + dir.toString());
      }
    }
    
    /**
     * Uninstall a package.
     * 
     * @param packageName the package to uninstall.
     * @param progress optional varargs parameter, that, if supplied, is expected to contain one
     * or more PrintStream objects to write progress to.
     * @throws Exception if the named package could not be removed for some reason.
     */
    public void uninstallPackage(String packageName, PrintStream... progress)
      throws Exception {
      File packageToDel = new File(m_packageHome.toString() + File.separator 
          + packageName);
      
      if (!packageToDel.exists()) {
        throw new Exception("[DefaultPackageManager] Can't remove " + packageName
            +" because it doesn't seem to be installed!");
      }
      
      deleteDir(packageToDel, progress);
    }
    
    /**
     * Install a package from an archive on the local file system.
     * 
     * @param packageArchivePath the path to the package archive file.
     * @param progress optional varargs parameter, that, if supplied, is expected to contain one
     * or more PrintStream objects to write progress to.
     * @return the name of the package installed
     * @throws Exception if the package can't be installed for some reason.
     */
    public String installPackageFromArchive(String packageArchivePath,
        PrintStream... progress) throws Exception {
      Properties packageProps = 
        (Properties)getPackageArchiveInfo(packageArchivePath).getPackageMetaData();
      String packageName = packageProps.getProperty("PackageName");
      if (packageName == null) {
        throw new Exception("Unable to find the name of the package in" +
        		" the Description file for " + packageArchivePath);
      }
      
      installPackage(packageArchivePath, packageName, progress);
      
      return packageName;
    }
    
    /**
     * Installs all the packages in the supplied list.
     * 
     * @param toInstall a list of Packages to install.
     * @param progress optional varargs parameter, that, if supplied, is expected to contain one
     * or more PrintStream objects to write progress to.
     * @throws Exception if something goes wrong during the installation process.
     */
    public void installPackages(List<Package> toInstall, 
        PrintStream... progress) throws Exception {
      File[] archivePaths = new File[toInstall.size()];
      
      for (int i = 0; i < toInstall.size(); i++) {
        Package toDownload = toInstall.get(i);
        archivePaths[i] = downloadPackage(toDownload.getPackageURL(), progress);
      }
      
      // OK, all downloaded successfully, now install them
      for (int i = 0; i < archivePaths.length; i++) {
        installPackageFromArchive(archivePaths[i].getAbsolutePath(), progress);
      }
    }
    
    /**
     * Checks a given package's list of dependencies for any conflicts with the packages
     * in the supplied Map. Any packages from this packages dependency list that are not
     * in the Map are simply added and checkDependencies is called recursively for each.
     * 
     * @param toCheck the package to check.
     * @param lookup a Map of package name, Dependency pairs to check against.
     * @param conflicts a list of Dependency objects for any conflicts that are detected.
     * @return true if no conflicts are found.
     * @throws Exception if a problem occurs when checking for conflicts.
     */
    protected static boolean checkDependencies(PackageConstraint toCheck, 
        Map<String, Dependency> lookup, Map<String, List<Dependency>> conflicts) 
      throws Exception {
      boolean ok = true;
      
      // get the dependencies for the package to check
      List<Dependency> deps = toCheck.getPackage().getDependencies();
      
      for (Dependency p : deps) {
        String depName = p.getTarget().getPackage().getPackageMetaDataElement("PackageName").toString();
        if (!lookup.containsKey(depName)) {
          // just add this package to the lookup
          lookup.put(depName, p);
          
          // check its dependencies
          ok = checkDependencies(p.getTarget(), lookup, conflicts);
        } else {
          // we have to see if the version number for this package is compatible
          // with the one already in the lookup
          Dependency checkAgainst = lookup.get(depName);
          PackageConstraint result = checkAgainst.getTarget().checkConstraint(p.getTarget());
          if (result != null) {
            checkAgainst.setTarget(result);
            lookup.put(depName, checkAgainst);
          } else {
            // there is a conflict here
            List<Dependency> conflictList = conflicts.get(depName);
            conflictList.add(p);         
            ok = false;
          }
        }
      }
      
      return ok;
    }
    
    /**
     * Gets a full list of packages (encapsulated in Dependency
     * objects) that are required by directly and indirectly by the
     * named target package. Also builds a Map of any packages that
     * are required by more than one package and where there is
     * a conflict of some sort (e.g. multiple conflicting versions).
     *  The keys of this map are package names
     * (strings), and each associated value is a list of Dependency 
     * objects.
     * 
     * @param target the package for which a list of dependencies is
     * required.
     * @param conflicts will hold any conflicts that are discovered while
     * building the full dependency list.
     * @return a list of packages that are directly and indirectly required
     * by the named target package.
     * @throws Exception if a problem occurs while building the dependency
     * list.
     */
    public List<Dependency> getAllDependenciesForPackage(Package target, 
        Map<String, List<Dependency>> conflicts) 
      throws Exception {
      
      
      // start with the target package's list of dependencies
      List<Dependency> initialList = target.getDependencies();
      
      // load them into a map for quick lookup
      Map<String, Dependency> lookup = new HashMap<String, Dependency>();
          
      for (Dependency d : initialList) {
        lookup.put(d.getTarget().getPackage().
            getPackageMetaDataElement("PackageName").toString(), d);
        ArrayList<Dependency> deps = new ArrayList<Dependency>();
        deps.add(d);
        
        // Pre-load a conficts Map
        conflicts.put(d.getTarget().getPackage().
            getPackageMetaDataElement("PackageName").toString(), deps);
      }
      
      // now process each of these to build the full list
      for (Dependency d : initialList) {
        checkDependencies(d.getTarget(), lookup, conflicts);
      }
      
      List<Dependency> fullList = 
        new ArrayList<Dependency>(lookup.values());
      
      // Prune packages from conflicts Map that only have one
      // item in their list (i.e. these ones have no conflicts)
      ArrayList<String> removeList = new ArrayList<String>();
      Iterator<String> keyIt = conflicts.keySet().iterator();
      
      while (keyIt.hasNext()) {
        String key = keyIt.next();
        List<Dependency> tempD = conflicts.get(key);
        if (tempD.size() == 1) {
          // remove this one
          removeList.add(key);
        }
      }
      
      for (String s : removeList) {
        conflicts.remove(s);
      }
      
      return fullList;
    }
      
    /**
     * Install a package sourced from the repository.
     * 
     * @param packageName the name of the package to install
     * @param version the version of the package to install (may be null if not applicable).
     * @param progress optional varargs parameter, that, if supplied, is expected to contain one
     * or more PrintStream objects to write progress to.
     * @throws Exception if the package can't be installed for some reason.
     */
    public void installPackageFromRepository(String packageName, Object version,
        PrintStream... progress) 
      throws Exception {
      Package toInstall = getRepositoryPackageInfo(packageName, version);    
      
      String urlString = toInstall.getPackageMetaDataElement("PackageURL").toString();
      URL packageURL = new URL(urlString);
      
      installPackageFromURL(packageURL, progress);
    }
    
    /**
     * Install a package sourced from a given URL.
     * 
     * @param packageURL the URL to the package.
     * @param progress optional varargs parameter, that, if supplied, is expected to contain one
     * or more PrintStream objects to write progress to.
     * @return the name of the package installed
     * @throws Exception if the package can't be installed for some reason.
     */
    public String installPackageFromURL(URL packageURL, 
        PrintStream... progress) throws Exception {
      File downloaded = downloadPackage(packageURL, progress);
      return installPackageFromArchive(downloaded.getAbsolutePath(), progress);
    }
    
    private static void copyStreams(InputStream input, OutputStream output) throws IOException {
      int count;
      byte data[] = new byte[1024];
      while ((count = input.read(data, 0, 1024)) != -1) {
        output.write(data, 0, count);
      }
    }
    
    /**
     * Installs a package from a zip/jar archive.
     * 
     * @param packageArchivePath the full path to the archived package to install.
     * @param packageName the name of the package to install.
     * @param progress optional varargs parameter, that, if supplied, is expected to contain one
     * or more PrintStream objects to write progress to.
     * @throws Exception if the package can't be installed for some reason.
     */
    protected void installPackage(String packageArchivePath, 
        String packageName, PrintStream... progress) throws Exception {
      
      if (!establishPackageHome()) {
        throw new Exception("Unable to install " + packageArchivePath
            + " because package home (" + m_packageHome.getAbsolutePath() 
            + ") can't be established.");
      }
      
      File destDir = new File(m_packageHome, packageName);
      if (!destDir.mkdir()) {
        /*throw new Exception("Unable to create package directory "
            + destDir.getAbsolutePath()); */
        
        // hopefully failure is because the directory already exists
      }
  
      InputStream input = null;
      OutputStream output = null;
  
      ZipFile zipFile = new ZipFile(packageArchivePath);
      Enumeration enumeration = zipFile.entries();
      while (enumeration.hasMoreElements()) {
        ZipEntry zipEntry = (ZipEntry) enumeration.nextElement();
        if (zipEntry.isDirectory()) {
          new File(destDir, zipEntry.getName()).mkdir();
          continue;
        }
  
        for (int i = 0; i < progress.length; i++) {
          progress[i].println("[DefaultPackageManager] Installing: " + zipEntry.getName());
        }
  
        input = new BufferedInputStream(zipFile.getInputStream(zipEntry));
        File destFile = new File(destDir, zipEntry.getName());
        FileOutputStream fos = new FileOutputStream(destFile);
        output = new BufferedOutputStream(fos);
        copyStreams(input, output);
        input.close();
        output.flush();
        output.close();
      }
    }
    
    private URLConnection getConnection(String urlString) throws IOException {
      URL connURL = new URL(urlString);
      URLConnection conn = null;
      
      // setup the proxy (if we are using one) and open the connect
      if (setProxyAuthentication()) {
        conn = connURL.openConnection(m_httpProxy);
      } else {
        conn = connURL.openConnection();
      }
      
      // Set a timeout of 60 seconds for establishing the connection
      conn.setConnectTimeout(60000);
      
      return conn;
    }
    
    private void transToBAOS(BufferedInputStream bi, 
        ByteArrayOutputStream bos) throws Exception {
      while (true) {
        synchronized (m_buffer) {
          int amountRead = bi.read(m_buffer);
          if (amountRead == -1) {
            break;
          }
          bos.write(m_buffer, 0, amountRead); 
        }
      }
      
      bi.close();
    }
    
    private void writeZipEntryForPackage(String packageName, 
        ZipOutputStream zos) throws Exception {
      
      ZipEntry packageDir = new ZipEntry(packageName + "/");
      zos.putNextEntry(packageDir);
      
      ZipEntry z = new ZipEntry(packageName + "/Latest.props");
      ZipEntry z2 = new ZipEntry(packageName + "/Latest.html");
      URLConnection conn = getConnection(m_packageRepository.toString()
          + "/" + packageName + "/Latest.props");
      
      BufferedInputStream bi = new BufferedInputStream(conn.getInputStream());    
      ByteArrayOutputStream bos = new ByteArrayOutputStream();
      
      transToBAOS(bi, bos);
      zos.putNextEntry(z);
      zos.write(bos.toByteArray());
      
      conn = getConnection(m_packageRepository.toString()
          + "/" + packageName + "/Latest.html");
      bi = new BufferedInputStream(conn.getInputStream());
      bos = new ByteArrayOutputStream();
      transToBAOS(bi, bos);
      zos.putNextEntry(z2);
      zos.write(bos.toByteArray());
      
      // write the versions.txt file to the zip
      z = new ZipEntry(packageName + "/versions.txt");
      conn = getConnection(m_packageRepository.toString()
          + "/" + packageName + "/versions.txt");
      bi = new BufferedInputStream(conn.getInputStream());
      bos = new ByteArrayOutputStream();
      transToBAOS(bi, bos);
      zos.putNextEntry(z);
      zos.write(bos.toByteArray());
      
      // write the index.html to the zip
      z = new ZipEntry(packageName + "/index.html");
      conn = getConnection(m_packageRepository.toString()
          + "/" + packageName + "/index.html");
      bi = new BufferedInputStream(conn.getInputStream());
      bos = new ByteArrayOutputStream();
      transToBAOS(bi, bos);
      zos.putNextEntry(z);
      zos.write(bos.toByteArray());
      
      // Now process the available versions
      List<Object> versions = getRepositoryPackageVersions(packageName);
      for (Object o : versions) {
        conn = getConnection(m_packageRepository.toString()
            + "/" + packageName + "/" + o.toString() + ".props");
        z = new ZipEntry(packageName + "/" + o.toString() + ".props");
        bi = new BufferedInputStream(conn.getInputStream());
        bos = new ByteArrayOutputStream();
        transToBAOS(bi, bos);
        zos.putNextEntry(z);
        zos.write(bos.toByteArray());
        
        conn = getConnection(m_packageRepository.toString()
            + "/" + packageName + "/" + o.toString() + ".html");
        z = new ZipEntry(packageName + "/" + o.toString() + ".html");
        bi = new BufferedInputStream(conn.getInputStream());
        bos = new ByteArrayOutputStream();
        transToBAOS(bi, bos);
        zos.putNextEntry(z);
        zos.write(bos.toByteArray());      
      }
    }
    
    /**
     * Gets an array of bytes containing a zip of all the repository
     * meta data and supporting files. Does *not* contain any package
     * archives etc., only a snapshot of the meta data. Could be used
     * by clients to establish a cache of meta data.
     * 
     * @return a zip compressed array of bytes.
     */
    public byte[] getRepositoryPackageMetaDataOnlyAsZip(PrintStream... progress) 
      throws Exception {
      
      if (getPackageRepositoryURL() == null) {
        throw new Exception("[DefaultPackageManager] No package repository set!!");
      }        
      
      String packageList = m_packageRepository.toString() + "/packageList.txt";
      /*URL packageListURL = new URL(packageList);
      
      
      // setup the proxy (if we are using one) and open the connect
      if (setProxyAuthentication()) {
        conn = packageListURL.openConnection(m_httpProxy);
      } else {
        conn = packageListURL.openConnection();
      } */
      
      URLConnection conn = null;
      conn = getConnection(packageList);
      
      BufferedReader bi = 
        new BufferedReader(new InputStreamReader(conn.getInputStream()));
          
      ByteArrayOutputStream bos = new ByteArrayOutputStream();
      ZipOutputStream zos = new ZipOutputStream(bos);
      
      // Process the packages
      String packageName;
      while ((packageName = bi.readLine()) != null) {      
        for (PrintStream p : progress) {
          p.println("Fetching meta data for " + packageName);
        }
        writeZipEntryForPackage(packageName, zos);
      }
      bi.close();
      
      // include the package list in the zip
      conn = getConnection(packageList);
      ZipEntry z = new ZipEntry("packageList.txt");
      BufferedInputStream bi2 = new BufferedInputStream(conn.getInputStream());
      ByteArrayOutputStream bos2 = new ByteArrayOutputStream();
      transToBAOS(bi2, bos2);
      zos.putNextEntry(z);
      zos.write(bos2.toByteArray());
      bi2.close();
      
      // Include the top level images
      String imageList = m_packageRepository.toString() + "/images.txt";
      conn = getConnection(imageList);
      bi = new BufferedReader(new InputStreamReader(conn.getInputStream()));
      
      String imageName;
      while((imageName = bi.readLine()) != null) {
        // System.err.println("Processing " + imageName);
        z = new ZipEntry(imageName);
        URLConnection conn2 = getConnection(m_packageRepository.toString()
            + "/" + imageName);
        bi2 = 
          new BufferedInputStream(conn2.getInputStream());
        bos2 = new ByteArrayOutputStream();
        transToBAOS(bi2, bos2);
        zos.putNextEntry(z);
        zos.write(bos2.toByteArray());
        bi2.close();
      }
      
      // include the image list in the zip
      conn = getConnection(imageList);
      z = new ZipEntry("images.txt");
      bi2 = new BufferedInputStream(conn.getInputStream());
      bos2 = new ByteArrayOutputStream();
      transToBAOS(bi2, bos2);
      zos.putNextEntry(z);
      zos.write(bos2.toByteArray());
      bi2.close();
  
      zos.close();
      
      return bos.toByteArray();
    }
    
    /**
     * Get all packages that the system knows about (i.e. all packages
     * contained in the repository).
     * 
     * @param progress optional varargs parameter, that, if supplied is
     * expected to contain one or more PrintStream objects to write
     * progress to.
     * @return a list of all packages.
     * @throws Exception if a list of packages can't be determined.
     */
    public List<Package> getAllPackages(PrintStream... progress) 
      throws Exception {
      ArrayList<Package> allPackages = new ArrayList<Package>();
      
      if (getPackageRepositoryURL() == null) {
        throw new Exception("[DefaultPackageManager] No package repository set!!");
      }        
      
      String packageList = m_packageRepository.toString() + "/packageList.txt";
  
      URL packageListURL = new URL(packageList);
      URLConnection conn = null;
      
      // setup the proxy (if we are using one) and open the connect
      if (setProxyAuthentication()) {
        conn = packageListURL.openConnection(m_httpProxy);
      } else {
        conn = packageListURL.openConnection();
      }
      
  
      BufferedReader bi = 
        new BufferedReader(new InputStreamReader(conn.getInputStream()));
      
      String packageName;
      while ((packageName = bi.readLine()) != null) {
        Package temp = getRepositoryPackageInfo(packageName);
        allPackages.add(temp);
      }
      
      return allPackages;
    }
    
    /**
     * Get a list of packages that are not currently installed.
     * 
     * @return a list of packages that are not currently installed.
     * @throws Exception if a list of packages can't be determined.
     */
    public List<Package> getAvailablePackages() throws Exception {
      List<Package> allP = getAllPackages();
      List<Package> available = new ArrayList<Package>();
      
      for (int i = 0; i < allP.size(); i++) {
        if (!allP.get(i).isInstalled()) {
          available.add(allP.get(i));
        }
      }
      
      return available;
    }
    
    /**
     * Get a list of installed packages.
     * 
     * @return a list of installed packages.
     * @throws Exception if a list of packages can't be determined.
     */
    public List<Package> getInstalledPackages() throws Exception {
      if (!establishPackageHome()) {
        throw new Exception("Unable to get list of installed packages "
            + "because package home (" + m_packageHome.getAbsolutePath() 
            + ") can't be established.");
      }
      List<Package> installedP = new ArrayList<Package>();
      
      File[] contents = m_packageHome.listFiles();
      
      for (int i = 0; i < contents.length; i++) {
        if (contents[i].isDirectory()) {
          File description = new File(contents[i].getAbsolutePath() 
              + File.separator + "Description.props");
          
          if (description.exists()) {
            try {
              Properties packageProperties = new Properties();
              BufferedInputStream bi = new BufferedInputStream(new FileInputStream(description));
              packageProperties.load(bi);
              bi.close();
              bi = null;
              DefaultPackage pkg = new DefaultPackage(m_packageHome, this, packageProperties);
              installedP.add(pkg);
            } catch (Exception ex) {
              // ignore if we can't load the description file for some reason
            }
          }
        }
      }
      
      return installedP;
    }
    
    /**
     * Pads a string to a specified length, inserting spaces on the left
     * as required. If the string is too long, characters are removed (from
     * the right).
     *
     * @param inString the input string
     * @param length the desired length of the output string
     * @return the output string
     */
    protected static String padLeft(String inString, int length) {
  
      return fixStringLength(inString, length, false);
    }
    
    /**
     * Pads a string to a specified length, inserting spaces on the right
     * as required. If the string is too long, characters are removed (from
     * the right).
     *
     * @param inString the input string
     * @param length the desired length of the output string
     * @return the output string
     */
    protected static String padRight(String inString, int length) {
  
      return fixStringLength(inString, length, true);
    }
    
    /**
     * Pads a string to a specified length, inserting spaces as
     * required. If the string is too long, characters are removed (from
     * the right).
     *
     * @param inString the input string
     * @param length the desired length of the output string
     * @param right true if inserted spaces should be added to the right
     * @return the output string
     */
    private static /*@pure@*/ String fixStringLength(String inString, int length,
                                          boolean right) {
  
      if (inString.length() < length) {
        while (inString.length() < length) {
          inString = (right ? inString.concat(" ") : " ".concat(inString));
        }
      } else if (inString.length() > length) {
        inString = inString.substring(0, length);
      }
      return inString;
    }
    
    /*public void printPackageInfo(String packagePath) throws Exception {
      Properties packageProps = (Properties)getPackageArchiveInfo(packagePath);
      Enumeration<?> e = packageProps.propertyNames();
      while (e.hasMoreElements()) {
        String key = (String) e.nextElement();
        String value = packageProps.getProperty(key);
        System.out.println(padLeft(key, 11) + ":\t" + value);
      }
    } */
    
    public static void main(String[] args) {
      try {
        URL url = new URL(args[0]);
        DefaultPackageManager pm = new DefaultPackageManager();
        pm.downloadPackage(url, System.out);
        
      } catch (Exception ex) {
        ex.printStackTrace();
      }
    }
  }
  