All Downloads are FREE. Search and download functionalities are using the official Maven repository.

weka.core.WekaPackageManager Maven / Gradle / Ivy

Go to download

The Waikato Environment for Knowledge Analysis (WEKA), a machine learning workbench. This version represents the developer version, the "bleeding edge" of development, you could say. New functionality gets added to this version.

There is a newer version: 3.9.6
Show newest version
/*
 *   This program is free software: you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License as published by
 *   the Free Software Foundation, either version 3 of the License, or
 *   (at your option) any later version.
 *
 *   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 General Public License for more details.
 *
 *   You should have received a copy of the GNU General Public License
 *   along with this program.  If not, see .
 */

/*
 *    WekaPackageManager.java
 *    Copyright (C) 2009-2013 University of Waikato, Hamilton, New Zealand
 */

package weka.core;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.PrintStream;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.net.URLConnection;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

import org.pentaho.packageManagement.DefaultPackageManager;
import org.pentaho.packageManagement.Dependency;
import org.pentaho.packageManagement.Package;
import org.pentaho.packageManagement.PackageConstraint;
import org.pentaho.packageManagement.PackageManager;

import weka.core.converters.ConverterUtils;
import weka.gui.GenericObjectEditor;
import weka.gui.GenericPropertiesCreator;
import weka.gui.beans.BeansProperties;
import weka.gui.beans.KnowledgeFlowApp;
import weka.gui.beans.PluginManager;
import weka.gui.explorer.ExplorerDefaults;

/**
 * Class providing package management and manipulation routines. Also provides a
 * command line interface for package management.
 * 
 * @author Mark Hall (mhall{[at]}pentaho{[dot]}com)
 * @version $Revision: 10626 $
 */
public class WekaPackageManager {

  /** The default folder name for Weka bits and bobs */
  private static String WEKAFILES_DIR_NAME = "wekafiles";

  /** Default path to where Weka's configuration and packages are stored */
  public static File WEKA_HOME = new File(System.getProperty("user.home")
    + File.separator + WEKAFILES_DIR_NAME);

  /** The default packages directory */
  public static File PACKAGES_DIR = new File(WEKA_HOME.toString()
    + File.separator + "packages");

  /** The default props dir name */
  private static String PROPERTIES_DIR_NAME = "props";

  /** The default properties directory */
  public static File PROPERTIES_DIR = new File(WEKA_HOME.toString()
    + File.separator + PROPERTIES_DIR_NAME);

  /** The underlying package manager */
  private static PackageManager PACKAGE_MANAGER = PackageManager.create();

  /** Current repository URL to use */
  private static URL REP_URL;

  /** Location of the repository cache */
  private static URL CACHE_URL;

  /** True if a cache build is required */
  private static boolean INITIAL_CACHE_BUILD_NEEDED = false;

  /** The name of the file that contains the list of packages in the repository */
  private static String PACKAGE_LIST_FILENAME = "packageListWithVersion.txt";

  /** Primary repository */
  private static String PRIMARY_REPOSITORY =
    "http://weka.sourceforge.net/packageMetaData";

  /** Backup mirror of the repository */
  private static String REP_MIRROR;

  /**
   * True if the user has specified a custom repository via a property in
   * PackageManager.props
   */
  private static boolean USER_SET_REPO = false;

  /** The package manager's property file */
  private static String PACKAGE_MANAGER_PROPS_FILE_NAME =
    "PackageManager.props";

  /** Operating offline? */
  public static boolean m_offline;

  /** Load packages? */
  private static boolean m_loadPackages = true;

  /** Established WEKA_HOME successfully? */
  protected static boolean m_wekaHomeEstablished;

  /** Packages loaded OK? */
  protected static boolean m_packagesLoaded;

  /** Package loading in progress? */
  public static boolean m_initialPackageLoadingInProcess = false;

  /* True if an initial cache build is needed and working offline */
  public static boolean m_noPackageMetaDataAvailable;

  /** The set of packages that the user has requested not to load */
  public static Set m_doNotLoadList;

  static {
    establishWekaHome();
  }

  /**
   * Establish WEKA_HOME if needed
   * 
   * @return true if WEKA_HOME was successfully established
   */
  protected static boolean establishWekaHome() {
    if (m_wekaHomeEstablished) {
      return true;
    }
    // look to see if WEKA_HOME has been defined as an environment
    // variable
    Environment env = Environment.getSystemWide();
    String wh = env.getVariableValue("WEKA_HOME");
    if (wh != null) {
      WEKA_HOME = new File(wh);
      PACKAGES_DIR = new File(wh + File.separator + "packages");
      PROPERTIES_DIR = new File(wh + File.separator + PROPERTIES_DIR_NAME);
    } else {
      env.addVariableSystemWide("WEKA_HOME", WEKA_HOME.toString());
    }

    boolean ok = true;
    if (!WEKA_HOME.exists()) {
      // create it for the user
      if (!WEKA_HOME.mkdir()) {
        System.err.println("Unable to create WEKA_HOME ("
          + WEKA_HOME.getAbsolutePath() + ")");
        ok = false;
      }
    }

    if (!PACKAGES_DIR.exists()) {
      // create the packages dir
      if (!PACKAGES_DIR.mkdir()) {
        System.err.println("Unable to create packages directory ("
          + PACKAGES_DIR.getAbsolutePath() + ")");
        ok = false;
      }
    }

    m_wekaHomeEstablished = ok;
    PACKAGE_MANAGER.setPackageHome(PACKAGES_DIR);

    m_doNotLoadList = getDoNotLoadList();
    try {
      // setup the backup mirror first
      // establishMirror();

      // user-supplied repository URL takes precedence over anything else
      String repURL =
        env.getVariableValue("weka.core.wekaPackageRepositoryURL");
      if (repURL == null || repURL.length() == 0) {
        // See if there is a URL named in
        // $WEKA_HOME/props/PackageRepository.props
        File repPropsFile =
          new File(PROPERTIES_DIR.toString() + File.separator
            + "PackageRepository.props");

        if (repPropsFile.exists()) {
          Properties repProps = new Properties();
          repProps.load(new FileInputStream(repPropsFile));
          repURL = repProps.getProperty("weka.core.wekaPackageRepositoryURL");
        }
      }

      if (repURL == null || repURL.length() == 0) {
        repURL = PRIMARY_REPOSITORY;
      } else {
        log(weka.core.logging.Logger.Level.INFO,
          "[WekaPackageManager] weka.core.WekaPackageRepositoryURL = " + repURL);
        // System.err.println("[WekaPackageManager] weka.core.WekaPackageRepositoryURL = "
        // + repURL);
        USER_SET_REPO = true;
      }

      REP_URL = new URL(repURL);
      PACKAGE_MANAGER.setPackageRepositoryURL(REP_URL);
    } catch (MalformedURLException ex) {
      ex.printStackTrace();
    } catch (IOException ex) {
      ex.printStackTrace();
    }

    PACKAGE_MANAGER.setBaseSystemName("weka");
    PACKAGE_MANAGER.setBaseSystemVersion(weka.core.Version.VERSION);

    // Now check the cache and establish it if necessary
    File cacheDir =
      new File(WEKA_HOME.toString() + File.separator + "repCache");
    try {
      String tempCacheString = "file://" + cacheDir.toString();
      // sanitize URI and fix slashes (for Windows)
      tempCacheString = tempCacheString.replace(" ", "%20");
      tempCacheString = tempCacheString.replace('\\', '/');
      if (tempCacheString.startsWith("file://")
        && !tempCacheString.startsWith("file:///")) {
        tempCacheString = tempCacheString.substring(7);
        tempCacheString = "file:///" + tempCacheString;
      }
      URI tempURI = new URI(tempCacheString);
      // System.err.println(" -- " + tempURI.toString());
      CACHE_URL = tempURI.toURL();
    } catch (Exception e) {
      e.printStackTrace();
    }

    File packagesList =
      new File(cacheDir.getAbsolutePath() + File.separator
        + PACKAGE_LIST_FILENAME);
    if (!cacheDir.exists()) {
      if (!cacheDir.mkdir()) {
        System.err.println("Unable to create repository cache directory ("
          + cacheDir.getAbsolutePath() + ")");
        log(
          weka.core.logging.Logger.Level.WARNING,
          "Unable to create repository cache directory ("
            + cacheDir.getAbsolutePath() + ")");
        CACHE_URL = null;
      } else {
        // refreshCache();
        INITIAL_CACHE_BUILD_NEEDED = true;
      }
    }

    if (!packagesList.exists()) {
      INITIAL_CACHE_BUILD_NEEDED = true;
    }

    // Package manager general properties
    // Set via system props first
    String offline = env.getVariableValue("weka.packageManager.offline");
    if (offline != null) {
      m_offline = offline.equalsIgnoreCase("true");
    }
    String loadPackages =
      env.getVariableValue("weka.packageManager.loadPackages");
    if (loadPackages == null) {
      // try legacy
      loadPackages = env.getVariableValue("weka.core.loadPackages");
    }

    if (loadPackages != null) {
      m_loadPackages = loadPackages.equalsIgnoreCase("true");
    }

    // load any general package manager properties from props file
    File generalProps =
      new File(PROPERTIES_DIR.toString() + File.separator
        + PACKAGE_MANAGER_PROPS_FILE_NAME);
    if (generalProps.exists()) {
      Properties gProps = new Properties();
      try {
        gProps.load(new FileInputStream(generalProps));
        // this one takes precedence over the legacy one
        String repURL =
          gProps.getProperty("weka.core.wekaPackageRepositoryURL");
        if (repURL != null && repURL.length() > 0) {
          REP_URL = new URL(repURL);
          PACKAGE_MANAGER.setPackageRepositoryURL(REP_URL);
        }

        offline = gProps.getProperty("weka.packageManager.offline");
        if (offline != null && offline.length() > 0) {
          m_offline = offline.equalsIgnoreCase("true");
        }

        loadPackages = gProps.getProperty("weka.packageManager.loadPackages");
        if (loadPackages == null) {
          // try legacy
          loadPackages = env.getVariableValue("weka.core.loadPackages");
        }
        if (loadPackages != null) {
          m_loadPackages = loadPackages.equalsIgnoreCase("true");
        }

        String pluginManagerDisableList =
          gProps.getProperty("weka.pluginManager.disable");
        if (pluginManagerDisableList != null
          && pluginManagerDisableList.length() > 0) {
          List disable = new ArrayList();
          String[] parts = pluginManagerDisableList.split(",");
          for (String s : parts) {
            disable.add(s.trim());
          }
          PluginManager.addToDisabledList(disable);
        }
      } catch (FileNotFoundException e) {
        e.printStackTrace();
      } catch (IOException e) {
        e.printStackTrace();
      }
    }

    if (INITIAL_CACHE_BUILD_NEEDED && m_offline) {
      m_noPackageMetaDataAvailable = true;
    }

    // Pass a valid http URL to the setProxyAuthentication()
    // method to ensure that a proxy or direct connection
    // is configured initially. This will prevent this method from
    // trying to configure the ProxySelector and proxy
    // via the file-based CACHE_URL somewhere down the track
    PACKAGE_MANAGER.setPackageRepositoryURL(REP_URL);
    PACKAGE_MANAGER.setProxyAuthentication(REP_URL);

    return ok;
  }

  /**
   * Establish the location of a mirror
   */
  protected static void establishMirror() {
    if (m_offline) {
      return;
    }

    try {
      String mirrorListURL =
        "http://www.cs.waikato.ac.nz/ml/weka/packageMetaDataMirror.txt";

      URLConnection conn = null;
      URL connURL = new URL(mirrorListURL);

      if (PACKAGE_MANAGER.setProxyAuthentication(connURL)) {
        conn = connURL.openConnection(PACKAGE_MANAGER.getProxy());
      } else {
        conn = connURL.openConnection();
      }

      conn.setConnectTimeout(10000); // timeout after 10 seconds
      conn.setReadTimeout(10000);

      BufferedReader bi =
        new BufferedReader(new InputStreamReader(conn.getInputStream()));

      REP_MIRROR = bi.readLine();

      bi.close();
      if (REP_MIRROR != null && REP_MIRROR.length() > 0) {
        // use the mirror if it is different from the primary repo
        // and the user hasn't specified an explicit repo via the
        // property
        if (!REP_MIRROR.equals(PRIMARY_REPOSITORY) && !USER_SET_REPO) {

          log(weka.core.logging.Logger.Level.INFO,
            "[WekaPackageManager] Package manager using repository mirror: "
              + REP_MIRROR);

          REP_URL = new URL(REP_MIRROR);
        }
      }
    } catch (Exception ex) {
      log(weka.core.logging.Logger.Level.WARNING,
        "[WekaPackageManager] The repository meta data mirror file seems "
          + "to be unavailable (" + ex.getMessage() + ")");
    }
  }

  /**
   * Write to the weka log
   * 
   * @param level logging level
   * @param message message to write
   */
  protected static void log(weka.core.logging.Logger.Level level, String message) {
    try {
      File logFile =
        new File(WEKA_HOME.toString() + File.separator + "weka.log");
      BufferedWriter writer = new BufferedWriter(new FileWriter(logFile, true));
      SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
      String linefeed = System.getProperty("line.separator");
      String m =
        format.format(new Date()) + " " + level + ": " + message + linefeed;
      writer.write(m);
      writer.flush();
      writer.close();
    } catch (Exception ex) {
    }
  }

  /**
   * Remove any ExplorerDefaults properties specified in the supplied package
   * 
   * @param installedPackageName the package specifying properties that should
   *          be removed from ExplorerDefaults
   */
  public static void removeExplorerProps(String installedPackageName) {
    try {
      Properties expProps = new Properties();
      String explorerProps =
        getPackageHome().getAbsolutePath() + File.separator
          + installedPackageName + File.separator + "Explorer.props";
      BufferedInputStream bi =
        new BufferedInputStream(new FileInputStream(explorerProps));
      expProps.load(bi);
      bi.close();
      bi = null;
      Set keys = expProps.keySet();
      Iterator keysI = keys.iterator();
      while (keysI.hasNext()) {
        String key = (String) keysI.next();
        if (!key.endsWith("Policy")) {
          // See if this key is in the Explorer props
          String existingVal = ExplorerDefaults.get(key, "");
          String toRemove = expProps.getProperty(key);
          if (existingVal.length() > 0) {
            // cover the case when the value to remove is at the start
            // or middle of a list
            existingVal = existingVal.replace(toRemove + ",", "");

            // the case when it's at the end
            existingVal = existingVal.replace("," + toRemove, "");
            ExplorerDefaults.set(key, existingVal);
          }
        }
      }
    } catch (Exception ex) {
    }
  }

  /**
   * Process a package's GenericPropertiesCreator.props file
   * 
   * @param propsFile the props file to process
   */
  protected static void processGenericPropertiesCreatorProps(File propsFile) {
    try {
      Properties expProps = new Properties();
      BufferedInputStream bi =
        new BufferedInputStream(new FileInputStream(propsFile));
      expProps.load(bi);
      bi.close();
      bi = null;
      Properties GPCInputProps =
        GenericPropertiesCreator.getGlobalInputProperties();

      Set keys = expProps.keySet();
      Iterator keysI = keys.iterator();
      while (keysI.hasNext()) {
        String key = (String) keysI.next();
        // see if this key is in the GPC input props
        String existingVal = GPCInputProps.getProperty(key, "");
        if (existingVal.length() > 0) {
          // append
          String newVal = expProps.getProperty(key);
          // only append if this value is not already there!!
          if (existingVal.indexOf(newVal) < 0) {
            newVal = existingVal + "," + newVal;
            GPCInputProps.put(key, newVal);
          }
        } else {
          // simply add this new key/value combo
          String newVal = expProps.getProperty(key);
          GPCInputProps.put(key, newVal);
        }
      }
    } catch (Exception ex) {
      // ignore
    }
  }

  /**
   * Process a package's Explorer.props file
   * 
   * @param propsFile the properties file to process
   */
  protected static void processExplorerProps(File propsFile) {
    try {
      Properties expProps = new Properties();
      BufferedInputStream bi =
        new BufferedInputStream(new FileInputStream(propsFile));
      expProps.load(bi);
      bi.close();
      bi = null;
      Set keys = expProps.keySet();
      Iterator keysI = keys.iterator();
      while (keysI.hasNext()) {
        String key = (String) keysI.next();
        if (!key.endsWith("Policy")) {
          // See if this key is in the Explorer props
          String existingVal = ExplorerDefaults.get(key, "");
          if (existingVal.length() > 0) {
            // get the replacement policy (if any)
            String replacePolicy = expProps.getProperty(key + "Policy");
            if (replacePolicy != null && replacePolicy.length() > 0) {
              if (replacePolicy.equalsIgnoreCase("replace")) {
                String newVal = expProps.getProperty(key);
                ExplorerDefaults.set(key, newVal);
              } else {
                // default to append
                String newVal = expProps.getProperty(key);

                // only append if this value is not already there!!
                if (existingVal.indexOf(newVal) < 0) {
                  newVal = existingVal + "," + newVal;
                  ExplorerDefaults.set(key, newVal);
                }
              }
            } else {
              // default to append
              String newVal = expProps.getProperty(key);
              // only append if this value is not already there!!
              if (existingVal.indexOf(newVal) < 0) {
                newVal = existingVal + "," + newVal;
                ExplorerDefaults.set(key, newVal);
              }
            }
          } else {
            // simply add this new key/value combo
            String newVal = expProps.getProperty(key);
            ExplorerDefaults.set(key, newVal);
          }
        }
      }
    } catch (Exception ex) {
      // ignore
    }
  }

  /**
   * Process a package's GUIEditors.props file
   * 
   * @param propsFile the properties file to process
   */
  protected static void processGUIEditorsProps(File propsFile) {
    GenericObjectEditor.registerEditors();
    try {
      Properties editorProps = new Properties();
      BufferedInputStream bi =
        new BufferedInputStream(new FileInputStream(propsFile));
      editorProps.load(bi);
      bi.close();
      bi = null;

      Enumeration enm = editorProps.propertyNames();
      while (enm.hasMoreElements()) {
        String name = enm.nextElement().toString();
        String value = editorProps.getProperty(name, "");
        System.err.println("Registering " + name + " " + value);
        GenericObjectEditor.registerEditor(name, value);
      }

    } catch (Exception ex) {
      // ignore
    }
  }

  /**
   * Process a package's PluginManager.props file
   * 
   * @param propsFile the properties file to process
   */
  protected static void processPluginManagerProps(File propsFile) {
    try {
      PluginManager.addFromProperties(propsFile);
    } catch (Exception ex) {
    }
  }

  /**
   * Recursively load the jars and process properties in a package directory
   * 
   * @param directory the package directory to process
   * @param verbose true for verbose output
   * @param goePropsFiles store any GenericObjectEditor.props files for
   *          post-processing after all jars have been loaded
   * @throws Exception if a problem occurs
   */
  protected static void loadPackageDirectory(File directory, boolean verbose,
    List goePropsFiles, boolean avoidTriggeringFullClassDiscovery)
    throws Exception {
    File[] contents = directory.listFiles();

    // make sure that jar files and lib directory get processed first
    for (File content : contents) {
      if (content.isFile() && content.getPath().endsWith(".jar")) {
        if (verbose) {
          System.out.println("[Weka] loading " + content.getPath());
        }
        ClassloaderUtil.addFile(content.getPath());
      } else if (content.isDirectory()
        && content.getName().equalsIgnoreCase("lib")) {
        // add any jar files in the lib directory to the classpath
        loadPackageDirectory(content, verbose, goePropsFiles,
          avoidTriggeringFullClassDiscovery);
      }
    }

    // now any auxilliary files
    for (File content : contents) {
      if (content.isFile() && content.getPath().endsWith("Beans.props")) {
        // KnowledgeFlow plugin -- add the Beans.props file to the list of
        // bean plugin props

        BeansProperties.addToPluginBeanProps(content);

        if (!avoidTriggeringFullClassDiscovery) {
          KnowledgeFlowApp.disposeSingleton();
        }
      } else if (content.isFile()
        && content.getPath().endsWith("Explorer.props")
        && !avoidTriggeringFullClassDiscovery) {
        // Explorer tabs plugin
        // process the keys in the properties file and append/add values
        processExplorerProps(content);
      } else if (content.isFile()
        && content.getPath().endsWith("GUIEditors.props")
        && !avoidTriggeringFullClassDiscovery) {
        // Editor for a particular component
        processGUIEditorsProps(content);
      } else if (content.isFile()
        && content.getPath().endsWith("GenericPropertiesCreator.props")
        && !avoidTriggeringFullClassDiscovery) {
        if (goePropsFiles != null) {
          goePropsFiles.add(content);
        } else {
          processGenericPropertiesCreatorProps(content);
        }
      } else if (content.isFile()
        && content.getPath().endsWith("PluginManager.props")) {
        processPluginManagerProps(content);
      }
    }
  }

  /**
   * Check whether a package should be loaded or not. Checks for missing
   * classes, unset environment variables, missing dependencies etc.
   * 
   * @param toLoad the package to check
   * @param packageRoot the root directory of the package
   * @param progress for reporting loading progress
   * @return true if the package is good to load
   */
  public static boolean loadCheck(Package toLoad, File packageRoot,
    PrintStream... progress) {

    // first check against the base version of the system
    boolean load;
    try {
      load = toLoad.isCompatibleBaseSystem();
    } catch (Exception ex) {
      ex.printStackTrace();
      return false;
    }

    if (!load) {
      for (PrintStream p : progress) {
        p.println("[Weka] Skipping package " + toLoad.getName()
          + " because it is not compatible with Weka "
          + PACKAGE_MANAGER.getBaseSystemVersion().toString());
      }
      return false;
    }

    load = !m_doNotLoadList.contains(toLoad.getName());
    if (!load) {
      for (PrintStream p : progress) {
        p.println("[Weka] Skipping package " + toLoad.getName()
          + " because it is has been marked as do not load");
      }
      return false;
    }

    // first check for missing special files/directories and
    // missing external classes (if any)

    if (!(checkForMissingClasses(toLoad, progress) && checkForMissingFiles(
      toLoad, packageRoot, progress))) {
      return false;
    }

    // check for unset environment variables
    if (!checkForUnsetEnvVar(toLoad, progress)) {
      return false;
    }

    if (m_offline) {
      return true;
    }

    // now check for missing dependencies
    try {
      List missing = toLoad.getMissingDependencies();
      if (missing.size() > 0) {
        for (PrintStream p : progress) {
          p.println("[Weka] " + toLoad.getName()
            + " can't be loaded because the following"
            + " packages are missing:");
          for (Dependency d : missing) {
            p.println(d.getTarget());
          }
        }
        return false;
      }
    } catch (Exception ex) {
      ex.printStackTrace();
      return false;
    }

    // now check for buggered dependencies
    try {
      List depends = toLoad.getDependencies();
      for (Dependency d : depends) {
        if (d.getTarget().getPackage().isInstalled()) {
          if (!loadCheck(d.getTarget().getPackage(), packageRoot, progress)) {
            for (PrintStream p : progress) {
              p.println("[Weka] Can't load " + toLoad.getName() + " because "
                + d.getTarget() + " can't be loaded.");
            }
            return false;
          }
        }
      }
    } catch (Exception ex) {
      ex.printStackTrace();
      return false;
    }

    return true;
  }

  /**
   * Checks to see if there are any environment variables or properties that
   * should be set at startup before allowing this package to be loaded. This is
   * useful for packages that might not be able to function correctly if certain
   * variables are not set correctly.
   * 
   * @param toLoad the package to check
   * @return true if good to go
   */
  public static boolean checkForUnsetEnvVar(Package toLoad,
    PrintStream... progress) {
    Object doNotLoadIfUnsetVar =
      toLoad.getPackageMetaDataElement("DoNotLoadIfEnvVarNotSet");

    boolean result = true;
    if (doNotLoadIfUnsetVar != null
      && doNotLoadIfUnsetVar.toString().length() > 0) {
      String[] elements = doNotLoadIfUnsetVar.toString().split(",");

      Environment env = Environment.getSystemWide();

      for (String var : elements) {
        if (env.getVariableValue(var.trim()) == null) {
          for (PrintStream p : progress) {
            p.println("[Weka] " + toLoad.getName()
              + " can't be loaded because " + "the environment variable " + var
              + " is not set.");
          }

          result = false;
          break;
        }
      }
    }

    if (!result) {
      // grab the message to print to the log (if any)
      Object doNotLoadMessage =
        toLoad.getPackageMetaDataElement("DoNotLoadIfEnvVarNotSetMessage");
      if (doNotLoadMessage != null && doNotLoadMessage.toString().length() > 0) {
        for (PrintStream p : progress) {
          String dnlM = doNotLoadMessage.toString();
          try {
            dnlM = Environment.getSystemWide().substitute(dnlM);
          } catch (Exception e) {
            // quietly ignore
          }
          p.println("[Weka] " + dnlM);
        }
      }
    }

    return result;
  }

  /**
   * Checks to see if there are any classes that we should try to instantiate
   * before allowing this package to be loaded. This is useful for checking to
   * see if third-party classes are accessible. An example would be Java3D,
   * which has an installer that installs into the JRE/JDK.
   * 
   * @param toLoad the package to check
   * @return true if good to go
   */
  public static boolean checkForMissingClasses(Package toLoad,
    PrintStream... progress) {
    boolean result = true;
    Object doNotLoadIfClassNotInstantiable =
      toLoad.getPackageMetaDataElement("DoNotLoadIfClassNotPresent");

    if (doNotLoadIfClassNotInstantiable != null
      && doNotLoadIfClassNotInstantiable.toString().length() > 0) {

      StringTokenizer tok =
        new StringTokenizer(doNotLoadIfClassNotInstantiable.toString(), ",");
      while (tok.hasMoreTokens()) {
        String nextT = tok.nextToken().trim();
        try {
          Class.forName(nextT);
        } catch (Exception ex) {
          for (PrintStream p : progress) {
            p.println("[Weka] " + toLoad.getName()
              + " can't be loaded because " + nextT + " can't be instantiated.");
          }
          result = false;
          break;
        }
      }
    }

    if (!result) {
      // grab the message to print to the log (if any)
      Object doNotLoadMessage =
        toLoad.getPackageMetaDataElement("DoNotLoadIfClassNotPresentMessage");
      if (doNotLoadMessage != null && doNotLoadMessage.toString().length() > 0) {
        for (PrintStream p : progress) {
          String dnlM = doNotLoadMessage.toString();
          try {
            dnlM = Environment.getSystemWide().substitute(dnlM);
          } catch (Exception e) {
            // quietly ignore
          }
          p.println("[Weka] " + dnlM);
        }
      }
    }

    return result;
  }

  /**
   * Checks to see if there are any missing files/directories for a given
   * package. If there are missing files, then the package can't be loaded. An
   * example would be a connector package that, for whatever reason, can't
   * include a necessary third-party jar file in its lib folder, and requires
   * the user to download and install this jar file manually.
   * 
   * @param toLoad the package to check
   * @param packageRoot the root directory of the package
   * @return true if good to go
   */
  public static boolean checkForMissingFiles(Package toLoad, File packageRoot,
    PrintStream... progress) {
    boolean result = true;

    Object doNotLoadIfFileMissing =
      toLoad.getPackageMetaDataElement("DoNotLoadIfFileNotPresent");
    String packageRootPath = packageRoot.getPath() + File.separator;

    if (doNotLoadIfFileMissing != null
      && doNotLoadIfFileMissing.toString().length() > 0) {

      StringTokenizer tok =
        new StringTokenizer(doNotLoadIfFileMissing.toString(), ",");
      while (tok.hasMoreTokens()) {
        String nextT = tok.nextToken().trim();
        File toCheck = new File(packageRootPath + nextT);
        if (!toCheck.exists()) {
          for (PrintStream p : progress) {
            p.println("[Weka] " + toLoad.getName()
              + " can't be loaded because " + toCheck.getPath()
              + " appears to be missing.");
          }
          result = false;
          break;
        }
      }
    }

    if (!result) {
      // grab the message to print to the log (if any)
      Object doNotLoadMessage =
        toLoad.getPackageMetaDataElement("DoNotLoadIfFileNotPresentMessage");
      if (doNotLoadMessage != null && doNotLoadMessage.toString().length() > 0) {
        String dnlM = doNotLoadMessage.toString();
        try {
          dnlM = Environment.getSystemWide().substitute(dnlM);
        } catch (Exception ex) {
          // quietly ignore
        }
        for (PrintStream p : progress) {
          p.println("[Weka] " + dnlM);
        }
      }
    }

    return result;
  }

  /**
   * Reads the doNotLoad list (if it exists) from the packages directory
   * 
   * @return a set of package names that should not be loaded. This will be
   *         empty if the doNotLoadList does not exist on disk.
   */
  @SuppressWarnings("unchecked")
  protected static Set getDoNotLoadList() {

    Set doNotLoad = new HashSet();
    File doNotLoadList =
      new File(PACKAGES_DIR.toString() + File.separator + "doNotLoad.ser");
    if (doNotLoadList.exists() && doNotLoadList.isFile()) {
      ObjectInputStream ois = null;

      try {
        ois =
          new ObjectInputStream(new BufferedInputStream(new FileInputStream(
            doNotLoadList)));
        doNotLoad = (Set) ois.readObject();
      } catch (FileNotFoundException ex) {
      } catch (IOException e) {
        System.err
          .println("An error occurred while reading the doNotLoad list: "
            + e.getMessage());
      } catch (ClassNotFoundException e) {
        System.err
          .println("An error occurred while reading the doNotLoad list: "
            + e.getMessage());
      } finally {
        if (ois != null) {
          try {
            ois.close();
          } catch (IOException ex) {
            ex.printStackTrace();
          }
        }
      }
    }

    return doNotLoad;
  }

  /**
   * Toggle the load status of the supplied list of package names
   * 
   * @param packageNames the packages to toggle the load status for
   * @return a list of unknown packages (i.e. any supplied package names that
   *         don't appear to be installed)
   * @throws Exception if a problem occurs
   */
  public static List toggleLoadStatus(List packageNames)
    throws Exception {

    List unknownPackages = new ArrayList();
    boolean changeMade = false;
    for (String s : packageNames) {
      Package p = PACKAGE_MANAGER.getInstalledPackageInfo(s);
      if (p == null) {
        unknownPackages.add(s);
      } else {
        if (m_doNotLoadList.contains(s)) {
          m_doNotLoadList.remove(s);
        } else {
          // only mark as do not load if a package is loadable
          if (loadCheck(p, new File(WekaPackageManager.getPackageHome()
            .toString() + File.separator + s))) {
            m_doNotLoadList.add(s);
          }
        }
        changeMade = true;
      }
    }

    if (changeMade) {
      // write the list back to disk
      File doNotLoadList =
        new File(PACKAGES_DIR.toString() + File.separator + "doNotLoad.ser");
      ObjectOutputStream oos = null;
      try {
        oos =
          new ObjectOutputStream(new BufferedOutputStream(new FileOutputStream(
            doNotLoadList)));
        oos.writeObject(m_doNotLoadList);
      } finally {
        if (oos != null) {
          oos.flush();
          oos.close();
        }
      }
    }

    return unknownPackages;
  }

  /**
   * Load all packages
   * 
   * @param verbose true if loading progress should be output
   */
  public static synchronized void loadPackages(boolean verbose) {
    loadPackages(verbose, false, true);
  }

  /**
   * Load all packages
   * 
   * @param verbose true if loading progress should be output
   * @param avoidTriggeringFullClassDiscovery true if we should avoid processing
   *          any properties files that might cause a full class discovery run,
   *          and may involve instantiating GUI classes.
   * @param refreshGOEProperties true if the GOE properties should be refreshed
   *          after loading (i.e. a re-run of the class discovery mechanism,
   *          re-initialization of the Knowledge Flow etc.)
   */
  public static synchronized void loadPackages(boolean verbose,
    boolean avoidTriggeringFullClassDiscovery, boolean refreshGOEProperties) {

    List goePropsFiles = new ArrayList();
    if (!m_loadPackages) {
      return;
    }

    if (m_packagesLoaded) {
      return;
    }

    m_packagesLoaded = true;
    m_initialPackageLoadingInProcess = true;
    if (establishWekaHome()) {

      // try and load any jar files and add to the classpath
      File[] contents = PACKAGES_DIR.listFiles();

      // if we have a non-empty packages dir then check
      // the integrity of our cache first
      if (contents.length > 0) {
        establishCacheIfNeeded(System.out);
      }

      for (File content : contents) {
        if (content.isDirectory()) {
          try {
            Package toLoad = getInstalledPackageInfo(content.getName());
            boolean load;
            // Only perform the check against the current version of Weka if
            // there exists
            // a Description.props file
            if (toLoad != null) {

              load = loadCheck(toLoad, content, System.err);

              if (load) {
                if (verbose) {
                  System.out.println("[Weka] loading package "
                    + content.getName());
                }
                loadPackageDirectory(content, verbose, goePropsFiles,
                  avoidTriggeringFullClassDiscovery);
              }
            }
          } catch (Exception ex) {
            ex.printStackTrace();
            System.err.println("[Weka] Problem loading package "
              + content.getName() + " skipping...");
          }
        }
      }
    }
    m_initialPackageLoadingInProcess = false;

    // it is best to process all of these after all jars have been
    // inserted into the classpath since the dynamic class discovery
    // mechanism will load classes during the process of determining
    // all implementations of base types, and this can cause problems
    // if processed at the time of package loading and there are
    // dependencies between packages
    if (!avoidTriggeringFullClassDiscovery) {
      for (File f : goePropsFiles) {
        processGenericPropertiesCreatorProps(f);
      }
    }

    // do we need to regenerate the list of available schemes for
    // the GUIs (this is not necessary when executing stuff from
    // the command line)
    if (refreshGOEProperties) {
      System.err.println("Refreshing GOE props...");
      refreshGOEProperties();
    }
  }

  /**
   * Refresh the generic object editor properties via re-running of the dynamic
   * class discovery process.
   */
  public static void refreshGOEProperties() {
    ClassDiscovery.clearClassCache();
    GenericPropertiesCreator.regenerateGlobalOutputProperties();
    GenericObjectEditor.determineClasses();
    ConverterUtils.initialize();
    KnowledgeFlowApp.disposeSingleton();
    KnowledgeFlowApp.reInitialize();
  }

  /**
   * Get the underlying package manager implementation
   * 
   * @return the underlying concrete package management implementation.
   */
  public static PackageManager getUnderlyingPackageManager() {
    return PACKAGE_MANAGER;
  }

  /**
   * Retrieves the size (in KB) of the repository zip archive stored on the
   * server.
   * 
   * @return the size of the repository zip archive in KB.
   */
  public static int repoZipArchiveSize() {
    int size = -1;

    try {
      PACKAGE_MANAGER.setPackageRepositoryURL(REP_URL);
      String numPackagesS =
        PACKAGE_MANAGER.getPackageRepositoryURL().toString() + "/repoSize.txt";
      URLConnection conn = null;
      URL connURL = new URL(numPackagesS);

      if (PACKAGE_MANAGER.setProxyAuthentication(connURL)) {
        conn = connURL.openConnection(PACKAGE_MANAGER.getProxy());
      } else {
        conn = connURL.openConnection();
      }

      conn.setConnectTimeout(30000); // timeout after 30 seconds

      BufferedReader bi =
        new BufferedReader(new InputStreamReader(conn.getInputStream()));

      String n = bi.readLine();
      try {
        size = Integer.parseInt(n);
      } catch (NumberFormatException ne) {
        System.err.println("[WekaPackageManager] problem parsing the size "
          + "of repository zip archive from the server.");
      }
      bi.close();

    } catch (Exception ex) {
      ex.printStackTrace();
    }

    return size;
  }

  /**
   * Get the number of packages that are available at the repository.
   * 
   * @return the number of packages that are available (or -1 if this can't be
   *         determined for some reason.
   */
  public static int numRepositoryPackages() {

    if (m_offline) {
      return -1;
    }
    int numPackages = -1;
    try {
      PACKAGE_MANAGER.setPackageRepositoryURL(REP_URL);
      String numPackagesS =
        PACKAGE_MANAGER.getPackageRepositoryURL().toString()
          + "/numPackages.txt";
      URLConnection conn = null;
      URL connURL = new URL(numPackagesS);

      if (PACKAGE_MANAGER.setProxyAuthentication(connURL)) {
        conn = connURL.openConnection(PACKAGE_MANAGER.getProxy());
      } else {
        conn = connURL.openConnection();
      }

      conn.setConnectTimeout(30000); // timeout after 30 seconds

      BufferedReader bi =
        new BufferedReader(new InputStreamReader(conn.getInputStream()));

      String n = bi.readLine();
      try {
        numPackages = Integer.parseInt(n);
      } catch (NumberFormatException ne) {
        System.err.println("[WekaPackageManager] problem parsing number "
          + "of packages from server.");
      }
      bi.close();

    } catch (Exception ex) {
      ex.printStackTrace();
    }

    return numPackages;
  }

  /**
   * Just get a list of the package names. This is faster than calling
   * getAllPackages(), especially if fetching from the online repository, since
   * the full meta data for each package doesn't have to be read.
   * 
   * @param local true if the local package list in the cache should be read
   *          rather than the online repository
   * 
   * @return a Map of all the package names available either
   *         locally or at the repository
   */
  public static Map getPackageList(boolean local) {
    Map result = new HashMap();

    try {
      useCacheOrOnlineRepository();

      if (!local) {
        PACKAGE_MANAGER.setPackageRepositoryURL(REP_URL);
      }

      String packageListS =
        PACKAGE_MANAGER.getPackageRepositoryURL().toString()
          + "/packageListWithVersion.txt";
      URLConnection conn = null;
      URL connURL = new URL(packageListS);

      if (PACKAGE_MANAGER.setProxyAuthentication(connURL)) {
        conn = connURL.openConnection(PACKAGE_MANAGER.getProxy());
      } else {
        conn = connURL.openConnection();
      }

      conn.setConnectTimeout(30000); // timeout after 30 seconds

      BufferedReader bi =
        new BufferedReader(new InputStreamReader(conn.getInputStream()));
      String l = null;
      while ((l = bi.readLine()) != null) {
        String[] parts = l.split(":");
        if (parts.length == 2) {
          result.put(parts[0], parts[1]);
        }
      }
      bi.close();

    } catch (Exception ex) {
      // ex.printStackTrace();
      result = null;
    }

    return result;
  }

  /**
   * Establish the local copy of the package meta data if needed
   * 
   * @param progress for reporting progress
   * @return any Exception raised or null if all is good
   */
  public static Exception establishCacheIfNeeded(PrintStream... progress) {
    if (m_offline) {
      return null;
    }

    if (REP_MIRROR == null) {
      establishMirror();
    }

    Exception problem = null;
    if (INITIAL_CACHE_BUILD_NEEDED) {
      for (PrintStream p : progress) {
        p.println("Caching repository meta data, please wait...");
      }

      problem = refreshCache(progress);

      INITIAL_CACHE_BUILD_NEEDED = false;
    }
    return problem;
  }

  /**
   * Check for new packages on the server and refresh the local cache if needed
   * 
   * @param progress to report progress to
   * @return any Exception raised or null if all is good
   */
  public static Exception checkForNewPackages(PrintStream... progress) {

    if (m_offline) {
      return null;
    }

    Exception problem = null;

    Map localPackageNameList = getPackageList(true);

    if (localPackageNameList == null) {

      System.err.println("Local package list is missing, trying a "
        + "cache refresh to restore...");
      refreshCache(progress);
      localPackageNameList = getPackageList(true);
      if (localPackageNameList == null) {
        // quietly return and see if we can continue anyway
        return null;
      }
    }

    Map repositoryPackageNameList = getPackageList(false);

    if (repositoryPackageNameList == null) {
      // quietly return and see if we can continue anyway
      return null;
    }

    if (repositoryPackageNameList.keySet().size() != localPackageNameList
      .keySet().size()) {
      // package(s) have disappeared from the repository.
      // Force a cache refresh...
      if (repositoryPackageNameList.keySet().size() < localPackageNameList
        .keySet().size()) {
        for (PrintStream p : progress) {
          p.println("Some packages no longer exist at the repository. "
            + "Refreshing cache...");
        }
      } else {
        for (PrintStream p : progress) {
          p.println("There are new packages at the repository. "
            + "Refreshing cache...");
        }
      }
      problem = refreshCache(progress);
    } else {
      // check for new versions of packages
      boolean refresh = false;
      for (String localPackage : localPackageNameList.keySet()) {
        String localVersion = localPackageNameList.get(localPackage);

        String repoVersion = repositoryPackageNameList.get(localPackage);
        if (repoVersion == null) {
          continue;
        }

        // a difference here indicates a newer version on the server
        if (!localVersion.equals(repoVersion)) {
          refresh = true;
          break;
        }
      }

      if (refresh) {
        for (PrintStream p : progress) {
          p.println("There are newer versions of existing packages "
            + "at the repository. Refreshing cache...");
        }
        problem = refreshCache(progress);
      }
    }

    return problem;
  }

  /**
   * Refresh the local copy of the package meta data
   * 
   * @param progress to report progress to
   * @return any Exception raised or null if all is successful
   */
  public static Exception refreshCache(PrintStream... progress) {
    Exception problem = null;
    if (CACHE_URL == null) {
      return null;
    }

    PACKAGE_MANAGER.setPackageRepositoryURL(REP_URL);
    String cacheDir = WEKA_HOME.toString() + File.separator + "repCache";
    try {
      for (PrintStream p : progress) {
        p.println("Refresh in progress. Please wait...");
      }
      byte[] zip =
        PACKAGE_MANAGER.getRepositoryPackageMetaDataOnlyAsZip(progress);

      ZipInputStream zis = new ZipInputStream(new ByteArrayInputStream(zip));
      ZipEntry ze;
      final byte[] buff = new byte[1024];
      while ((ze = zis.getNextEntry()) != null) {
        // System.out.println("Cache: inflating " + ze.getName());
        if (ze.isDirectory()) {
          new File(cacheDir, ze.getName()).mkdir();
          continue;
        }
        BufferedOutputStream bo =
          new BufferedOutputStream(new FileOutputStream(new File(cacheDir,
            ze.getName())));
        while (true) {
          int amountRead = zis.read(buff);
          if (amountRead == -1) {
            break;
          }
          // write the data here
          bo.write(buff, 0, amountRead);
        }
        bo.close();
      }
    } catch (Exception e) {
      e.printStackTrace();

      // OK, we have a problem with the repository cache - use
      // the repository itself instead and delete repCache
      CACHE_URL = null;
      try {
        DefaultPackageManager.deleteDir(new File(cacheDir), System.out);
      } catch (Exception e1) {
        e1.printStackTrace();
      }

      return e;
    }

    return problem;
  }

  /**
   * Check if a named resource exists in an installed package
   * 
   * @param packageName the name of the package in question
   * @param resourceName the name of the resource to check for
   * @return true if the resource exists in the package
   */
  public static boolean installedPackageResourceExists(String packageName,
    String resourceName) {

    String fullResourcePath =
      getPackageHome().toString() + File.separator + packageName
        + File.separator + resourceName;

    return new File(fullResourcePath).exists();
  }

  /**
   * Determines whether to use the local cache or online repository for meta
   * data
   */
  private static void useCacheOrOnlineRepository() {
    if (REP_MIRROR == null) {
      establishMirror();
    }

    if (CACHE_URL != null) {
      PACKAGE_MANAGER.setPackageRepositoryURL(CACHE_URL);
    } else if (REP_URL != null) {
      PACKAGE_MANAGER.setPackageRepositoryURL(REP_URL);
    }
  }

  public static File getPackageHome() {
    return PACKAGE_MANAGER.getPackageHome();
  }

  /**
   * Find the most recent version of the package encapsulated in the supplied
   * PackageConstraint argument that satisfies the constraint
   * 
   * @param toCheck the PackageConstraint containing the package in question
   * @return the most recent version of the package satisfying the constraint
   * @throws Exception if a version can't be found that satisfies the constraint
   *           or an error occurs while communicating with the respository
   */
  public static Package mostRecentVersionWithRespectToConstraint(
    PackageConstraint toCheck) throws Exception {
    Package target = toCheck.getPackage();
    Package result = null;

    List availableVersions =
      PACKAGE_MANAGER.getRepositoryPackageVersions(target.getName());

    // version numbers will be in descending sorted order from the repository
    // we want the most recent version that meets the target constraint
    for (Object version : availableVersions) {
      Package candidate =
        PACKAGE_MANAGER.getRepositoryPackageInfo(target.getName(), version);
      if (toCheck.checkConstraint(candidate)) {
        result = candidate;
        break;
      }
    }

    if (result == null) {
      throw new Exception("[WekaPackageManager] unable to find a version of "
        + "package " + target.getName() + " that meets constraint "
        + toCheck.toString());
    }

    return result;
  }

  /**
   * Install the supplied list of packages
   * 
   * @param toInstall packages to install
   * @param progress to report progress to
   * @return true if successful
   * @throws Exception if a problem occurs
   */
  public static boolean installPackages(List toInstall,
    PrintStream... progress) throws Exception {

    useCacheOrOnlineRepository();
    List upgrades = new ArrayList();
    for (Package p : toInstall) {
      if (p.isInstalled()) {
        upgrades.add(new Boolean(true));
      } else {
        upgrades.add(new Boolean(false));
      }
    }
    PACKAGE_MANAGER.installPackages(toInstall, progress);
    boolean atLeastOneUpgrade = false;

    List gpcFiles = new ArrayList();
    int i = 0;
    for (Package p : toInstall) {
      boolean isAnUpgrade = upgrades.get(i++);
      if (isAnUpgrade) {
        atLeastOneUpgrade = true;
      }

      String packageName = p.getName();
      File packageDir =
        new File(PACKAGE_MANAGER.getPackageHome().toString() + File.separator
          + packageName);

      boolean loadIt = loadCheck(p, packageDir, progress);

      if (loadIt & !isAnUpgrade) {
        loadPackageDirectory(packageDir, false, gpcFiles, false);
      }
    }

    for (File f : gpcFiles) {
      processGenericPropertiesCreatorProps(f);
    }

    return atLeastOneUpgrade;
  }

  /**
   * Get the versions of the supplied package available on the server
   * 
   * @param packageName the package name to get available versions for
   * @return a list of available versions
   * @throws Exception if a problem occurs
   */
  public static List getRepositoryPackageVersions(String packageName)
    throws Exception {
    useCacheOrOnlineRepository();
    return PACKAGE_MANAGER.getRepositoryPackageVersions(packageName);
  }

  /**
   * Get the package repository URL
   * 
   * @return the package repository URL
   */
  public static URL getPackageRepositoryURL() {
    useCacheOrOnlineRepository();
    return PACKAGE_MANAGER.getPackageRepositoryURL();
  }

  /**
   * Get a list of all packages
   * 
   * @return a list of all packages
   * @throws Exception if a problem occurs
   */
  public static List getAllPackages() throws Exception {
    useCacheOrOnlineRepository();
    return PACKAGE_MANAGER.getAllPackages();
  }

  /**
   * Get a list of all available packages (i.e. those not yet installed(.
   * 
   * @return a list of all available packages
   * @throws Exception if a problem occurs
   */
  public static List getAvailablePackages() throws Exception {
    useCacheOrOnlineRepository();
    return PACKAGE_MANAGER.getAvailablePackages();
  }

  /**
   * Get a list of the most recent version of all available packages (i.e. those
   * not yet installed) that are compatible with the version of Weka that is
   * installed.
   * 
   * @return a list of packages that are compatible with the installed version
   *         of Weka
   * @throws Exception if a problem occurs
   */
  public static List getAvailableCompatiblePackages() throws Exception {
    List allAvail = getAvailablePackages();
    List compatible = new ArrayList();

    for (Package p : allAvail) {
      List availableVersions =
        PACKAGE_MANAGER.getRepositoryPackageVersions(p.getName());

      // version numbers will be in descending sorted order from the repository
      // we want the most recent version that is compatible with the base weka
      // version
      for (Object version : availableVersions) {
        Package versionedPackage =
          getRepositoryPackageInfo(p.getName(), version.toString());
        if (versionedPackage.isCompatibleBaseSystem()) {
          compatible.add(p);
          break;
        }
      }
    }

    return compatible;
  }

  /**
   * Get a list of installed packages
   * 
   * @return a list of installed packages
   * @throws Exception if a problem occurs
   */
  public static List getInstalledPackages() throws Exception {
    useCacheOrOnlineRepository();
    return PACKAGE_MANAGER.getInstalledPackages();
  }

  /**
   * Get a list of dependencies for a given package
   * 
   * @param target the package to get the dependencies for
   * @param conflicts will hold any conflicts
   * @return a list of dependencies for the target package
   * @throws Exception if a problem occurs
   */
  public static List getAllDependenciesForPackage(Package target,
    Map> conflicts) throws Exception {
    useCacheOrOnlineRepository();
    return PACKAGE_MANAGER.getAllDependenciesForPackage(target, conflicts);
  }

  /**
   * Extract meta data from a package archive
   * 
   * @param packageArchivePath the path to the package archive
   * @return the meta data for the package
   * @throws Exception if a problem occurs
   */
  public static Package getPackageArchiveInfo(String packageArchivePath)
    throws Exception {
    useCacheOrOnlineRepository();
    return PACKAGE_MANAGER.getPackageArchiveInfo(packageArchivePath);
  }

  /**
   * Get meta data for an installed package
   * 
   * @param packageName the name of the package
   * @return the meta data for the package
   * @throws Exception if a problem occurs
   */
  public static Package getInstalledPackageInfo(String packageName)
    throws Exception {
    useCacheOrOnlineRepository();
    return PACKAGE_MANAGER.getInstalledPackageInfo(packageName);
  }

  /**
   * Get meta data for the latest version of a package from the repository
   * 
   * @param packageName the name of the package
   * @return the meta data for the package
   * @throws Exception if a problem occurs
   */
  public static Package getRepositoryPackageInfo(String packageName)
    throws Exception {
    useCacheOrOnlineRepository();
    return PACKAGE_MANAGER.getRepositoryPackageInfo(packageName);
  }

  /**
   * Get meta data for a specific version of a package from the repository
   * 
   * @param packageName the name of the package
   * @param version the version to get meta data for
   * @return the meta data for the package
   * @throws Exception if a problem occurs
   */
  public static Package getRepositoryPackageInfo(String packageName,
    String version) throws Exception {
    useCacheOrOnlineRepository();
    return PACKAGE_MANAGER.getRepositoryPackageInfo(packageName, version);
  }

  /**
   * Install a named package by retrieving the location of the archive from the
   * meta data stored in the repository
   * 
   * @param packageName the name of the package to install
   * @param version the version of the package to install
   * @param progress for reporting progress
   * @return true if the package was installed successfully
   * @throws Exception if a problem occurs
   */
  public static boolean installPackageFromRepository(String packageName,
    String version, PrintStream... progress) throws Exception {
    useCacheOrOnlineRepository();
    Package toLoad = getRepositoryPackageInfo(packageName);

    // check to see if a version is already installed. If so, we
    // wont load the updated version into the classpath immediately in
    // order to avoid conflicts, class not found exceptions etc. The
    // user is told to restart Weka for the changes to come into affect
    // anyway, so there is no point in loading the updated package now.
    boolean isAnUpgrade = toLoad.isInstalled();

    Object specialInstallMessage =
      toLoad.getPackageMetaDataElement("MessageToDisplayOnInstallation");
    if (specialInstallMessage != null
      && specialInstallMessage.toString().length() > 0) {
      String siM = specialInstallMessage.toString();
      try {
        siM = Environment.getSystemWide().substitute(siM);
      } catch (Exception ex) {
        // quietly ignore
      }
      String message =
        "**** Special installation message ****\n" + siM
          + "\n**** Special installation message ****";
      for (PrintStream p : progress) {
        p.println(message);
      }
    }

    PACKAGE_MANAGER
      .installPackageFromRepository(packageName, version, progress);
    File packageDir =
      new File(PACKAGE_MANAGER.getPackageHome().toString() + File.separator
        + packageName);

    boolean loadIt = checkForMissingClasses(toLoad, progress);
    if (loadIt && !isAnUpgrade) {
      File packageRoot =
        new File(PACKAGE_MANAGER.getPackageHome() + File.separator
          + packageName);
      loadIt = checkForMissingFiles(toLoad, packageRoot, progress);
      if (loadIt) {
        loadPackageDirectory(packageDir, false, null, false);
      }
    }

    return isAnUpgrade;
  }

  /**
   * Install a package from an archive
   * 
   * @param packageArchivePath the path to the package archive file to install
   * @param progress for reporting progress
   * @return true if the package was installed successfully
   * @throws Exception if a problem occurs
   */
  public static String installPackageFromArchive(String packageArchivePath,
    PrintStream... progress) throws Exception {
    useCacheOrOnlineRepository();
    Package toInstall =
      PACKAGE_MANAGER.getPackageArchiveInfo(packageArchivePath);

    Object specialInstallMessage =
      toInstall.getPackageMetaDataElement("MessageToDisplayOnInstallation");
    if (specialInstallMessage != null
      && specialInstallMessage.toString().length() > 0) {
      String siM = specialInstallMessage.toString();
      try {
        siM = Environment.getSystemWide().substitute(siM);
      } catch (Exception ex) {
        // quietly ignore
      }
      String message =
        "**** Special installation message ****\n" + siM
          + "\n**** Special installation message ****";
      for (PrintStream p : progress) {
        p.println(message);
      }
    }

    PACKAGE_MANAGER.installPackageFromArchive(packageArchivePath, progress);

    boolean loadIt = checkForMissingClasses(toInstall, progress);
    if (loadIt) {
      File packageRoot =
        new File(PACKAGE_MANAGER.getPackageHome() + File.separator
          + toInstall.getName());
      loadIt = checkForMissingFiles(toInstall, packageRoot, progress);
      if (loadIt) {
        loadPackageDirectory(packageRoot, false, null, false);
      }
    }

    return toInstall.getName();
  }

  /**
   * Insstall a package from the supplied URL
   * 
   * @param packageURL the URL to the package archive to install
   * @param progress for reporting progress
   * @return true if the package was installed successfully
   * @throws Exception if a problem occurs
   */
  public static String installPackageFromURL(URL packageURL,
    PrintStream... progress) throws Exception {
    useCacheOrOnlineRepository();
    String packageName =
      PACKAGE_MANAGER.installPackageFromURL(packageURL, progress);

    Package installed = PACKAGE_MANAGER.getInstalledPackageInfo(packageName);

    Object specialInstallMessage =
      installed.getPackageMetaDataElement("MessageToDisplayOnInstallation");
    if (specialInstallMessage != null
      && specialInstallMessage.toString().length() > 0) {
      String message =
        "**** Special installation message ****\n"
          + specialInstallMessage.toString()
          + "\n**** Special installation message ****";
      for (PrintStream p : progress) {
        p.println(message);
      }
    }

    boolean loadIt = checkForMissingClasses(installed, progress);
    if (loadIt) {
      File packageRoot =
        new File(PACKAGE_MANAGER.getPackageHome() + File.separator
          + installed.getName());
      loadIt = checkForMissingFiles(installed, packageRoot, progress);
      if (loadIt) {
        loadPackageDirectory(packageRoot, false, null, false);
      }
    }
    return packageName;
  }

  /**
   * Uninstall a named package
   * 
   * @param packageName the name of the package to remove
   * @param updateKnowledgeFlow true if any Knowledge Flow beans provided by the
   *          package should be deregistered from the KnoweledgeFlow
   * @param progress for reporting progress
   * @throws Exception if a problem occurs
   */
  public static void uninstallPackage(String packageName,
    boolean updateKnowledgeFlow, PrintStream... progress) throws Exception {

    // check to see if this is a KnowledgeFlow package (presence of Beans.props
    // file)
    if (updateKnowledgeFlow) {
      File packageToDel =
        new File(PACKAGE_MANAGER.getPackageHome().toString() + File.separator
          + packageName);
      if (packageToDel.exists() && packageToDel.isDirectory()) {
        File[] contents = packageToDel.listFiles();
        for (File content : contents) {
          if (content.isFile() && content.getPath().endsWith("Beans.props")) {
            // KnowledgeFlow plugin -- remove this properties file from the list
            // of
            // bean plugin props

            KnowledgeFlowApp.removeFromPluginBeanProps(content);
            KnowledgeFlowApp.disposeSingleton();
            break;
          }
        }
      }
    }

    PACKAGE_MANAGER.uninstallPackage(packageName, progress);
  }

  private static void printPackageInfo(Map packageProps) {
    Set keys = packageProps.keySet();
    Iterator i = keys.iterator();

    while (i.hasNext()) {
      Object key = i.next();
      Object value = packageProps.get(key);
      System.out.println(Utils.padLeft(key.toString(), 11) + ":\t"
        + value.toString());
    }
  }

  /**
   * Print meta data on a package
   * 
   * @param packagePath the path to the package to print meta data for
   * @throws Exception if a problem occurs
   */
  protected static void printPackageArchiveInfo(String packagePath)
    throws Exception {
    Map packageProps =
      getPackageArchiveInfo(packagePath).getPackageMetaData();
    printPackageInfo(packageProps);
  }

  /**
   * Print meta data for an installed package
   * 
   * @param packageName the name of the package to print meta data for
   * @throws Exception if a problem occurs
   */
  protected static void printInstalledPackageInfo(String packageName)
    throws Exception {
    Map packageProps =
      getInstalledPackageInfo(packageName).getPackageMetaData();
    printPackageInfo(packageProps);
  }

  /**
   * Print meta data for a package listed in the repository
   * 
   * @param packageName the name of the package to print meta data for
   * @param version the version of the package
   * @throws Exception if a problem occurs
   */
  protected static void printRepositoryPackageInfo(String packageName,
    String version) throws Exception {
    Map packageProps =
      getRepositoryPackageInfo(packageName, version).getPackageMetaData();
    printPackageInfo(packageProps);
  }

  private static String queryUser() {
    java.io.BufferedReader br =
      new java.io.BufferedReader(new java.io.InputStreamReader(System.in));

    String result = null;
    try {
      result = br.readLine();
    } catch (java.io.IOException ex) {
      // ignore
    }

    return result;
  }

  private static void removeInstalledPackage(String packageName, boolean force,
    PrintStream... progress) throws Exception {

    List compromised = new ArrayList();

    // Now check to see which other installed packages depend on this one
    List installedPackages = null;
    if (!force) {
      installedPackages = getInstalledPackages();
      for (Package p : installedPackages) {
        List tempDeps = p.getDependencies();

        for (Dependency d : tempDeps) {
          if (d.getTarget().getPackage().getName().equals(packageName)) {
            // add this installed package to the list
            compromised.add(p);
            break;
          }
        }
      }

      if (compromised.size() > 0) {
        System.out.println("The following installed packages depend on "
          + packageName + " :\n");
        for (Package p : compromised) {
          System.out.println("\t" + p.getName());
        }

        System.out.println("\nDo you wish to proceed [y/n]?");
        String response = queryUser();
        if (response != null
          && (response.equalsIgnoreCase("n") || response.equalsIgnoreCase("no"))) {
          return; // bail out here
        }
      }
    }

    if (force) {
      System.out.println("Forced uninstall.");
    }

    compromised = null;
    installedPackages = null;

    uninstallPackage(packageName, false, progress);
  }

  private static void installPackageFromRepository(String packageName,
    String version, boolean force) throws Exception {

    Package toInstall = null;
    try {
      toInstall = getRepositoryPackageInfo(packageName, version);
    } catch (Exception ex) {
      System.err.println("[WekaPackageManager] Package " + packageName
        + " at version " + version + " doesn't seem to exist!");
      // System.exit(1);
      return;
    }
    // First check to see if this package is compatible with the base system

    if (!force) {
      boolean ok = toInstall.isCompatibleBaseSystem();

      if (!ok) {
        List baseSysDep = toInstall.getBaseSystemDependency();
        StringBuffer depList = new StringBuffer();
        for (Dependency bd : baseSysDep) {
          depList.append(bd.getTarget().toString() + " ");
        }
        System.err.println("Can't install package " + packageName
          + " because it requires " + depList.toString());
        return;
      }

      if (toInstall.isInstalled()) {
        Package installedVersion = getInstalledPackageInfo(packageName);
        if (!toInstall.equals(installedVersion)) {

          System.out.println("Package " + packageName + "[" + installedVersion
            + "] is already installed. Replace with " + toInstall + " [y/n]?");

          String response = queryUser();
          if (response != null
            && (response.equalsIgnoreCase("n") || response
              .equalsIgnoreCase("no"))) {
            return; // bail out here
          }
        } else {
          System.out.println("Package " + packageName + "[" + installedVersion
            + "] is already installed. Install again [y/n]?");
          String response = queryUser();
          if (response != null
            && (response.equalsIgnoreCase("n") || response
              .equalsIgnoreCase("no"))) {
            return; // bail out here
          }
        }
      }

      // Now get a full list of dependencies for this package and
      // check for any conflicts
      Map> conflicts =
        new HashMap>();
      List dependencies =
        getAllDependenciesForPackage(toInstall, conflicts);

      if (conflicts.size() > 0) {
        System.err.println("Package " + packageName
          + " requires the following packages:\n");
        Iterator depI = dependencies.iterator();
        while (depI.hasNext()) {
          Dependency d = depI.next();
          System.err.println("\t" + d);
        }

        System.err.println("\nThere are conflicting dependencies:\n");
        Set pNames = conflicts.keySet();
        Iterator pNameI = pNames.iterator();
        while (pNameI.hasNext()) {
          String pName = pNameI.next();
          System.err.println("Conflicts for " + pName);
          List confsForPackage = conflicts.get(pName);
          Iterator confs = confsForPackage.iterator();
          while (confs.hasNext()) {
            Dependency problem = confs.next();
            System.err.println("\t" + problem);
          }
        }

        System.err.println("Unable to continue with installation.");
        return; // bail out here.
      }

      // Next check all dependencies against what is installed and
      // inform the user about which installed packages will be altered. Also
      // build the list of only those packages that need to be installed or
      // upgraded (excluding those that are already installed and are OK).
      List needsUpgrade = new ArrayList();
      List finalListToInstall = new ArrayList();

      Iterator depI = dependencies.iterator();
      while (depI.hasNext()) {
        Dependency toCheck = depI.next();
        if (toCheck.getTarget().getPackage().isInstalled()) {
          String toCheckName =
            toCheck.getTarget().getPackage()
              .getPackageMetaDataElement("PackageName").toString();
          Package installedVersion =
            PACKAGE_MANAGER.getInstalledPackageInfo(toCheckName);
          if (!toCheck.getTarget().checkConstraint(installedVersion)) {
            needsUpgrade.add(toCheck.getTarget());
            Package mostRecent = toCheck.getTarget().getPackage();
            if (toCheck.getTarget() instanceof org.pentaho.packageManagement.VersionPackageConstraint) {
              mostRecent =
                WekaPackageManager
                  .mostRecentVersionWithRespectToConstraint(toCheck.getTarget());
            }
            finalListToInstall.add(mostRecent);
          }
        } else {
          Package mostRecent = toCheck.getTarget().getPackage();
          if (toCheck.getTarget() instanceof org.pentaho.packageManagement.VersionPackageConstraint) {
            mostRecent =
              WekaPackageManager
                .mostRecentVersionWithRespectToConstraint(toCheck.getTarget());
          }
          finalListToInstall.add(mostRecent);
        }
      }

      if (needsUpgrade.size() > 0) {
        System.out
          .println("The following packages will be upgraded in order to install "
            + packageName);
        Iterator upI = needsUpgrade.iterator();
        while (upI.hasNext()) {
          PackageConstraint tempC = upI.next();
          System.out.println("\t" + tempC);
        }

        System.out.print("\nOK to continue [y/n]? > ");
        String response = queryUser();
        if (response != null
          && (response.equalsIgnoreCase("n") || response.equalsIgnoreCase("no"))) {
          return; // bail out here
        }

        // now take a look at the other installed packages and see if
        // any would have a problem when these ones are upgraded
        boolean conflictsAfterUpgrade = false;
        List installed = getInstalledPackages();
        List toUpgrade = new ArrayList();
        upI = needsUpgrade.iterator();
        while (upI.hasNext()) {
          toUpgrade.add(upI.next().getPackage());
        }

        // add the actual package the user is wanting to install if it
        // is going to be an up/downgrade rather than a first install since
        // other installed packages may depend on the currently installed
        // version
        // and thus could be affected after the up/downgrade
        toUpgrade.add(toInstall);

        for (int i = 0; i < installed.size(); i++) {
          Package tempP = installed.get(i);
          String tempPName = tempP.getName();
          boolean checkIt = true;
          for (int j = 0; j < needsUpgrade.size(); j++) {
            if (tempPName.equals(needsUpgrade.get(j).getPackage().getName())) {
              checkIt = false;
              break;
            }
          }

          if (checkIt) {
            List problem =
              tempP.getIncompatibleDependencies(toUpgrade);
            if (problem.size() > 0) {
              conflictsAfterUpgrade = true;

              System.err.println("Package " + tempP.getName()
                + " will have a compatibility"
                + "problem with the following packages after upgrading them:");
              Iterator dI = problem.iterator();
              while (dI.hasNext()) {
                System.err.println("\t" + dI.next().getTarget().getPackage());
              }
            }
          }
        }

        if (conflictsAfterUpgrade) {
          System.err.println("Unable to continue with installation.");
          return; // bail out here
        }

      }

      if (finalListToInstall.size() > 0) {
        System.out.println("To install " + packageName
          + " the following packages will" + " be installed/upgraded:\n\n");
        Iterator i = finalListToInstall.iterator();
        while (i.hasNext()) {
          System.out.println("\t" + i.next());
        }
        System.out.print("\nOK to proceed [y/n]? > ");
        String response = queryUser();

        if (response != null
          && (response.equalsIgnoreCase("n") || response.equalsIgnoreCase("no"))) {
          return; // bail out here
        }
      }

      // OK, now we can download and install everything

      // First install the final list of dependencies
      installPackages(finalListToInstall, System.out);

      // Now install the package itself
      installPackageFromRepository(packageName, version, System.out);

    } else {
      // just install this package without checking/downloading dependencies
      // etc.
      installPackageFromRepository(packageName, version, System.out);
    }
  }

  private static void listPackages(String arg) throws Exception {

    if (m_offline
      && (arg.equalsIgnoreCase("all") || arg.equalsIgnoreCase("available"))) {
      System.out.println("Running offline - unable to display "
        + "available or all package information");
      return;
    }

    List packageList = null;
    useCacheOrOnlineRepository();

    if (arg.equalsIgnoreCase("all")) {
      packageList = PACKAGE_MANAGER.getAllPackages();
    } else if (arg.equals("installed")) {
      packageList = PACKAGE_MANAGER.getInstalledPackages();
    } else if (arg.equals("available")) {
      packageList = PACKAGE_MANAGER.getAvailablePackages();
    } else {
      System.err.println("[WekaPackageManager] Unknown argument " + arg);
      printUsage();
      // System.exit(1);
      return;
    }

    StringBuffer result = new StringBuffer();
    result.append("Installed\tRepository\tLoaded\tPackage\n");
    result.append("=========\t==========\t======\t=======\n");

    boolean userOptedNoLoad = false;
    Iterator i = packageList.iterator();
    while (i.hasNext()) {
      Package p = i.next();
      String installedV = "-----    ";
      String repositoryV = "-----     ";
      String loaded = "No";
      if (p.isInstalled()) {
        Package installedP = getInstalledPackageInfo(p.getName());
        if (loadCheck(installedP, new File(WekaPackageManager.getPackageHome()
          .toString() + File.separator + p.getName()))) {
          loaded = "Yes";
        } else {
          if (m_doNotLoadList.contains(installedP.getName())) {
            loaded = "No*";
            userOptedNoLoad = true;
          }
        }
        installedV =
          installedP.getPackageMetaDataElement("Version").toString() + "    ";
        try {
          if (!m_offline) {
            Package repP = getRepositoryPackageInfo(p.getName());
            repositoryV =
              repP.getPackageMetaDataElement("Version").toString() + "     ";
          }
        } catch (Exception ex) {
          // not at the repository
        }
      } else {
        repositoryV =
          p.getPackageMetaDataElement("Version").toString() + "     ";
      }
      String title = p.getPackageMetaDataElement("Title").toString();
      result.append(installedV + "\t" + repositoryV + "\t" + loaded + "\t"
        + p.getName() + ": " + title + "\n");
    }
    if (userOptedNoLoad) {
      result.append("* User flagged as no load\n");
    }

    System.out.println(result.toString());
  }

  private static void printUsage() {
    System.out
      .println("Usage: weka.core.WekaPackageManager [-offline] [option]");
    System.out
      .println("Options:\n"
        + "\t-list-packages \n"
        + "\t-package-info  "
        + "\n\t-install-package  [version]\n"
        + "\t-uninstall-package packageName\n"
        + "\t-toggle-load-status packageName [packageName packageName ...]\n"
        + "\t-refresh-cache");
  }

  /**
   * Main method for using the package manager from the command line
   * 
   * @param args command line arguments
   */
  public static void main(String[] args) {
    weka.core.logging.Logger.log(weka.core.logging.Logger.Level.INFO,
      "Logging started");
    try {

      // scan for -offline
      for (int i = 0; i < args.length; i++) {
        if (args[i].equals("-offline")) {
          m_offline = true;
          String[] temp = new String[args.length - 1];
          if (i > 0) {
            System.arraycopy(args, 0, temp, 0, i);
          }
          System.arraycopy(args, i + 1, temp, i, args.length - (i + 1));
          args = temp;
        }
      }

      establishCacheIfNeeded(System.out);
      checkForNewPackages(System.out);

      if (args.length == 0 || args[0].equalsIgnoreCase("-h")
        || args[0].equalsIgnoreCase("-help")) {
        printUsage();
        // System.exit(1);
        return;
      }

      if (args[0].equals("-package-info")) {
        if (args.length < 3) {
          printUsage();
          return;
          // System.exit(1);
        }
        if (args[1].equals("archive")) {
          printPackageArchiveInfo(args[2]);
        } else if (args[1].equals("installed")) {
          printInstalledPackageInfo(args[2]);
        } else if (args[1].equals("repository")) {
          String version = "Latest";
          if (args.length == 4) {
            version = args[3];
          }
          try {
            printRepositoryPackageInfo(args[2], version);
          } catch (Exception ex) {
            // problem with getting info on package from repository?
            // Must not be an "official" repository package
            System.out
              .println("[WekaPackageManager] Nothing known about package "
                + args[2] + " at the repository!");
          }
        } else {
          System.err
            .println("[WekaPackageManager] Unknown argument " + args[2]);
          printUsage();
          return;
          // System.exit(1);
        }
      } else if (args[0].equals("-install-package")) {
        String targetLowerCase = args[1].toLowerCase();
        if (targetLowerCase.startsWith("http://")
          || targetLowerCase.startsWith("https://")) {
          URL packageURL = new URL(args[1]);
          installPackageFromURL(packageURL, System.out);
        } else if (targetLowerCase.endsWith(".zip")) {
          installPackageFromArchive(args[1], System.out);
        } else {
          // assume a named package at the central repository
          String version = "Latest";
          if (args.length == 3) {
            version = args[2];
          }
          installPackageFromRepository(args[1], version, false);
        }
      } else if (args[0].equals("-uninstall-package")) {
        if (args.length < 2) {
          printUsage();
          return;
          // System.exit(1);
        }
        boolean force = false;

        if (args.length == 3) {
          if (args[2].equals("-force")) {
            force = true;
          }
        }

        removeInstalledPackage(args[1], force, System.out);
        // System.exit(0);
        return;
      } else if (args[0].equals("-list-packages")) {
        if (args.length < 2) {
          printUsage();
          // System.exit(1);
          return;
        }
        listPackages(args[1]);

      } else if (args[0].equals("-toggle-load-status")) {
        if (args.length == 1) {
          printUsage();
          return;
        }
        List toToggle = new ArrayList();
        for (int i = 1; i < args.length; i++) {
          toToggle.add(args[i].trim());
        }
        if (toToggle.size() >= 1) {
          toggleLoadStatus(toToggle);
        }
      } else if (args[0].equals("-refresh-cache")) {
        refreshCache(System.out);
      } else {
        System.err.println("Unknown option: " + args[0]);
        printUsage();
      }

      System.exit(0);
    } catch (Exception ex) {
      ex.printStackTrace();
    }
  }
}